| 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 | |