1// Aseprite
2// Copyright (C) 2019-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_context.h"
13
14#include "app/app.h"
15#include "app/doc.h"
16#include "app/modules/editors.h"
17#include "app/site.h"
18#include "app/ui/color_bar.h"
19#include "app/ui/doc_view.h"
20#include "app/ui/editor/editor.h"
21#include "app/ui/input_chain.h"
22#include "app/ui/main_window.h"
23#include "app/ui/preview_editor.h"
24#include "app/ui/status_bar.h"
25#include "app/ui/timeline/timeline.h"
26#include "app/ui/workspace.h"
27#include "app/ui/workspace_tabs.h"
28#include "doc/sprite.h"
29#include "ui/system.h"
30
31#include <algorithm>
32
33namespace app {
34
35UIContext* UIContext::m_instance = nullptr;
36
37UIContext::UIContext()
38 : m_lastSelectedView(nullptr)
39 , m_closedDocs(preferences())
40{
41 ASSERT(m_instance == nullptr);
42 m_instance = this;
43}
44
45UIContext::~UIContext()
46{
47 ASSERT(m_instance == this);
48 m_instance = nullptr;
49
50 // The context must be empty at this point. (It's to check if the UI
51 // is working correctly, i.e. closing all files when the user can
52 // take any action about it.)
53 //
54 // Note: This assert is commented because it's really common to hit
55 // it when the program crashes by any other reason, and we would
56 // like to see that other reason instead of this assert.
57
58 //ASSERT(documents().empty());
59}
60
61bool UIContext::isUIAvailable() const
62{
63 return App::instance()->isGui();
64}
65
66DocView* UIContext::activeView() const
67{
68 if (!isUIAvailable())
69 return nullptr;
70
71 Workspace* workspace = App::instance()->workspace();
72 if (!workspace)
73 return nullptr;
74
75 WorkspaceView* view = workspace->activeView();
76 if (DocView* docView = dynamic_cast<DocView*>(view))
77 return docView;
78 else
79 return nullptr;
80}
81
82void UIContext::setActiveView(DocView* docView)
83{
84 MainWindow* mainWin = App::instance()->mainWindow();
85
86 // This can happen when the main window is being destroyed when we
87 // close the app, and the active view is changing because we are
88 // closing down every single tab.
89 if (!mainWin)
90 return;
91
92 // Prioritize workspace for user input.
93 App::instance()->inputChain().prioritize(mainWin->getWorkspace(), nullptr);
94
95 // Do nothing cases: 1) the view is already selected, or 2) the view
96 // is the a preview.
97 if (m_lastSelectedView == docView ||
98 (docView && docView->isPreview()))
99 return;
100
101 if (docView) {
102 current_editor = docView->editor();
103 mainWin->getTabsBar()->selectTab(docView);
104
105 if (mainWin->getWorkspace()->activeView() != docView)
106 mainWin->getWorkspace()->setActiveView(docView);
107
108 if (current_editor)
109 current_editor->requestFocus();
110 }
111 else
112 current_editor = nullptr;
113
114 mainWin->getTimeline()->updateUsingEditor(current_editor);
115 mainWin->getPreviewEditor()->updateUsingEditor(current_editor);
116
117 // Change the image-type of color bar.
118 ColorBar::instance()->setPixelFormat(app_get_current_pixel_format());
119
120 // Restore the palette of the selected document.
121 app_refresh_screen();
122
123 // Change the main frame title.
124 App::instance()->updateDisplayTitleBar();
125
126 m_lastSelectedView = docView;
127
128 // TODO all the calls to functions like updateUsingEditor(),
129 // setPixelFormat(), app_refresh_screen(), updateDisplayTitleBar(),
130 // etc. could be replaced with the Transaction class, which is a
131 // DocObserver and handles updates on the screen processing the
132 // observed changes.
133 notifyActiveSiteChanged();
134}
135
136void UIContext::onSetActiveDocument(Doc* document, bool notify)
137{
138 notify = (notify && lastSelectedDoc() != document);
139 app::Context::onSetActiveDocument(document, false);
140
141 DocView* docView = getFirstDocView(document);
142 if (docView) { // The view can be null if we are in --batch mode
143 setActiveView(docView);
144 notify = false;
145 }
146
147 if (notify)
148 notifyActiveSiteChanged();
149}
150
151void UIContext::onSetActiveLayer(doc::Layer* layer)
152{
153 if (DocView* docView = activeView()) {
154 if (Editor* editor = docView->editor())
155 editor->setLayer(layer);
156 }
157 else if (!isUIAvailable())
158 Context::onSetActiveLayer(layer);
159}
160
161void UIContext::onSetActiveFrame(const doc::frame_t frame)
162{
163 if (DocView* docView = activeView()) {
164 if (Editor* editor = docView->editor())
165 editor->setFrame(frame);
166 }
167 else if (!isUIAvailable())
168 Context::onSetActiveFrame(frame);
169}
170
171void UIContext::onSetRange(const DocRange& range)
172{
173 Timeline* timeline =
174 (App::instance()->mainWindow() ?
175 App::instance()->mainWindow()->getTimeline(): nullptr);
176 if (timeline) {
177 timeline->setRange(range);
178 }
179 else if (!isUIAvailable()) {
180 Context::onSetRange(range);
181 }
182}
183
184void UIContext::onSetSelectedColors(const doc::PalettePicks& picks)
185{
186 if (activeView()) {
187 if (ColorBar* colorBar = ColorBar::instance())
188 colorBar->getPaletteView()->setSelectedEntries(picks);
189 }
190 else if (!isUIAvailable())
191 Context::onSetSelectedColors(picks);
192}
193
194void UIContext::onSetSelectedTiles(const doc::PalettePicks& picks)
195{
196 if (activeView()) {
197 if (ColorBar* colorBar = ColorBar::instance())
198 colorBar->getTilesView()->setSelectedEntries(picks);
199 }
200 else if (!isUIAvailable())
201 Context::onSetSelectedTiles(picks);
202}
203
204DocView* UIContext::getFirstDocView(Doc* document) const
205{
206 Workspace* workspace = App::instance()->workspace();
207 if (!workspace) // Workspace (main window) can be null if we are in --batch mode
208 return nullptr;
209
210 for (WorkspaceView* view : *workspace) {
211 if (DocView* docView = dynamic_cast<DocView*>(view)) {
212 if (docView->document() == document) {
213 return docView;
214 }
215 }
216 }
217
218 return nullptr;
219}
220
221DocViews UIContext::getAllDocViews(Doc* document) const
222{
223 DocViews docViews;
224 // The workspace can be nullptr when we are running in batch mode.
225 if (Workspace* workspace = App::instance()->workspace()) {
226 for (WorkspaceView* view : *workspace) {
227 if (DocView* docView = dynamic_cast<DocView*>(view)) {
228 if (docView->document() == document) {
229 docViews.push_back(docView);
230 }
231 }
232 }
233 }
234 return docViews;
235}
236
237Editors UIContext::getAllEditorsIncludingPreview(Doc* document) const
238{
239 std::vector<Editor*> editors;
240 for (DocView* docView : getAllDocViews(document)) {
241 if (docView->editor())
242 editors.push_back(docView->editor());
243 }
244
245 if (MainWindow* mainWin = App::instance()->mainWindow()) {
246 PreviewEditorWindow* previewWin = mainWin->getPreviewEditor();
247 if (previewWin) {
248 Editor* miniEditor = previewWin->previewEditor();
249 if (miniEditor && miniEditor->document() == document)
250 editors.push_back(miniEditor);
251 }
252 }
253 return editors;
254}
255
256Editor* UIContext::activeEditor()
257{
258 DocView* view = activeView();
259 if (view)
260 return view->editor();
261 else
262 return NULL;
263}
264
265Editor* UIContext::getEditorFor(Doc* document)
266{
267 if (auto view = getFirstDocView(document))
268 return view->editor();
269 else
270 return nullptr;
271}
272
273bool UIContext::hasClosedDocs()
274{
275 return m_closedDocs.hasClosedDocs();
276}
277
278void UIContext::reopenLastClosedDoc()
279{
280 if (Doc* doc = m_closedDocs.reopenLastClosedDoc()) {
281 // Put the document in the context again.
282 doc->setContext(this);
283 }
284}
285
286std::vector<Doc*> UIContext::getAndRemoveAllClosedDocs()
287{
288 return m_closedDocs.getAndRemoveAllClosedDocs();
289}
290
291void UIContext::onAddDocument(Doc* doc)
292{
293 app::Context::onAddDocument(doc);
294
295 // We don't create views in batch mode.
296 if (!App::instance()->isGui())
297 return;
298
299 // Add a new view for this document
300 DocView* view = new DocView(
301 lastSelectedDoc(),
302 DocView::Normal,
303 App::instance()->mainWindow()->getPreviewEditor());
304
305 // Add a tab with the new view for the document
306 App::instance()->workspace()->addView(view);
307
308 setActiveView(view);
309 view->editor()->setDefaultScroll();
310}
311
312void UIContext::onRemoveDocument(Doc* doc)
313{
314 app::Context::onRemoveDocument(doc);
315
316 // We don't destroy views in batch mode.
317 if (isUIAvailable()) {
318 Workspace* workspace = App::instance()->workspace();
319
320 for (DocView* docView : getAllDocViews(doc)) {
321 workspace->removeView(docView);
322 delete docView;
323 }
324 }
325}
326
327void UIContext::onGetActiveSite(Site* site) const
328{
329 // We can use the activeView only from the UI thread.
330#ifdef _DEBUG
331 if (isUIAvailable())
332 ui::assert_ui_thread();
333#endif
334
335 DocView* view = activeView();
336 if (view) {
337 view->getSite(site);
338
339 if (site->sprite()) {
340 // Selected range in the timeline. We use it only if the
341 // timeline is visible. A common scenario might be
342 // undoing/redoing actions where the range is re-selected, that
343 // could enable the range even if the timeline is hidden. In
344 // this way we avoid using the timeline selection unexpectedly.
345 Timeline* timeline = App::instance()->timeline();
346 if (timeline &&
347 timeline->isVisible() &&
348 timeline->range().enabled()) {
349 site->range(timeline->range());
350 }
351 else {
352 ColorBar* colorBar = ColorBar::instance();
353 if (colorBar &&
354 colorBar->getPaletteView()->getSelectedEntriesCount() > 0) {
355 site->focus(Site::InColorBar);
356
357 doc::PalettePicks picks;
358 colorBar->getPaletteView()->getSelectedEntries(picks);
359 site->selectedColors(picks);
360 }
361 else {
362 site->focus(Site::InEditor);
363 }
364 }
365 }
366 }
367 else if (!isUIAvailable()) {
368 return app::Context::onGetActiveSite(site);
369 }
370}
371
372void UIContext::onCloseDocument(Doc* doc)
373{
374 ASSERT(doc != nullptr);
375 ASSERT(doc->context() == nullptr);
376
377 m_closedDocs.addClosedDoc(doc);
378}
379
380} // namespace app
381