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/context.h" |
13 | |
14 | #include "app/active_site_handler.h" |
15 | #include "app/app.h" |
16 | #include "app/commands/command.h" |
17 | #include "app/commands/commands.h" |
18 | #include "app/console.h" |
19 | #include "app/doc.h" |
20 | #include "app/pref/preferences.h" |
21 | #include "app/site.h" |
22 | #include "app/util/clipboard.h" |
23 | #include "base/scoped_value.h" |
24 | #include "doc/layer.h" |
25 | #include "ui/system.h" |
26 | |
27 | #ifdef _DEBUG |
28 | #include "doc/layer_tilemap.h" |
29 | #include "doc/tileset.h" |
30 | #include "doc/tilesets.h" |
31 | #endif |
32 | |
33 | #include <algorithm> |
34 | #include <stdexcept> |
35 | |
36 | namespace app { |
37 | |
38 | Context::Context() |
39 | : m_docs(this) |
40 | , m_lastSelectedDoc(nullptr) |
41 | , m_preferences(nullptr) |
42 | { |
43 | m_docs.add_observer(this); |
44 | } |
45 | |
46 | Context::~Context() |
47 | { |
48 | if (m_preferences) |
49 | m_docs.remove_observer(m_preferences.get()); |
50 | |
51 | m_docs.remove_observer(this); |
52 | } |
53 | |
54 | Preferences& Context::preferences() const |
55 | { |
56 | if (!m_preferences) { |
57 | m_preferences.reset(new Preferences); |
58 | m_docs.add_observer(m_preferences.get()); |
59 | } |
60 | return *m_preferences; |
61 | } |
62 | |
63 | Clipboard* Context::clipboard() const |
64 | { |
65 | #ifdef ENABLE_UI |
66 | return Clipboard::instance(); |
67 | #else |
68 | // TODO support clipboard when !ENABLE_UI |
69 | throw std::runtime_error("Clipboard not supported" ); |
70 | #endif |
71 | } |
72 | |
73 | void Context::sendDocumentToTop(Doc* document) |
74 | { |
75 | ASSERT(document != NULL); |
76 | |
77 | documents().move(document, 0); |
78 | } |
79 | |
80 | void Context::closeDocument(Doc* doc) |
81 | { |
82 | onCloseDocument(doc); |
83 | } |
84 | |
85 | Site Context::activeSite() const |
86 | { |
87 | Site site; |
88 | onGetActiveSite(&site); |
89 | return site; |
90 | } |
91 | |
92 | Doc* Context::activeDocument() const |
93 | { |
94 | Site site; |
95 | onGetActiveSite(&site); |
96 | return site.document(); |
97 | } |
98 | |
99 | void Context::setActiveDocument(Doc* document) |
100 | { |
101 | onSetActiveDocument(document, true); |
102 | } |
103 | |
104 | void Context::setActiveLayer(doc::Layer* layer) |
105 | { |
106 | onSetActiveLayer(layer); |
107 | } |
108 | |
109 | void Context::setActiveFrame(const doc::frame_t frame) |
110 | { |
111 | onSetActiveFrame(frame); |
112 | } |
113 | |
114 | void Context::setRange(const DocRange& range) |
115 | { |
116 | onSetRange(range); |
117 | } |
118 | |
119 | void Context::setSelectedColors(const doc::PalettePicks& picks) |
120 | { |
121 | onSetSelectedColors(picks); |
122 | } |
123 | |
124 | void Context::setSelectedTiles(const doc::PalettePicks& picks) |
125 | { |
126 | onSetSelectedTiles(picks); |
127 | } |
128 | |
129 | bool Context::hasModifiedDocuments() const |
130 | { |
131 | for (auto doc : documents()) |
132 | if (doc->isModified()) |
133 | return true; |
134 | return false; |
135 | } |
136 | |
137 | void Context::notifyActiveSiteChanged() |
138 | { |
139 | Site site = activeSite(); |
140 | notify_observers<const Site&>(&ContextObserver::onActiveSiteChange, site); |
141 | } |
142 | |
143 | void Context::executeCommandFromMenuOrShortcut(Command* command, const Params& params) |
144 | { |
145 | ui::assert_ui_thread(); |
146 | |
147 | // With this we avoid executing a command when we are inside another |
148 | // command (e.g. if we press Cmd-S quickly the program can enter two |
149 | // times in the File > Save command and hang). |
150 | static Command* executingCommand = nullptr; |
151 | if (executingCommand) { // Ignore command execution |
152 | LOG(VERBOSE, "CTXT: Ignoring command %s because we are inside %s\n" , |
153 | command->id().c_str(), executingCommand->id().c_str()); |
154 | return; |
155 | } |
156 | base::ScopedValue<Command*> commandGuard(executingCommand, |
157 | command, nullptr); |
158 | |
159 | executeCommand(command, params); |
160 | } |
161 | |
162 | void Context::executeCommand(Command* command, const Params& params) |
163 | { |
164 | ASSERT(command); |
165 | if (!command) |
166 | return; |
167 | |
168 | m_result.reset(); |
169 | |
170 | Console console; |
171 | LOG(VERBOSE, "CTXT: Executing command %s\n" , command->id().c_str()); |
172 | try { |
173 | m_flags.update(this); |
174 | |
175 | #if 0 |
176 | // params.empty() can be empty when we call the command from Lua |
177 | // with a table. |
178 | ASSERT(!command->needsParams() || !params.empty()); |
179 | #endif |
180 | |
181 | command->loadParams(params); |
182 | |
183 | CommandExecutionEvent ev(command); |
184 | BeforeCommandExecution(ev); |
185 | |
186 | if (ev.isCanceled()) { |
187 | LOG(VERBOSE, "CTXT: Command %s was canceled/simulated.\n" , command->id().c_str()); |
188 | } |
189 | else if (command->isEnabled(this)) { |
190 | command->execute(this); |
191 | LOG(VERBOSE, "CTXT: Command %s executed successfully\n" , command->id().c_str()); |
192 | } |
193 | else { |
194 | LOG(VERBOSE, "CTXT: Command %s is disabled\n" , command->id().c_str()); |
195 | } |
196 | |
197 | AfterCommandExecution(ev); |
198 | |
199 | // TODO move this code to another place (e.g. a Workplace/Tabs widget) |
200 | if (isUIAvailable()) |
201 | app_rebuild_documents_tabs(); |
202 | |
203 | #ifdef _DEBUG // Special checks for debugging purposes |
204 | { |
205 | Site site = activeSite(); |
206 | // Check that all tileset hash tables are valid |
207 | if (site.sprite() && |
208 | site.sprite()->hasTilesets()) { |
209 | for (Tileset* tileset : *site.sprite()->tilesets()) { |
210 | if (tileset) |
211 | tileset->assertValidHashTable(); |
212 | } |
213 | } |
214 | } |
215 | #endif |
216 | } |
217 | catch (base::Exception& e) { |
218 | m_result = CommandResult(CommandResult::kError); |
219 | |
220 | LOG(ERROR, "CTXT: Exception caught executing %s command\n%s\n" , |
221 | command->id().c_str(), e.what()); |
222 | Console::showException(e); |
223 | } |
224 | catch (std::exception& e) { |
225 | m_result = CommandResult(CommandResult::kError); |
226 | |
227 | LOG(ERROR, "CTXT: std::exception caught executing %s command\n%s\n" , |
228 | command->id().c_str(), e.what()); |
229 | console.printf("An error ocurred executing the command.\n\nDetails:\n%s" , e.what()); |
230 | } |
231 | #ifdef NDEBUG |
232 | catch (...) { |
233 | m_result = CommandResult(CommandResult::kError); |
234 | |
235 | LOG(ERROR, "CTXT: Unknown exception executing %s command\n" , |
236 | command->id().c_str()); |
237 | |
238 | console.printf("An unknown error ocurred executing the command.\n" |
239 | "Please save your work, close the program, try it\n" |
240 | "again, and report this bug.\n\n" |
241 | "Details: Unknown exception caught. This can be bad\n" |
242 | "memory access, divison by zero, etc." ); |
243 | } |
244 | #endif |
245 | } |
246 | |
247 | void Context::setCommandResult(const CommandResult& result) |
248 | { |
249 | m_result = result; |
250 | } |
251 | |
252 | void Context::onAddDocument(Doc* doc) |
253 | { |
254 | m_lastSelectedDoc = doc; |
255 | |
256 | if (m_activeSiteHandler) |
257 | m_activeSiteHandler->addDoc(doc); |
258 | |
259 | notifyActiveSiteChanged(); |
260 | } |
261 | |
262 | void Context::onRemoveDocument(Doc* doc) |
263 | { |
264 | if (m_activeSiteHandler) |
265 | m_activeSiteHandler->removeDoc(doc); |
266 | |
267 | if (doc == m_lastSelectedDoc) { |
268 | m_lastSelectedDoc = nullptr; |
269 | notifyActiveSiteChanged(); |
270 | } |
271 | } |
272 | |
273 | void Context::onGetActiveSite(Site* site) const |
274 | { |
275 | // Default/dummy site (maybe for batch/command line mode) |
276 | if (Doc* doc = m_lastSelectedDoc) |
277 | activeSiteHandler()->getActiveSiteForDoc(doc, site); |
278 | } |
279 | |
280 | void Context::onSetActiveDocument(Doc* doc, bool notify) |
281 | { |
282 | m_lastSelectedDoc = doc; |
283 | if (notify) |
284 | notifyActiveSiteChanged(); |
285 | } |
286 | |
287 | void Context::onSetActiveLayer(doc::Layer* layer) |
288 | { |
289 | Doc* newDoc = (layer ? static_cast<Doc*>(layer->sprite()->document()): nullptr); |
290 | if (!newDoc) |
291 | return; |
292 | |
293 | activeSiteHandler()->setActiveLayerInDoc(newDoc, layer); |
294 | |
295 | if (newDoc != m_lastSelectedDoc) |
296 | setActiveDocument(newDoc); |
297 | else |
298 | notifyActiveSiteChanged(); |
299 | } |
300 | |
301 | void Context::onSetActiveFrame(const doc::frame_t frame) |
302 | { |
303 | if (m_lastSelectedDoc) |
304 | activeSiteHandler()->setActiveFrameInDoc(m_lastSelectedDoc, frame); |
305 | |
306 | notifyActiveSiteChanged(); |
307 | } |
308 | |
309 | void Context::onSetRange(const DocRange& range) |
310 | { |
311 | if (m_lastSelectedDoc) |
312 | activeSiteHandler()->setRangeInDoc(m_lastSelectedDoc, range); |
313 | } |
314 | |
315 | void Context::onSetSelectedColors(const doc::PalettePicks& picks) |
316 | { |
317 | if (m_lastSelectedDoc) |
318 | activeSiteHandler()->setSelectedColorsInDoc(m_lastSelectedDoc, picks); |
319 | } |
320 | |
321 | void Context::onSetSelectedTiles(const doc::PalettePicks& picks) |
322 | { |
323 | if (m_lastSelectedDoc) |
324 | activeSiteHandler()->setSelectedTilesInDoc(m_lastSelectedDoc, picks); |
325 | } |
326 | |
327 | ActiveSiteHandler* Context::activeSiteHandler() const |
328 | { |
329 | if (!m_activeSiteHandler) |
330 | m_activeSiteHandler.reset(new ActiveSiteHandler); |
331 | return m_activeSiteHandler.get(); |
332 | } |
333 | |
334 | void Context::onCloseDocument(Doc* doc) |
335 | { |
336 | ASSERT(doc != nullptr); |
337 | ASSERT(doc->context() == nullptr); |
338 | delete doc; |
339 | } |
340 | |
341 | } // namespace app |
342 | |