1// Aseprite
2// Copyright (C) 2020-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/editor/play_state.h"
13
14#include "app/commands/command.h"
15#include "app/commands/commands.h"
16#include "app/loop_tag.h"
17#include "app/pref/preferences.h"
18#include "app/tools/ink.h"
19#include "app/ui/editor/editor.h"
20#include "app/ui/editor/editor_customization_delegate.h"
21#include "app/ui/editor/scrolling_state.h"
22#include "app/ui/skin/skin_theme.h"
23#include "app/ui_context.h"
24#include "doc/handle_anidir.h"
25#include "doc/tag.h"
26#include "ui/manager.h"
27#include "ui/message.h"
28#include "ui/system.h"
29
30namespace app {
31
32using namespace ui;
33
34PlayState::PlayState(const bool playOnce,
35 const bool playAll)
36 : m_editor(nullptr)
37 , m_playOnce(playOnce)
38 , m_playAll(playAll)
39 , m_toScroll(false)
40 , m_playTimer(10)
41 , m_nextFrameTime(-1)
42 , m_pingPongForward(true)
43 , m_refFrame(0)
44 , m_tag(nullptr)
45{
46 m_playTimer.Tick.connect(&PlayState::onPlaybackTick, this);
47
48 // Hook BeforeCommandExecution signal so we know if the user wants
49 // to execute other command, so we can stop the animation.
50 m_ctxConn = UIContext::instance()->BeforeCommandExecution.connect(
51 &PlayState::onBeforeCommandExecution, this);
52}
53
54Tag* PlayState::playingTag() const
55{
56 return m_tag;
57}
58
59void PlayState::onEnterState(Editor* editor)
60{
61 StateWithWheelBehavior::onEnterState(editor);
62
63 if (!m_editor) {
64 m_editor = editor;
65 m_refFrame = editor->frame();
66 }
67
68 // Get the tag
69 if (!m_playAll)
70 m_tag = m_editor
71 ->getCustomizationDelegate()
72 ->getTagProvider()
73 ->getTagByFrame(m_refFrame, true);
74
75 // Go to the first frame of the animation or active frame tag
76 if (m_playOnce) {
77 frame_t frame = 0;
78
79 if (m_tag) {
80 frame = (m_tag->aniDir() == AniDir::REVERSE ?
81 m_tag->toFrame():
82 m_tag->fromFrame());
83 }
84
85 m_editor->setFrame(frame);
86 }
87
88 m_toScroll = false;
89 m_nextFrameTime = getNextFrameTime();
90 m_curFrameTick = base::current_tick();
91 m_pingPongForward = true;
92
93 // Maybe we came from ScrollingState and the timer is already
94 // running.
95 if (!m_playTimer.isRunning())
96 m_playTimer.start();
97}
98
99EditorState::LeaveAction PlayState::onLeaveState(Editor* editor, EditorState* newState)
100{
101 // We don't stop the timer if we are going to the ScrollingState
102 // (we keep playing the animation).
103 if (!m_toScroll) {
104 m_playTimer.stop();
105
106 if (m_playOnce || Preferences::instance().general.rewindOnStop())
107 m_editor->setFrame(m_refFrame);
108 }
109 return KeepState;
110}
111
112void PlayState::onBeforePopState(Editor* editor)
113{
114 m_ctxConn.disconnect();
115 StateWithWheelBehavior::onBeforePopState(editor);
116}
117
118bool PlayState::onMouseDown(Editor* editor, MouseMessage* msg)
119{
120 if (editor->hasCapture())
121 return true;
122
123 // When an editor is clicked the current view is changed.
124 UIContext* context = UIContext::instance();
125 context->setActiveView(editor->getDocView());
126
127 // A click with right-button stops the animation
128 if (msg->button() == kButtonRight) {
129 editor->stop();
130 return true;
131 }
132
133 // Set this flag to indicate that we are going to ScrollingState for
134 // some time, so we don't change the current frame.
135 m_toScroll = true;
136
137 // If the active tool is the Zoom tool, we start zooming.
138 if (editor->checkForZoom(msg))
139 return true;
140
141 // Start scroll loop
142 editor->startScrollingState(msg);
143 return true;
144}
145
146bool PlayState::onMouseUp(Editor* editor, MouseMessage* msg)
147{
148 editor->releaseMouse();
149 return true;
150}
151
152bool PlayState::onMouseMove(Editor* editor, MouseMessage* msg)
153{
154 editor->updateStatusBar();
155 return true;
156}
157
158bool PlayState::onKeyDown(Editor* editor, KeyMessage* msg)
159{
160 return false;
161}
162
163bool PlayState::onKeyUp(Editor* editor, KeyMessage* msg)
164{
165 return false;
166}
167
168bool PlayState::onSetCursor(Editor* editor, const gfx::Point& mouseScreenPos)
169{
170 tools::Ink* ink = editor->getCurrentEditorInk();
171 if (ink) {
172 if (ink->isZoom()) {
173 auto theme = skin::SkinTheme::get(editor);
174 editor->showMouseCursor(
175 kCustomCursor, theme->cursors.magnifier());
176 return true;
177 }
178 }
179 editor->showMouseCursor(kScrollCursor);
180 return true;
181}
182
183void PlayState::onRemoveTag(Editor* editor, doc::Tag* tag)
184{
185 if (m_tag == tag)
186 m_tag = nullptr;
187}
188
189void PlayState::onPlaybackTick()
190{
191 ASSERT(m_playTimer.isRunning());
192
193 if (m_nextFrameTime < 0)
194 return;
195
196 m_nextFrameTime -= (base::current_tick() - m_curFrameTick);
197
198 doc::Sprite* sprite = m_editor->sprite();
199
200 while (m_nextFrameTime <= 0) {
201 doc::frame_t frame = m_editor->frame();
202
203 if (m_playOnce) {
204 bool atEnd = false;
205 if (m_tag) {
206 switch (m_tag->aniDir()) {
207 case AniDir::FORWARD:
208 atEnd = (frame == m_tag->toFrame());
209 break;
210 case AniDir::REVERSE:
211 atEnd = (frame == m_tag->fromFrame());
212 break;
213 case AniDir::PING_PONG:
214 atEnd = (!m_pingPongForward &&
215 frame == m_tag->fromFrame());
216 break;
217 }
218 }
219 else {
220 atEnd = (frame == sprite->lastFrame());
221 }
222 if (atEnd) {
223 m_editor->stop();
224 break;
225 }
226 }
227
228 frame = calculate_next_frame(
229 sprite, frame, frame_t(1), m_tag,
230 m_pingPongForward);
231
232 m_editor->setFrame(frame);
233 m_nextFrameTime += getNextFrameTime();
234 }
235
236 m_curFrameTick = base::current_tick();
237}
238
239// Before executing any command, we stop the animation
240void PlayState::onBeforeCommandExecution(CommandExecutionEvent& ev)
241{
242 // This check just in case we stay connected to context signals when
243 // the editor is already deleted.
244 ASSERT(m_editor);
245 ASSERT(m_editor->manager() == ui::Manager::getDefault());
246
247 // If the command is for other editor, we don't stop the animation.
248 if (!m_editor->isActive())
249 return;
250
251 // If we're executing PlayAnimation command, it means that the
252 // user wants to stop the animation. We cannot stop the animation
253 // here, because if it's stopped, PlayAnimation will re-play it
254 // (so it would be impossible to stop the animation using
255 // PlayAnimation command/Enter key).
256 //
257 // There are other commands that just doesn't stop the animation
258 // (zoom, scroll, etc.)
259 if (ev.command()->id() == CommandId::PlayAnimation() ||
260 ev.command()->id() == CommandId::Zoom() ||
261 ev.command()->id() == CommandId::Scroll() ||
262 ev.command()->id() == CommandId::Timeline()) {
263 return;
264 }
265
266 m_editor->stop();
267}
268
269double PlayState::getNextFrameTime()
270{
271 return
272 m_editor->sprite()->frameDuration(m_editor->frame())
273 / m_editor->getAnimationSpeedMultiplier(); // The "speed multiplier" is a "duration divider"
274}
275
276} // namespace app
277