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 | |
30 | namespace app { |
31 | |
32 | using namespace ui; |
33 | |
34 | PlayState::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 | |
54 | Tag* PlayState::playingTag() const |
55 | { |
56 | return m_tag; |
57 | } |
58 | |
59 | void 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 | |
99 | EditorState::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 | |
112 | void PlayState::onBeforePopState(Editor* editor) |
113 | { |
114 | m_ctxConn.disconnect(); |
115 | StateWithWheelBehavior::onBeforePopState(editor); |
116 | } |
117 | |
118 | bool 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 | |
146 | bool PlayState::onMouseUp(Editor* editor, MouseMessage* msg) |
147 | { |
148 | editor->releaseMouse(); |
149 | return true; |
150 | } |
151 | |
152 | bool PlayState::onMouseMove(Editor* editor, MouseMessage* msg) |
153 | { |
154 | editor->updateStatusBar(); |
155 | return true; |
156 | } |
157 | |
158 | bool PlayState::onKeyDown(Editor* editor, KeyMessage* msg) |
159 | { |
160 | return false; |
161 | } |
162 | |
163 | bool PlayState::onKeyUp(Editor* editor, KeyMessage* msg) |
164 | { |
165 | return false; |
166 | } |
167 | |
168 | bool 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 | |
183 | void PlayState::onRemoveTag(Editor* editor, doc::Tag* tag) |
184 | { |
185 | if (m_tag == tag) |
186 | m_tag = nullptr; |
187 | } |
188 | |
189 | void 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 |
240 | void 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 | |
269 | double 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 | |