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