| 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 | |