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
36namespace app {
37
38Context::Context()
39 : m_docs(this)
40 , m_lastSelectedDoc(nullptr)
41 , m_preferences(nullptr)
42{
43 m_docs.add_observer(this);
44}
45
46Context::~Context()
47{
48 if (m_preferences)
49 m_docs.remove_observer(m_preferences.get());
50
51 m_docs.remove_observer(this);
52}
53
54Preferences& 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
63Clipboard* 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
73void Context::sendDocumentToTop(Doc* document)
74{
75 ASSERT(document != NULL);
76
77 documents().move(document, 0);
78}
79
80void Context::closeDocument(Doc* doc)
81{
82 onCloseDocument(doc);
83}
84
85Site Context::activeSite() const
86{
87 Site site;
88 onGetActiveSite(&site);
89 return site;
90}
91
92Doc* Context::activeDocument() const
93{
94 Site site;
95 onGetActiveSite(&site);
96 return site.document();
97}
98
99void Context::setActiveDocument(Doc* document)
100{
101 onSetActiveDocument(document, true);
102}
103
104void Context::setActiveLayer(doc::Layer* layer)
105{
106 onSetActiveLayer(layer);
107}
108
109void Context::setActiveFrame(const doc::frame_t frame)
110{
111 onSetActiveFrame(frame);
112}
113
114void Context::setRange(const DocRange& range)
115{
116 onSetRange(range);
117}
118
119void Context::setSelectedColors(const doc::PalettePicks& picks)
120{
121 onSetSelectedColors(picks);
122}
123
124void Context::setSelectedTiles(const doc::PalettePicks& picks)
125{
126 onSetSelectedTiles(picks);
127}
128
129bool Context::hasModifiedDocuments() const
130{
131 for (auto doc : documents())
132 if (doc->isModified())
133 return true;
134 return false;
135}
136
137void Context::notifyActiveSiteChanged()
138{
139 Site site = activeSite();
140 notify_observers<const Site&>(&ContextObserver::onActiveSiteChange, site);
141}
142
143void 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
162void 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
247void Context::setCommandResult(const CommandResult& result)
248{
249 m_result = result;
250}
251
252void Context::onAddDocument(Doc* doc)
253{
254 m_lastSelectedDoc = doc;
255
256 if (m_activeSiteHandler)
257 m_activeSiteHandler->addDoc(doc);
258
259 notifyActiveSiteChanged();
260}
261
262void 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
273void 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
280void Context::onSetActiveDocument(Doc* doc, bool notify)
281{
282 m_lastSelectedDoc = doc;
283 if (notify)
284 notifyActiveSiteChanged();
285}
286
287void 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
301void Context::onSetActiveFrame(const doc::frame_t frame)
302{
303 if (m_lastSelectedDoc)
304 activeSiteHandler()->setActiveFrameInDoc(m_lastSelectedDoc, frame);
305
306 notifyActiveSiteChanged();
307}
308
309void Context::onSetRange(const DocRange& range)
310{
311 if (m_lastSelectedDoc)
312 activeSiteHandler()->setRangeInDoc(m_lastSelectedDoc, range);
313}
314
315void Context::onSetSelectedColors(const doc::PalettePicks& picks)
316{
317 if (m_lastSelectedDoc)
318 activeSiteHandler()->setSelectedColorsInDoc(m_lastSelectedDoc, picks);
319}
320
321void Context::onSetSelectedTiles(const doc::PalettePicks& picks)
322{
323 if (m_lastSelectedDoc)
324 activeSiteHandler()->setSelectedTilesInDoc(m_lastSelectedDoc, picks);
325}
326
327ActiveSiteHandler* Context::activeSiteHandler() const
328{
329 if (!m_activeSiteHandler)
330 m_activeSiteHandler.reset(new ActiveSiteHandler);
331 return m_activeSiteHandler.get();
332}
333
334void Context::onCloseDocument(Doc* doc)
335{
336 ASSERT(doc != nullptr);
337 ASSERT(doc->context() == nullptr);
338 delete doc;
339}
340
341} // namespace app
342