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/ui/preview_editor.h"
13
14#include "app/app.h"
15#include "app/doc.h"
16#include "app/doc_event.h"
17#include "app/ini_file.h"
18#include "app/loop_tag.h"
19#include "app/i18n/strings.h"
20#include "app/modules/editors.h"
21#include "app/modules/gui.h"
22#include "app/pref/preferences.h"
23#include "app/ui/editor/editor.h"
24#include "app/ui/editor/editor_customization_delegate.h"
25#include "app/ui/editor/editor_view.h"
26#include "app/ui/editor/play_state.h"
27#include "app/ui/skin/skin_theme.h"
28#include "app/ui/status_bar.h"
29#include "app/ui/toolbar.h"
30#include "app/ui_context.h"
31#include "doc/sprite.h"
32#include "gfx/rect.h"
33#include "ui/base.h"
34#include "ui/button.h"
35#include "ui/close_event.h"
36#include "ui/fit_bounds.h"
37#include "ui/message.h"
38#include "ui/system.h"
39
40#include "doc/tag.h"
41
42namespace app {
43
44using namespace app::skin;
45using namespace ui;
46
47class MiniCenterButton : public CheckBox {
48public:
49 MiniCenterButton() : CheckBox("") {
50 setDecorative(true);
51 setSelected(true);
52 initTheme();
53 }
54
55protected:
56 void onInitTheme(ui::InitThemeEvent& ev) override {
57 CheckBox::onInitTheme(ev);
58
59 auto theme = SkinTheme::get(this);
60 setStyle(theme->styles.windowCenterButton());
61 }
62
63 void onSetDecorativeWidgetBounds() override {
64 auto theme = SkinTheme::get(this);
65 Widget* window = parent();
66 gfx::Rect rect(0, 0, 0, 0);
67 gfx::Size centerSize = this->sizeHint();
68 gfx::Size playSize = theme->calcSizeHint(this, theme->styles.windowPlayButton());
69 gfx::Size closeSize = theme->calcSizeHint(this, theme->styles.windowCloseButton());
70
71 rect.w = centerSize.w;
72 rect.h = centerSize.h;
73 rect.offset(window->bounds().x2()
74 - theme->styles.windowCloseButton()->margin().width() - closeSize.w
75 - theme->styles.windowPlayButton()->margin().width() - playSize.w
76 - style()->margin().right() - centerSize.w,
77 window->bounds().y + style()->margin().top());
78
79 setBounds(rect);
80 }
81
82 bool onProcessMessage(Message* msg) override {
83 switch (msg->type()) {
84
85 case kSetCursorMessage:
86 ui::set_mouse_cursor(kArrowCursor);
87 return true;
88 }
89
90 return CheckBox::onProcessMessage(msg);
91 }
92};
93
94class MiniPlayButton : public Button {
95public:
96 MiniPlayButton() : Button(""), m_isPlaying(false) {
97 enableFlags(CTRL_RIGHT_CLICK);
98 setDecorative(true);
99 initTheme();
100 }
101
102 bool isPlaying() const { return m_isPlaying; }
103
104 void setPlaying(bool state) {
105 m_isPlaying = state;
106 setupIcons();
107 invalidate();
108 }
109
110 obs::signal<void()> Popup;
111
112private:
113 void onInitTheme(ui::InitThemeEvent& ev) override {
114 Button::onInitTheme(ev);
115 setupIcons();
116 }
117
118 void onClick(Event& ev) override {
119 m_isPlaying = !m_isPlaying;
120 setupIcons();
121
122 Button::onClick(ev);
123 }
124
125 void onSetDecorativeWidgetBounds() override {
126 auto theme = SkinTheme::get(this);
127 Widget* window = parent();
128 gfx::Rect rect(0, 0, 0, 0);
129 gfx::Size playSize = this->sizeHint();
130 gfx::Size closeSize = theme->calcSizeHint(this, theme->styles.windowCloseButton());
131 gfx::Border margin(0, 0, 0, 0);
132
133 rect.w = playSize.w;
134 rect.h = playSize.h;
135 rect.offset(window->bounds().x2()
136 - theme->styles.windowCloseButton()->margin().width() - closeSize.w
137 - style()->margin().right() - playSize.w,
138 window->bounds().y + style()->margin().top());
139
140 setBounds(rect);
141 }
142
143 bool onProcessMessage(Message* msg) override {
144 switch (msg->type()) {
145
146 case kSetCursorMessage:
147 ui::set_mouse_cursor(kArrowCursor);
148 return true;
149
150 case kMouseUpMessage: {
151 MouseMessage* mouseMsg = static_cast<MouseMessage*>(msg);
152 if (mouseMsg->right()) {
153 if (hasCapture()) {
154 releaseMouse();
155 Popup();
156
157 setSelected(false);
158 return true;
159 }
160 }
161 break;
162 }
163 }
164
165 return Button::onProcessMessage(msg);
166 }
167
168 void setupIcons() {
169 auto theme = SkinTheme::get(this);
170 if (m_isPlaying)
171 setStyle(theme->styles.windowStopButton());
172 else
173 setStyle(theme->styles.windowPlayButton());
174 }
175
176 bool m_isPlaying;
177};
178
179PreviewEditorWindow::PreviewEditorWindow()
180 : Window(WithTitleBar, Strings::preview_title())
181 , m_docView(NULL)
182 , m_centerButton(new MiniCenterButton())
183 , m_playButton(new MiniPlayButton())
184 , m_refFrame(0)
185 , m_aniSpeed(1.0)
186 , m_relatedEditor(nullptr)
187{
188 setAutoRemap(false);
189 setWantFocus(false);
190
191 m_isEnabled = get_config_bool("MiniEditor", "Enabled", true);
192
193 m_centerButton->Click.connect([this]{ onCenterClicked(); });
194 m_playButton->Click.connect([this]{ onPlayClicked(); });
195 m_playButton->Popup.connect([this]{ onPopupSpeed(); });
196
197 addChild(m_centerButton);
198 addChild(m_playButton);
199
200 initTheme();
201}
202
203PreviewEditorWindow::~PreviewEditorWindow()
204{
205 set_config_bool("MiniEditor", "Enabled", m_isEnabled);
206}
207
208void PreviewEditorWindow::setPreviewEnabled(bool state)
209{
210 m_isEnabled = state;
211
212 updateUsingEditor(current_editor);
213}
214
215void PreviewEditorWindow::pressPlayButton()
216{
217 m_playButton->setPlaying(!m_playButton->isPlaying());
218 onPlayClicked();
219}
220
221bool PreviewEditorWindow::onProcessMessage(ui::Message* msg)
222{
223 switch (msg->type()) {
224
225 case kOpenMessage: {
226 Manager* manager = this->manager();
227 Display* mainDisplay = manager->display();
228
229 gfx::Rect defaultBounds(mainDisplay->size() / 4);
230 auto theme = SkinTheme::get(this);
231 gfx::Rect mainWindow = manager->bounds();
232
233 int extra = theme->dimensions.miniScrollbarSize();
234 if (get_multiple_displays()) {
235 extra *= mainDisplay->scale();
236 }
237 defaultBounds.x = mainWindow.x2() - ToolBar::instance()->sizeHint().w - defaultBounds.w - extra;
238 defaultBounds.y = mainWindow.y2() - StatusBar::instance()->sizeHint().h - defaultBounds.h - extra;
239
240 fit_bounds(mainDisplay, this, defaultBounds);
241
242 load_window_pos(this, "MiniEditor", false);
243 invalidate();
244 break;
245 }
246
247 case kCloseMessage:
248 save_window_pos(this, "MiniEditor");
249 break;
250
251 }
252
253 return Window::onProcessMessage(msg);
254}
255
256void PreviewEditorWindow::onInitTheme(ui::InitThemeEvent& ev)
257{
258 Window::onInitTheme(ev);
259 setChildSpacing(0);
260}
261
262void PreviewEditorWindow::onClose(ui::CloseEvent& ev)
263{
264 ButtonBase* closeButton = dynamic_cast<ButtonBase*>(ev.getSource());
265 if (closeButton &&
266 closeButton->type() == kWindowCloseButtonWidget) {
267 // Here we don't use "setPreviewEnabled" to change the state of
268 // "m_isEnabled" because we're coming from a close event of the
269 // window.
270 m_isEnabled = false;
271
272 // Redraw the tool bar because it shows the mini editor enabled
273 // state. TODO abstract this event
274 ToolBar::instance()->invalidate();
275
276 destroyDocView();
277 }
278}
279
280void PreviewEditorWindow::onWindowResize()
281{
282 Window::onWindowResize();
283
284 DocView* view = UIContext::instance()->activeView();
285 if (view)
286 updateUsingEditor(view->editor());
287}
288
289bool PreviewEditorWindow::hasDocument() const
290{
291 return (m_docView && m_docView->document() != nullptr);
292}
293
294DocumentPreferences& PreviewEditorWindow::docPref()
295{
296 Doc* doc = (m_docView ? m_docView->document(): nullptr);
297 return Preferences::instance().document(doc);
298}
299
300void PreviewEditorWindow::onCenterClicked()
301{
302 if (!m_relatedEditor || !hasDocument())
303 return;
304
305 bool autoScroll = m_centerButton->isSelected();
306 docPref().preview.autoScroll(autoScroll);
307 if (autoScroll)
308 updateUsingEditor(m_relatedEditor);
309}
310
311void PreviewEditorWindow::onPlayClicked()
312{
313 Editor* miniEditor = (m_docView ? m_docView->editor(): nullptr);
314 if (!miniEditor || !miniEditor->document())
315 return;
316
317 if (m_playButton->isPlaying()) {
318 m_refFrame = miniEditor->frame();
319 miniEditor->play(Preferences::instance().preview.playOnce(),
320 Preferences::instance().preview.playAll());
321 }
322 else {
323 miniEditor->stop();
324 if (m_relatedEditor)
325 miniEditor->setFrame(m_relatedEditor->frame());
326 }
327}
328
329void PreviewEditorWindow::onPopupSpeed()
330{
331 Editor* miniEditor = (m_docView ? m_docView->editor(): nullptr);
332 if (!miniEditor || !miniEditor->document())
333 return;
334
335 auto& pref = Preferences::instance();
336
337 miniEditor->showAnimationSpeedMultiplierPopup(
338 pref.preview.playOnce,
339 pref.preview.playAll,
340 false);
341 m_aniSpeed = miniEditor->getAnimationSpeedMultiplier();
342}
343
344Editor* PreviewEditorWindow::previewEditor() const
345{
346 return (m_docView ? m_docView->editor(): nullptr);
347}
348
349void PreviewEditorWindow::updateUsingEditor(Editor* editor)
350{
351 if (!m_isEnabled || !editor) {
352 hideWindow();
353 m_relatedEditor = nullptr;
354 return;
355 }
356
357 if (!editor->isActive())
358 return;
359
360 m_relatedEditor = editor;
361
362 Doc* document = editor->document();
363 Editor* miniEditor = (m_docView ? m_docView->editor(): nullptr);
364
365 if (!isVisible())
366 openWindow();
367
368 // Document preferences used to store the preferred zoom/scroll point
369 auto& docPref = Preferences::instance().document(document);
370 bool autoScroll = docPref.preview.autoScroll();
371
372 // Set the same location as in the given editor.
373 if (!miniEditor || miniEditor->document() != document) {
374 destroyDocView();
375
376 m_docView = new DocView(document, DocView::Preview, this);
377 addChild(m_docView);
378
379 miniEditor = m_docView->editor();
380 miniEditor->setZoom(render::Zoom::fromScale(docPref.preview.zoom()));
381 miniEditor->setLayer(editor->layer());
382 miniEditor->setFrame(editor->frame());
383 miniEditor->setAnimationSpeedMultiplier(m_aniSpeed);
384 miniEditor->add_observer(this);
385 layout();
386
387 if (!autoScroll)
388 miniEditor->setEditorScroll(docPref.preview.scroll());
389 }
390
391 m_centerButton->setSelected(autoScroll);
392 if (autoScroll) {
393 gfx::Point centerPoint = editor->getVisibleSpriteBounds().center();
394 miniEditor->centerInSpritePoint(centerPoint);
395
396 saveScrollPref();
397 }
398
399 if (!m_playButton->isPlaying()) {
400 miniEditor->stop();
401 miniEditor->setLayer(editor->layer());
402 miniEditor->setFrame(editor->frame());
403 }
404 else {
405 adjustPlayingTag();
406 }
407}
408
409void PreviewEditorWindow::uncheckCenterButton()
410{
411 if (m_centerButton->isSelected()) {
412 m_centerButton->setSelected(false);
413 onCenterClicked();
414 }
415}
416
417void PreviewEditorWindow::onStateChanged(Editor* editor)
418{
419 // Sync editor playing state with MiniPlayButton state
420 if (m_playButton->isPlaying() != editor->isPlaying())
421 m_playButton->setPlaying(editor->isPlaying());
422}
423
424void PreviewEditorWindow::onScrollChanged(Editor* miniEditor)
425{
426 if (miniEditor->hasCapture()) {
427 saveScrollPref();
428 uncheckCenterButton();
429 }
430}
431
432void PreviewEditorWindow::onZoomChanged(Editor* miniEditor)
433{
434 saveScrollPref();
435}
436
437void PreviewEditorWindow::saveScrollPref()
438{
439 ASSERT(m_docView);
440 if (!m_docView)
441 return;
442
443 Editor* miniEditor = m_docView->editor();
444 ASSERT(miniEditor);
445
446 docPref().preview.scroll(View::getView(miniEditor)->viewScroll());
447 docPref().preview.zoom(miniEditor->zoom().scale());
448}
449
450void PreviewEditorWindow::onScrollOtherEditor(Editor* editor)
451{
452 updateUsingEditor(editor);
453}
454
455void PreviewEditorWindow::onDisposeOtherEditor(Editor* editor)
456{
457 if (m_relatedEditor == editor)
458 updateUsingEditor(nullptr);
459}
460
461void PreviewEditorWindow::onPreviewOtherEditor(Editor* editor)
462{
463 updateUsingEditor(editor);
464}
465
466void PreviewEditorWindow::onTagChangeEditor(Editor* editor, DocEvent& ev)
467{
468 if (m_playButton->isPlaying())
469 adjustPlayingTag();
470}
471
472void PreviewEditorWindow::hideWindow()
473{
474 destroyDocView();
475 if (isVisible())
476 closeWindow(NULL);
477}
478
479void PreviewEditorWindow::destroyDocView()
480{
481 if (m_docView) {
482 m_docView->editor()->remove_observer(this);
483
484 delete m_docView;
485 m_docView = nullptr;
486 }
487}
488
489void PreviewEditorWindow::adjustPlayingTag()
490{
491 Editor* editor = m_relatedEditor;
492 if (!editor || !m_docView)
493 return;
494
495 Editor* miniEditor = m_docView->editor();
496
497 if (miniEditor->isPlaying()) {
498 doc::Tag* tag = editor
499 ->getCustomizationDelegate()
500 ->getTagProvider()
501 ->getTagByFrame(editor->frame(), true);
502
503 auto playState = dynamic_cast<PlayState*>(miniEditor->getState().get());
504 doc::Tag* playingTag = (playState ? playState->playingTag(): nullptr);
505
506 if (tag == playingTag)
507 return;
508
509 miniEditor->stop();
510 }
511
512 if (!miniEditor->isPlaying())
513 miniEditor->setFrame(m_refFrame = editor->frame());
514
515 miniEditor->play(Preferences::instance().preview.playOnce(),
516 Preferences::instance().preview.playAll());
517}
518
519} // namespace app
520