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 | |
42 | namespace app { |
43 | |
44 | using namespace app::skin; |
45 | using namespace ui; |
46 | |
47 | class MiniCenterButton : public CheckBox { |
48 | public: |
49 | MiniCenterButton() : CheckBox("" ) { |
50 | setDecorative(true); |
51 | setSelected(true); |
52 | initTheme(); |
53 | } |
54 | |
55 | protected: |
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 | |
94 | class MiniPlayButton : public Button { |
95 | public: |
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()> ; |
111 | |
112 | private: |
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 | |
179 | PreviewEditorWindow::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 | |
203 | PreviewEditorWindow::~PreviewEditorWindow() |
204 | { |
205 | set_config_bool("MiniEditor" , "Enabled" , m_isEnabled); |
206 | } |
207 | |
208 | void PreviewEditorWindow::setPreviewEnabled(bool state) |
209 | { |
210 | m_isEnabled = state; |
211 | |
212 | updateUsingEditor(current_editor); |
213 | } |
214 | |
215 | void PreviewEditorWindow::pressPlayButton() |
216 | { |
217 | m_playButton->setPlaying(!m_playButton->isPlaying()); |
218 | onPlayClicked(); |
219 | } |
220 | |
221 | bool 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 = 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 | |
256 | void PreviewEditorWindow::onInitTheme(ui::InitThemeEvent& ev) |
257 | { |
258 | Window::onInitTheme(ev); |
259 | setChildSpacing(0); |
260 | } |
261 | |
262 | void 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 | |
280 | void PreviewEditorWindow::onWindowResize() |
281 | { |
282 | Window::onWindowResize(); |
283 | |
284 | DocView* view = UIContext::instance()->activeView(); |
285 | if (view) |
286 | updateUsingEditor(view->editor()); |
287 | } |
288 | |
289 | bool PreviewEditorWindow::hasDocument() const |
290 | { |
291 | return (m_docView && m_docView->document() != nullptr); |
292 | } |
293 | |
294 | DocumentPreferences& PreviewEditorWindow::docPref() |
295 | { |
296 | Doc* doc = (m_docView ? m_docView->document(): nullptr); |
297 | return Preferences::instance().document(doc); |
298 | } |
299 | |
300 | void 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 | |
311 | void 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 | |
329 | void PreviewEditorWindow::() |
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 | |
344 | Editor* PreviewEditorWindow::previewEditor() const |
345 | { |
346 | return (m_docView ? m_docView->editor(): nullptr); |
347 | } |
348 | |
349 | void 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 | |
409 | void PreviewEditorWindow::uncheckCenterButton() |
410 | { |
411 | if (m_centerButton->isSelected()) { |
412 | m_centerButton->setSelected(false); |
413 | onCenterClicked(); |
414 | } |
415 | } |
416 | |
417 | void 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 | |
424 | void PreviewEditorWindow::onScrollChanged(Editor* miniEditor) |
425 | { |
426 | if (miniEditor->hasCapture()) { |
427 | saveScrollPref(); |
428 | uncheckCenterButton(); |
429 | } |
430 | } |
431 | |
432 | void PreviewEditorWindow::onZoomChanged(Editor* miniEditor) |
433 | { |
434 | saveScrollPref(); |
435 | } |
436 | |
437 | void 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 | |
450 | void PreviewEditorWindow::onScrollOtherEditor(Editor* editor) |
451 | { |
452 | updateUsingEditor(editor); |
453 | } |
454 | |
455 | void PreviewEditorWindow::onDisposeOtherEditor(Editor* editor) |
456 | { |
457 | if (m_relatedEditor == editor) |
458 | updateUsingEditor(nullptr); |
459 | } |
460 | |
461 | void PreviewEditorWindow::onPreviewOtherEditor(Editor* editor) |
462 | { |
463 | updateUsingEditor(editor); |
464 | } |
465 | |
466 | void PreviewEditorWindow::onTagChangeEditor(Editor* editor, DocEvent& ev) |
467 | { |
468 | if (m_playButton->isPlaying()) |
469 | adjustPlayingTag(); |
470 | } |
471 | |
472 | void PreviewEditorWindow::hideWindow() |
473 | { |
474 | destroyDocView(); |
475 | if (isVisible()) |
476 | closeWindow(NULL); |
477 | } |
478 | |
479 | void 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 | |
489 | void 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 | |