1// Aseprite
2// Copyright (C) 2018-2021 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/ui/doc_view.h"
13
14#include "app/app.h"
15#include "app/app_menus.h"
16#include "app/cmd/clear_mask.h"
17#include "app/cmd/deselect_mask.h"
18#include "app/cmd/trim_cel.h"
19#include "app/commands/commands.h"
20#include "app/console.h"
21#include "app/context_access.h"
22#include "app/doc_access.h"
23#include "app/doc_event.h"
24#include "app/i18n/strings.h"
25#include "app/modules/editors.h"
26#include "app/modules/palettes.h"
27#include "app/pref/preferences.h"
28#include "app/tx.h"
29#include "app/ui/editor/editor.h"
30#include "app/ui/editor/editor_customization_delegate.h"
31#include "app/ui/editor/editor_view.h"
32#include "app/ui/editor/navigate_state.h"
33#include "app/ui/keyboard_shortcuts.h"
34#include "app/ui/main_window.h"
35#include "app/ui/status_bar.h"
36#include "app/ui/timeline/timeline.h"
37#include "app/ui/workspace.h"
38#include "app/ui_context.h"
39#include "app/util/clipboard.h"
40#include "app/util/range_utils.h"
41#include "base/fs.h"
42#include "doc/color.h"
43#include "doc/layer.h"
44#include "doc/sprite.h"
45#include "fmt/format.h"
46#include "ui/accelerator.h"
47#include "ui/alert.h"
48#include "ui/menu.h"
49#include "ui/message.h"
50#include "ui/system.h"
51#include "ui/view.h"
52
53#include <typeinfo>
54
55namespace app {
56
57using namespace ui;
58
59namespace {
60
61// Used to show a view temporarily (the one with the file to be
62// closed) and restore the previous view. E.g. When we close the
63// non-active sprite pressing the cross button in a sprite tab.
64class SetRestoreDocView {
65public:
66 SetRestoreDocView(UIContext* ctx, DocView* newView)
67 : m_ctx(ctx)
68 , m_oldView(ctx->activeView()) {
69 if (newView != m_oldView)
70 m_ctx->setActiveView(newView);
71 else
72 m_oldView = nullptr;
73 }
74
75 ~SetRestoreDocView() {
76 if (m_oldView)
77 m_ctx->setActiveView(m_oldView);
78 }
79
80private:
81 UIContext* m_ctx;
82 DocView* m_oldView;
83};
84
85class AppEditor : public Editor,
86 public EditorObserver,
87 public EditorCustomizationDelegate {
88public:
89 AppEditor(Doc* document,
90 DocViewPreviewDelegate* previewDelegate)
91 : Editor(document)
92 , m_previewDelegate(previewDelegate) {
93 add_observer(this);
94 setCustomizationDelegate(this);
95 }
96
97 ~AppEditor() {
98 remove_observer(this);
99 setCustomizationDelegate(NULL);
100 }
101
102 // EditorObserver implementation
103 void dispose() override {
104 m_previewDelegate->onDisposeOtherEditor(this);
105 }
106
107 void onScrollChanged(Editor* editor) override {
108 m_previewDelegate->onScrollOtherEditor(this);
109
110 if (isActive())
111 StatusBar::instance()->updateFromEditor(this);
112 }
113
114 void onAfterFrameChanged(Editor* editor) override {
115 m_previewDelegate->onPreviewOtherEditor(this);
116
117 if (isActive())
118 set_current_palette(editor->sprite()->palette(editor->frame()), false);
119 }
120
121 void onAfterLayerChanged(Editor* editor) override {
122 m_previewDelegate->onPreviewOtherEditor(this);
123 }
124
125 // EditorCustomizationDelegate implementation
126 tools::Tool* getQuickTool(tools::Tool* currentTool) override {
127 return KeyboardShortcuts::instance()
128 ->getCurrentQuicktool(currentTool);
129 }
130
131 KeyAction getPressedKeyAction(KeyContext context) override {
132 return KeyboardShortcuts::instance()->getCurrentActionModifiers(context);
133 }
134
135 TagProvider* getTagProvider() override {
136 return App::instance()->mainWindow()->getTimeline();
137 }
138
139protected:
140 bool onProcessMessage(Message* msg) override {
141 switch (msg->type()) {
142
143 case kKeyDownMessage:
144 case kKeyUpMessage:
145 if (static_cast<KeyMessage*>(msg)->repeat() == 0) {
146 KeyboardShortcuts* keys = KeyboardShortcuts::instance();
147 KeyPtr lmb = keys->action(KeyAction::LeftMouseButton, KeyContext::Any);
148 KeyPtr rmb = keys->action(KeyAction::RightMouseButton, KeyContext::Any);
149
150 // Convert action keys into mouse messages.
151 if (lmb->isPressed(msg, *keys) ||
152 rmb->isPressed(msg, *keys)) {
153 MouseMessage mouseMsg(
154 (msg->type() == kKeyDownMessage ? kMouseDownMessage: kMouseUpMessage),
155 PointerType::Unknown,
156 (lmb->isPressed(msg, *keys) ? kButtonLeft: kButtonRight),
157 msg->modifiers(),
158 mousePosInDisplay());
159
160 sendMessage(&mouseMsg);
161 return true;
162 }
163 }
164 break;
165 }
166
167 try {
168 return Editor::onProcessMessage(msg);
169 }
170 catch (const std::exception& ex) {
171 EditorState* state = getState().get();
172
173 Console console;
174 Console::showException(ex);
175 console.printf("\nInternal details:\n"
176 "- Message type: %d\n"
177 "- Editor state: %s\n",
178 msg->type(),
179 state ? typeid(*state).name(): "None");
180 return false;
181 }
182 }
183
184private:
185 DocViewPreviewDelegate* m_previewDelegate;
186};
187
188class PreviewEditor : public Editor,
189 public EditorCustomizationDelegate {
190public:
191 PreviewEditor(Doc* document)
192 : Editor(document,
193 Editor::kShowOutside, // Don't show grid/mask in preview preview
194 std::make_shared<NavigateState>())
195 {
196 setCustomizationDelegate(this);
197 }
198
199 ~PreviewEditor() {
200 // As we are destroying this instance, we have to remove it as the
201 // customization delegate. Editor::~Editor() will call
202 // setCustomizationDelegate(nullptr) too which triggers a
203 // EditorCustomizationDelegate::dispose() if the customization
204 // isn't nullptr.
205 setCustomizationDelegate(nullptr);
206 }
207
208 // EditorCustomizationDelegate implementation
209 void dispose() override {
210 // Do nothing
211 }
212
213 tools::Tool* getQuickTool(tools::Tool* currentTool) override {
214 return nullptr;
215 }
216
217 KeyAction getPressedKeyAction(KeyContext context) override {
218 return KeyAction::None;
219 }
220
221 TagProvider* getTagProvider() override {
222 return App::instance()->mainWindow()->getTimeline();
223 }
224};
225
226} // anonymous namespace
227
228DocView::DocView(Doc* document, Type type,
229 DocViewPreviewDelegate* previewDelegate)
230 : Box(VERTICAL)
231 , m_type(type)
232 , m_document(document)
233 , m_view(new EditorView(type == Normal ? EditorView::CurrentEditorMode:
234 EditorView::AlwaysSelected))
235 , m_previewDelegate(previewDelegate)
236 , m_editor((type == Normal ?
237 (Editor*)new AppEditor(document, previewDelegate):
238 (Editor*)new PreviewEditor(document)))
239{
240 addChild(m_view);
241
242 m_view->attachToView(m_editor);
243 m_view->setExpansive(true);
244
245 m_editor->setDocView(this);
246 m_document->add_observer(this);
247}
248
249DocView::~DocView()
250{
251 m_document->remove_observer(this);
252 delete m_editor;
253}
254
255void DocView::getSite(Site* site) const
256{
257 m_editor->getSite(site);
258}
259
260std::string DocView::getTabText()
261{
262 return m_document->name();
263}
264
265TabIcon DocView::getTabIcon()
266{
267 return TabIcon::NONE;
268}
269
270gfx::Color DocView::getTabColor()
271{
272 color_t c = m_editor->sprite()->userData().color();
273 return gfx::rgba(doc::rgba_getr(c), doc::rgba_getg(c), doc::rgba_getb(c), doc::rgba_geta(c));
274}
275
276WorkspaceView* DocView::cloneWorkspaceView()
277{
278 return new DocView(m_document, Normal, m_previewDelegate);
279}
280
281void DocView::onWorkspaceViewSelected()
282{
283 if (auto statusBar = StatusBar::instance())
284 statusBar->showDefaultText(m_document);
285}
286
287void DocView::onClonedFrom(WorkspaceView* from)
288{
289 Editor* newEditor = this->editor();
290 Editor* srcEditor = static_cast<DocView*>(from)->editor();
291
292 newEditor->setLayer(srcEditor->layer());
293 newEditor->setFrame(srcEditor->frame());
294 newEditor->setZoom(srcEditor->zoom());
295
296 View::getView(newEditor)
297 ->setViewScroll(View::getView(srcEditor)->viewScroll());
298}
299
300bool DocView::onCloseView(Workspace* workspace, bool quitting)
301{
302 if (m_editor->isMovingPixels())
303 m_editor->dropMovingPixels();
304
305 // If there is another view for this document, just close the view.
306 for (auto view : *workspace) {
307 DocView* docView = dynamic_cast<DocView*>(view);
308 if (docView && docView != this &&
309 docView->document() == document()) {
310 workspace->removeView(this);
311 delete this;
312 return true;
313 }
314 }
315
316 UIContext* ctx = UIContext::instance();
317 SetRestoreDocView restoreView(ctx, this);
318 bool save_it;
319 bool try_again = true;
320
321 while (try_again) {
322 // This flag indicates if we have to sabe the sprite before to destroy it
323 save_it = false;
324
325 // See if the sprite has changes
326 while (m_document->isModified()) {
327 // ask what want to do the user with the changes in the sprite
328 int ret = Alert::show(
329 fmt::format(
330 Strings::alerts_save_sprite_changes(),
331 m_document->name(),
332 (quitting ? Strings::alerts_save_sprite_changes_quitting():
333 Strings::alerts_save_sprite_changes_closing())));
334
335 if (ret == 1) {
336 // "save": save the changes
337 save_it = true;
338 break;
339 }
340 else if (ret != 2) {
341 // "cancel" or "ESC" */
342 return false; // we back doing nothing
343 }
344 else {
345 // "discard"
346 break;
347 }
348 }
349
350 // Does we need to save the sprite?
351 if (save_it) {
352 ctx->updateFlags();
353
354 Command* save_command =
355 Commands::instance()->byId(CommandId::SaveFile());
356 ctx->executeCommand(save_command);
357
358 try_again = true;
359 }
360 else
361 try_again = false;
362 }
363
364 try {
365 // Destroy the sprite (locking it as writer)
366 DocDestroyer destroyer(
367 static_cast<app::Context*>(m_document->context()), m_document, 500);
368
369 StatusBar::instance()->setStatusText(
370 0, fmt::format("Sprite '{}' closed.",
371 m_document->name()));
372
373 // Just close the document (so we can reopen it with
374 // ReopenClosedFile command).
375 destroyer.closeDocument();
376
377 // At this point the view is already destroyed
378 return true;
379 }
380 catch (const LockedDocException& ex) {
381 Console::showException(ex);
382 return false;
383 }
384}
385
386void DocView::onTabPopup(Workspace* workspace)
387{
388 Menu* menu = AppMenus::instance()->getDocumentTabPopupMenu();
389 if (!menu)
390 return;
391
392 UIContext* ctx = UIContext::instance();
393 ctx->setActiveView(this);
394 ctx->updateFlags();
395
396 menu->showPopup(mousePosInDisplay(), display());
397}
398
399bool DocView::onProcessMessage(Message* msg)
400{
401 switch (msg->type()) {
402 case kFocusEnterMessage:
403 if (msg->recipient() != m_editor)
404 m_editor->requestFocus();
405 break;
406 }
407 return Box::onProcessMessage(msg);
408}
409
410void DocView::onGeneralUpdate(DocEvent& ev)
411{
412 if (m_editor->isVisible())
413 m_editor->updateEditor(true);
414}
415
416void DocView::onSpritePixelsModified(DocEvent& ev)
417{
418 if (m_editor->isVisible() &&
419 m_editor->frame() == ev.frame())
420 m_editor->drawSpriteClipped(ev.region());
421}
422
423void DocView::onLayerMergedDown(DocEvent& ev)
424{
425 m_editor->setLayer(ev.targetLayer());
426}
427
428void DocView::onAddLayer(DocEvent& ev)
429{
430 if (current_editor == m_editor) {
431 ASSERT(ev.layer() != NULL);
432 m_editor->setLayer(ev.layer());
433 }
434}
435
436void DocView::onAddFrame(DocEvent& ev)
437{
438 if (current_editor == m_editor)
439 m_editor->setFrame(ev.frame());
440 else if (m_editor->frame() > ev.frame())
441 m_editor->setFrame(m_editor->frame()+1);
442}
443
444void DocView::onRemoveFrame(DocEvent& ev)
445{
446 // Adjust current frame of all editors that are in a frame more
447 // advanced that the removed one.
448 if (m_editor->frame() > ev.frame()) {
449 m_editor->setFrame(m_editor->frame()-1);
450 }
451 // If the editor was in the previous "last frame" (current value of
452 // totalFrames()), we've to adjust it to the new last frame
453 // (lastFrame())
454 else if (m_editor->frame() >= m_editor->sprite()->totalFrames()) {
455 m_editor->setFrame(m_editor->sprite()->lastFrame());
456 }
457}
458
459void DocView::onTagChange(DocEvent& ev)
460{
461 if (m_previewDelegate)
462 m_previewDelegate->onTagChangeEditor(m_editor, ev);
463}
464
465void DocView::onAddCel(DocEvent& ev)
466{
467 UIContext::instance()->notifyActiveSiteChanged();
468}
469
470void DocView::onAfterRemoveCel(DocEvent& ev)
471{
472 // This can happen when we apply a filter that clear the whole cel
473 // and then the cel is removed in a background/job
474 // thread. (e.g. applying a convolution matrix)
475 if (!ui::is_ui_thread())
476 return;
477
478 UIContext::instance()->notifyActiveSiteChanged();
479}
480
481void DocView::onTotalFramesChanged(DocEvent& ev)
482{
483 if (m_editor->frame() >= m_editor->sprite()->totalFrames()) {
484 m_editor->setFrame(m_editor->sprite()->lastFrame());
485 }
486}
487
488void DocView::onLayerRestacked(DocEvent& ev)
489{
490 m_editor->invalidate();
491}
492
493void DocView::onTilesetChanged(DocEvent& ev)
494{
495 // This can happen when a filter is applied to each tile in a
496 // background thread.
497 if (!ui::is_ui_thread())
498 return;
499
500 m_editor->invalidate();
501}
502
503void DocView::onNewInputPriority(InputChainElement* element,
504 const ui::Message* msg)
505{
506 // Do nothing
507}
508
509bool DocView::onCanCut(Context* ctx)
510{
511 if (ctx->checkFlags(ContextFlags::ActiveDocumentIsWritable |
512 ContextFlags::ActiveLayerIsVisible |
513 ContextFlags::ActiveLayerIsEditable |
514 ContextFlags::HasVisibleMask |
515 ContextFlags::HasActiveImage)
516 && !ctx->checkFlags(ContextFlags::ActiveLayerIsReference))
517 return true;
518 else if (m_editor->isMovingPixels())
519 return true;
520 else
521 return false;
522}
523
524bool DocView::onCanCopy(Context* ctx)
525{
526 if (ctx->checkFlags(ContextFlags::ActiveDocumentIsWritable |
527 ContextFlags::ActiveLayerIsVisible |
528 ContextFlags::HasVisibleMask |
529 ContextFlags::HasActiveImage)
530 && !ctx->checkFlags(ContextFlags::ActiveLayerIsReference))
531 return true;
532 else if (m_editor->isMovingPixels())
533 return true;
534 else
535 return false;
536}
537
538bool DocView::onCanPaste(Context* ctx)
539{
540 if (ctx->checkFlags(ContextFlags::ActiveDocumentIsWritable |
541 ContextFlags::ActiveLayerIsVisible |
542 ContextFlags::ActiveLayerIsEditable |
543 ContextFlags::ActiveLayerIsImage)
544 && !ctx->checkFlags(ContextFlags::ActiveLayerIsReference)) {
545 auto format = ctx->clipboard()->format();
546 if (format == ClipboardFormat::Image) {
547 return true;
548 }
549 else if (format == ClipboardFormat::Tilemap &&
550 ctx->checkFlags(ContextFlags::ActiveLayerIsTilemap)) {
551 return true;
552 }
553 }
554 return false;
555}
556
557bool DocView::onCanClear(Context* ctx)
558{
559 if (ctx->checkFlags(ContextFlags::ActiveDocumentIsWritable |
560 ContextFlags::ActiveLayerIsVisible |
561 ContextFlags::ActiveLayerIsEditable |
562 ContextFlags::ActiveLayerIsImage)
563 && !ctx->checkFlags(ContextFlags::ActiveLayerIsReference)) {
564 return true;
565 }
566 else if (m_editor->isMovingPixels()) {
567 return true;
568 }
569 else
570 return false;
571}
572
573bool DocView::onCut(Context* ctx)
574{
575 ContextWriter writer(ctx);
576 ctx->clipboard()->cut(writer);
577 return true;
578}
579
580bool DocView::onCopy(Context* ctx)
581{
582 const ContextReader reader(ctx);
583 if (reader.site()->document() &&
584 static_cast<const Doc*>(reader.site()->document())->isMaskVisible() &&
585 reader.site()->image()) {
586 ctx->clipboard()->copy(reader);
587 return true;
588 }
589 else
590 return false;
591}
592
593bool DocView::onPaste(Context* ctx)
594{
595 auto clipboard = ctx->clipboard();
596 if (clipboard->format() == ClipboardFormat::Image ||
597 clipboard->format() == ClipboardFormat::Tilemap) {
598 clipboard->paste(ctx, true);
599 return true;
600 }
601 else
602 return false;
603}
604
605bool DocView::onClear(Context* ctx)
606{
607 // First we check if there is a selected slice, so we'll delete
608 // those slices.
609 Site site = ctx->activeSite();
610 if (!site.selectedSlices().empty()) {
611 Command* removeSlices = Commands::instance()->byId(CommandId::RemoveSlice());
612 ctx->executeCommand(removeSlices);
613 return true;
614 }
615
616 // In other case we delete the mask or the cel.
617 ContextWriter writer(ctx);
618 Doc* document = site.document();
619 bool visibleMask = document->isMaskVisible();
620
621 CelList cels;
622 if (site.range().enabled()) {
623 cels = get_unique_cels_to_edit_pixels(site.sprite(), site.range());
624 }
625 else if (site.cel()) {
626 cels.push_back(site.cel());
627 }
628
629 if (cels.empty()) // No cels to modify
630 return false;
631
632 // TODO This code is similar to clipboard::cut()
633 {
634 Tx tx(writer.context(), "Clear");
635 const bool deselectMask =
636 (visibleMask &&
637 !Preferences::instance().selection.keepSelectionAfterClear());
638
639 ctx->clipboard()->clearMaskFromCels(
640 tx, document, site, cels, deselectMask);
641
642 tx.commit();
643 }
644
645 if (visibleMask)
646 document->generateMaskBoundaries();
647
648 document->notifyGeneralUpdate();
649 return true;
650}
651
652void DocView::onCancel(Context* ctx)
653{
654 if (m_editor)
655 m_editor->cancelSelections();
656
657 // Deselect mask
658 if (ctx->checkFlags(ContextFlags::ActiveDocumentIsWritable |
659 ContextFlags::HasVisibleMask)) {
660 Command* deselectMask = Commands::instance()->byId(CommandId::DeselectMask());
661 ctx->executeCommand(deselectMask);
662 }
663}
664
665} // namespace app
666