| 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 "ui/ui.h" |
| 13 | |
| 14 | #include "app/commands/command.h" |
| 15 | #include "app/commands/commands.h" |
| 16 | #include "app/context.h" |
| 17 | #include "app/modules/editors.h" |
| 18 | #include "app/modules/gfx.h" |
| 19 | #include "app/pref/preferences.h" |
| 20 | #include "app/ui/editor/editor.h" |
| 21 | #include "app/ui/editor/editor_render.h" |
| 22 | #include "app/ui/keyboard_shortcuts.h" |
| 23 | #include "app/ui/status_bar.h" |
| 24 | #include "app/util/conversion_to_surface.h" |
| 25 | #include "doc/image.h" |
| 26 | #include "doc/palette.h" |
| 27 | #include "doc/primitives.h" |
| 28 | #include "doc/sprite.h" |
| 29 | #include "os/surface.h" |
| 30 | #include "os/system.h" |
| 31 | |
| 32 | #include <cstring> |
| 33 | |
| 34 | #define PREVIEW_TILED 1 |
| 35 | #define PREVIEW_FIT_ON_SCREEN 2 |
| 36 | |
| 37 | namespace app { |
| 38 | |
| 39 | using namespace ui; |
| 40 | using namespace doc; |
| 41 | using namespace filters; |
| 42 | |
| 43 | class PreviewWindow : public Window { |
| 44 | public: |
| 45 | PreviewWindow(Context* context, Editor* editor) |
| 46 | : Window(DesktopWindow) |
| 47 | , m_context(context) |
| 48 | , m_editor(editor) |
| 49 | , m_doc(editor->document()) |
| 50 | , m_sprite(editor->sprite()) |
| 51 | , m_pal(m_sprite->palette(editor->frame())) |
| 52 | , m_proj(editor->projection()) |
| 53 | , m_index_bg_color(-1) |
| 54 | , m_doublebuf(Image::create( |
| 55 | IMAGE_RGB, |
| 56 | editor->display()->size().w, |
| 57 | editor->display()->size().h)) |
| 58 | , m_doublesur(os::instance()->makeRgbaSurface( |
| 59 | editor->display()->size().w, |
| 60 | editor->display()->size().h)) { |
| 61 | // Do not use DocWriter (do not lock the document) because we |
| 62 | // will call other sub-commands (e.g. previous frame, next frame, |
| 63 | // etc.). |
| 64 | View* view = View::getView(editor); |
| 65 | DocumentPreferences& docPref = Preferences::instance().document(m_doc); |
| 66 | m_tiled = (filters::TiledMode)docPref.tiled.mode(); |
| 67 | |
| 68 | // Free mouse |
| 69 | editor->manager()->freeMouse(); |
| 70 | |
| 71 | // Clear extras (e.g. pen preview) |
| 72 | m_doc->setExtraCel(ExtraCelRef(nullptr)); |
| 73 | |
| 74 | gfx::Rect vp = view->viewportBounds(); |
| 75 | gfx::Point scroll = view->viewScroll(); |
| 76 | |
| 77 | m_oldMousePos = mousePosInDisplay(); |
| 78 | m_pos.x = -scroll.x + vp.x + editor->padding().x; |
| 79 | m_pos.y = -scroll.y + vp.y + editor->padding().y; |
| 80 | |
| 81 | setFocusStop(true); |
| 82 | captureMouse(); |
| 83 | } |
| 84 | |
| 85 | protected: |
| 86 | virtual bool onProcessMessage(Message* msg) override { |
| 87 | switch (msg->type()) { |
| 88 | |
| 89 | case kCloseMessage: |
| 90 | releaseMouse(); |
| 91 | break; |
| 92 | |
| 93 | case kMouseMoveMessage: { |
| 94 | MouseMessage* mouseMsg = static_cast<MouseMessage*>(msg); |
| 95 | gfx::Point mousePos = mouseMsg->position(); |
| 96 | |
| 97 | gfx::Rect bounds = this->bounds(); |
| 98 | gfx::Border border; |
| 99 | if (bounds.w > 64*guiscale()) { |
| 100 | border.left(32*guiscale()); |
| 101 | border.right(32*guiscale()); |
| 102 | } |
| 103 | if (bounds.h > 64*guiscale()) { |
| 104 | border.top(32*guiscale()); |
| 105 | border.bottom(32*guiscale()); |
| 106 | } |
| 107 | |
| 108 | m_delta += mousePos - m_oldMousePos; |
| 109 | m_oldMousePos = mousePos; |
| 110 | |
| 111 | invalidate(); |
| 112 | break; |
| 113 | } |
| 114 | |
| 115 | case kMouseUpMessage: { |
| 116 | closeWindow(this); |
| 117 | break; |
| 118 | } |
| 119 | |
| 120 | case kKeyDownMessage: { |
| 121 | KeyMessage* keyMsg = static_cast<KeyMessage*>(msg); |
| 122 | Command* command = NULL; |
| 123 | Params params; |
| 124 | KeyboardShortcuts::instance() |
| 125 | ->getCommandFromKeyMessage(msg, &command, ¶ms); |
| 126 | |
| 127 | // Change frame |
| 128 | if (command != NULL && |
| 129 | (command->id() == CommandId::GotoFirstFrame() || |
| 130 | command->id() == CommandId::GotoPreviousFrame() || |
| 131 | command->id() == CommandId::GotoNextFrame() || |
| 132 | command->id() == CommandId::GotoLastFrame())) { |
| 133 | m_context->executeCommand(command, params); |
| 134 | invalidate(); |
| 135 | m_render.reset(nullptr); // Re-render |
| 136 | } |
| 137 | #if 0 |
| 138 | // Play the animation |
| 139 | else if (command != NULL && |
| 140 | std::strcmp(command->short_name(), CommandId::PlayAnimation()) == 0) { |
| 141 | // TODO |
| 142 | } |
| 143 | #endif |
| 144 | // Change background color |
| 145 | else if (keyMsg->scancode() == kKeyPlusPad || |
| 146 | keyMsg->unicodeChar() == '+') { |
| 147 | if (m_index_bg_color == -1 || |
| 148 | m_index_bg_color < m_pal->size()-1) { |
| 149 | ++m_index_bg_color; |
| 150 | |
| 151 | invalidate(); |
| 152 | } |
| 153 | } |
| 154 | else if (keyMsg->scancode() == kKeyMinusPad || |
| 155 | keyMsg->unicodeChar() == '-') { |
| 156 | if (m_index_bg_color >= 0) { |
| 157 | --m_index_bg_color; // can be -1 which is the checkered background |
| 158 | |
| 159 | invalidate(); |
| 160 | } |
| 161 | } |
| 162 | else { |
| 163 | closeWindow(this); |
| 164 | } |
| 165 | |
| 166 | return true; |
| 167 | } |
| 168 | |
| 169 | case kSetCursorMessage: |
| 170 | ui::set_mouse_cursor(kNoCursor); |
| 171 | return true; |
| 172 | } |
| 173 | |
| 174 | return Window::onProcessMessage(msg); |
| 175 | } |
| 176 | |
| 177 | virtual void onPaint(PaintEvent& ev) override { |
| 178 | gfx::Size displaySize = display()->size(); |
| 179 | Graphics* g = ev.graphics(); |
| 180 | EditorRender& render = m_editor->renderEngine(); |
| 181 | render.setRefLayersVisiblity(false); |
| 182 | render.setProjection(render::Projection()); |
| 183 | render.disableOnionskin(); |
| 184 | render.setTransparentBackground(); |
| 185 | |
| 186 | // Render sprite and leave the result in 'm_render' variable |
| 187 | if (m_render == nullptr) { |
| 188 | ImageBufferPtr buf = render.getRenderImageBuffer(); |
| 189 | m_render.reset(Image::create(IMAGE_RGB, |
| 190 | m_sprite->width(), m_sprite->height(), buf)); |
| 191 | |
| 192 | render.renderSprite( |
| 193 | m_render.get(), m_sprite, m_editor->frame()); |
| 194 | } |
| 195 | |
| 196 | int x, y, w, h, u, v; |
| 197 | x = m_pos.x + m_proj.applyX(m_proj.removeX(m_delta.x)); |
| 198 | y = m_pos.y + m_proj.applyY(m_proj.removeY(m_delta.y)); |
| 199 | w = m_proj.applyX(m_sprite->width()); |
| 200 | h = m_proj.applyY(m_sprite->height()); |
| 201 | |
| 202 | if (int(m_tiled) & int(TiledMode::X_AXIS)) x = SGN(x) * (ABS(x)%w); |
| 203 | if (int(m_tiled) & int(TiledMode::Y_AXIS)) y = SGN(y) * (ABS(y)%h); |
| 204 | |
| 205 | render.setProjection(m_proj); |
| 206 | if (m_index_bg_color == -1) { |
| 207 | render.setupBackground(m_doc, m_doublebuf->pixelFormat()); |
| 208 | render.renderCheckeredBackground( |
| 209 | m_doublebuf.get(), |
| 210 | gfx::Clip(0, 0, -m_pos.x, -m_pos.y, |
| 211 | m_doublebuf->width(), m_doublebuf->height())); |
| 212 | } |
| 213 | else { |
| 214 | doc::clear_image(m_doublebuf.get(), m_pal->getEntry(m_index_bg_color)); |
| 215 | } |
| 216 | |
| 217 | switch (m_tiled) { |
| 218 | case TiledMode::NONE: |
| 219 | render.renderImage(m_doublebuf.get(), m_render.get(), m_pal, x, y, |
| 220 | 255, doc::BlendMode::NORMAL); |
| 221 | break; |
| 222 | case TiledMode::X_AXIS: |
| 223 | for (u=x-w; u<displaySize.w+w; u+=w) |
| 224 | render.renderImage(m_doublebuf.get(), m_render.get(), m_pal, u, y, |
| 225 | 255, doc::BlendMode::NORMAL); |
| 226 | break; |
| 227 | case TiledMode::Y_AXIS: |
| 228 | for (v=y-h; v<displaySize.h+h; v+=h) |
| 229 | render.renderImage(m_doublebuf.get(), m_render.get(), m_pal, x, v, |
| 230 | 255, doc::BlendMode::NORMAL); |
| 231 | break; |
| 232 | case TiledMode::BOTH: |
| 233 | for (v=y-h; v<displaySize.h+h; v+=h) |
| 234 | for (u=x-w; u<displaySize.w+w; u+=w) |
| 235 | render.renderImage(m_doublebuf.get(), m_render.get(), m_pal, u, v, |
| 236 | 255, doc::BlendMode::NORMAL); |
| 237 | break; |
| 238 | } |
| 239 | |
| 240 | convert_image_to_surface(m_doublebuf.get(), m_pal, |
| 241 | m_doublesur.get(), 0, 0, 0, 0, m_doublebuf->width(), m_doublebuf->height()); |
| 242 | g->blit(m_doublesur.get(), 0, 0, 0, 0, m_doublesur->width(), m_doublesur->height()); |
| 243 | } |
| 244 | |
| 245 | private: |
| 246 | Context* m_context; |
| 247 | Editor* m_editor; |
| 248 | Doc* m_doc; |
| 249 | Sprite* m_sprite; |
| 250 | const Palette* m_pal; |
| 251 | gfx::Point m_pos; |
| 252 | gfx::Point m_oldMousePos; |
| 253 | gfx::Point m_delta; |
| 254 | render::Projection m_proj; |
| 255 | int m_index_bg_color; |
| 256 | std::unique_ptr<Image> m_render; |
| 257 | std::unique_ptr<Image> m_doublebuf; |
| 258 | os::SurfaceRef m_doublesur; |
| 259 | filters::TiledMode m_tiled; |
| 260 | }; |
| 261 | |
| 262 | class FullscreenPreviewCommand : public Command { |
| 263 | public: |
| 264 | FullscreenPreviewCommand(); |
| 265 | |
| 266 | protected: |
| 267 | bool onEnabled(Context* context) override; |
| 268 | void onExecute(Context* context) override; |
| 269 | }; |
| 270 | |
| 271 | FullscreenPreviewCommand::FullscreenPreviewCommand() |
| 272 | : Command(CommandId::FullscreenPreview(), CmdUIOnlyFlag) |
| 273 | { |
| 274 | } |
| 275 | |
| 276 | bool FullscreenPreviewCommand::onEnabled(Context* context) |
| 277 | { |
| 278 | return context->checkFlags(ContextFlags::ActiveDocumentIsWritable | |
| 279 | ContextFlags::HasActiveSprite); |
| 280 | } |
| 281 | |
| 282 | // Shows the sprite using the complete screen. |
| 283 | void FullscreenPreviewCommand::onExecute(Context* context) |
| 284 | { |
| 285 | Editor* editor = current_editor; |
| 286 | |
| 287 | // Cancel operation if current editor does not have a sprite |
| 288 | if (!editor || !editor->sprite()) |
| 289 | return; |
| 290 | |
| 291 | PreviewWindow window(context, editor); |
| 292 | window.openWindowInForeground(); |
| 293 | |
| 294 | // Check that the full screen invalidation code is working |
| 295 | // correctly. This check is just in case that some regression is |
| 296 | // introduced in ui::Manager() that doesn't handle correctly the |
| 297 | // invalidation of the manager when it's fully covered by the closed |
| 298 | // window (desktop windows, like PreviewWindow, match this case). |
| 299 | ASSERT(editor->manager()->hasFlags(DIRTY)); |
| 300 | } |
| 301 | |
| 302 | Command* CommandFactory::createFullscreenPreviewCommand() |
| 303 | { |
| 304 | return new FullscreenPreviewCommand; |
| 305 | } |
| 306 | |
| 307 | } // namespace app |
| 308 | |