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/moving_cel_state.h"
13
14#include "app/app.h"
15#include "app/cmd/set_cel_bounds.h"
16#include "app/commands/command.h"
17#include "app/context_access.h"
18#include "app/doc_api.h"
19#include "app/doc_range.h"
20#include "app/tx.h"
21#include "app/ui/editor/editor.h"
22#include "app/ui/editor/editor_customization_delegate.h"
23#include "app/ui/main_window.h"
24#include "app/ui/status_bar.h"
25#include "app/ui/timeline/timeline.h"
26#include "app/ui_context.h"
27#include "app/util/range_utils.h"
28#include "doc/cel.h"
29#include "doc/layer.h"
30#include "doc/mask.h"
31#include "doc/sprite.h"
32#include "fmt/format.h"
33#include "ui/message.h"
34
35#include <algorithm>
36#include <cmath>
37
38namespace app {
39
40using namespace ui;
41
42MovingCelCollect::MovingCelCollect(Editor* editor, Layer* layer)
43 : m_mainCel(nullptr)
44{
45 ASSERT(editor);
46
47 if (layer && layer->isImage())
48 m_mainCel = layer->cel(editor->frame());
49
50 Timeline* timeline = App::instance()->timeline();
51 DocRange range = timeline->range();
52 if (!range.enabled() ||
53 !timeline->isVisible()) {
54 range.startRange(editor->layer(), editor->frame(), DocRange::kCels);
55 range.endRange(editor->layer(), editor->frame());
56 }
57
58 DocRange range2 = range;
59 for (Layer* layer : range.selectedLayers()) {
60 if (layer && layer->isGroup()) {
61 LayerList childrenList;
62 static_cast<LayerGroup*>(layer)->allLayers(childrenList);
63
64 SelectedLayers selChildren;
65 for (auto layer : childrenList)
66 selChildren.insert(layer);
67
68 range2.selectLayers(selChildren);
69 }
70 }
71
72 // Record start positions of all cels in selected range
73 for (Cel* cel : get_unique_cels_to_move_cel(editor->sprite(), range2)) {
74 Layer* layer = cel->layer();
75 ASSERT(layer);
76
77 if (layer && layer->isMovable() && !layer->isBackground())
78 m_celList.push_back(cel);
79 }
80}
81
82MovingCelState::MovingCelState(Editor* editor,
83 const MouseMessage* msg,
84 const HandleType handle,
85 const MovingCelCollect& collect)
86 : m_reader(UIContext::instance(), 500)
87 , m_delayedMouseMove(this, editor, 5)
88 , m_cel(nullptr)
89 , m_celList(collect.celList())
90 , m_celOffset(0.0, 0.0)
91 , m_celScale(1.0, 1.0)
92 , m_handle(handle)
93 , m_editor(editor)
94{
95 ContextWriter writer(m_reader);
96 Doc* document = editor->document();
97 ASSERT(!m_celList.empty());
98
99 m_cel = collect.mainCel();
100 if (m_cel) {
101 if (m_cel->data()->hasBoundsF())
102 m_celMainSize = m_cel->boundsF().size();
103 else
104 m_celMainSize = gfx::SizeF(m_cel->bounds().size());
105 }
106
107 // Record start positions of all cels in selected range
108 for (Cel* cel : m_celList) {
109 Layer* layer = cel->layer();
110 ASSERT(layer);
111
112 if (layer && layer->isMovable() && !layer->isBackground()) {
113 if (layer->isReference()) {
114 m_celStarts.push_back(cel->boundsF());
115 m_hasReference = true;
116 }
117 else
118 m_celStarts.push_back(cel->bounds());
119 }
120 }
121
122 // Hook BeforeCommandExecution signal so we know if the user wants
123 // to execute other command, so we can drop pixels.
124 m_ctxConn = UIContext::instance()->BeforeCommandExecution.connect(
125 &MovingCelState::onBeforeCommandExecution, this);
126
127 m_cursorStart = editor->screenToEditorF(msg->position());
128 editor->captureMouse();
129
130 // Hide the mask (temporarily, until mouse-up event)
131 m_maskVisible = document->isMaskVisible();
132 if (m_maskVisible) {
133 document->setMaskVisible(false);
134 document->generateMaskBoundaries();
135 }
136
137 m_delayedMouseMove.onMouseDown(msg);
138}
139
140void MovingCelState::onBeforePopState(Editor* editor)
141{
142 m_ctxConn.disconnect();
143 StandbyState::onBeforePopState(editor);
144}
145
146bool MovingCelState::onMouseUp(Editor* editor, MouseMessage* msg)
147{
148 m_delayedMouseMove.onMouseUp(msg);
149
150 Doc* document = editor->document();
151 bool modified = restoreCelStartPosition();
152
153 if (modified) {
154 {
155 ContextWriter writer(m_reader, 1000);
156 Tx tx(writer.context(), "Cel Movement", ModifyDocument);
157 DocApi api = document->getApi(tx);
158 gfx::Point intOffset = intCelOffset();
159
160 // And now we move the cel (or all selected range) to the new position.
161 for (Cel* cel : m_celList) {
162 // Change reference layer with subpixel precision
163 if (cel->layer()->isReference()) {
164 gfx::RectF celBounds = cel->boundsF();
165 celBounds.x += m_celOffset.x;
166 celBounds.y += m_celOffset.y;
167 if (m_scaled) {
168 celBounds.w *= m_celScale.w;
169 celBounds.h *= m_celScale.h;
170 }
171 tx(new cmd::SetCelBoundsF(cel, celBounds));
172 }
173 else {
174 api.setCelPosition(writer.sprite(), cel,
175 cel->x() + intOffset.x,
176 cel->y() + intOffset.y);
177 }
178 }
179
180 // Move selection if it was visible
181 if (m_maskVisible) {
182 // TODO Moving the mask when we move a ref layer (e.g. by
183 // m_celOffset=(0.5,0.5)) will not move the final
184 // position of the mask (so the ref layer is moved and
185 // the mask isn't).
186
187 api.setMaskPosition(document->mask()->bounds().x + intOffset.x,
188 document->mask()->bounds().y + intOffset.y);
189 }
190
191 tx.commit();
192 }
193
194 // Redraw all editors. We've to notify all views about this
195 // general update because MovingCelState::onMouseMove() redraws
196 // only the cels in the current editor. And at this point we'd
197 // like to update all the editors.
198 document->notifyGeneralUpdate();
199 }
200 // Just a click in the current layer
201 else if (!m_moved & !m_scaled) {
202 // Deselect the whole range if we are in "Auto Select Layer"
203 if (editor->isAutoSelectLayer()) {
204 App::instance()->timeline()->clearAndInvalidateRange();
205 }
206 }
207
208 // Restore the mask visibility.
209 if (m_maskVisible) {
210 document->setMaskVisible(m_maskVisible);
211 document->generateMaskBoundaries();
212 }
213
214 editor->backToPreviousState();
215 editor->releaseMouse();
216 return true;
217}
218
219bool MovingCelState::onMouseMove(Editor* editor, MouseMessage* msg)
220{
221 m_delayedMouseMove.onMouseMove(msg);
222
223 // Use StandbyState implementation
224 return StandbyState::onMouseMove(editor, msg);
225}
226
227void MovingCelState::onCommitMouseMove(Editor* editor,
228 const gfx::PointF& newCursorPos)
229{
230 switch (m_handle) {
231
232 case MovePixelsHandle:
233 m_celOffset = newCursorPos - m_cursorStart;
234 if (int(editor->getCustomizationDelegate()
235 ->getPressedKeyAction(KeyContext::TranslatingSelection) & KeyAction::LockAxis)) {
236 if (ABS(m_celOffset.x) < ABS(m_celOffset.y)) {
237 m_celOffset.x = 0;
238 }
239 else {
240 m_celOffset.y = 0;
241 }
242 }
243 if (!m_moved && intCelOffset() != gfx::Point(0, 0))
244 m_moved = true;
245 break;
246
247 case ScaleSEHandle: {
248 gfx::PointF delta(newCursorPos - m_cursorStart);
249 m_celScale.w = 1.0 + (delta.x / m_celMainSize.w);
250 m_celScale.h = 1.0 + (delta.y / m_celMainSize.h);
251 if (m_celScale.w < 1.0/m_celMainSize.w) m_celScale.w = 1.0/m_celMainSize.w;
252 if (m_celScale.h < 1.0/m_celMainSize.h) m_celScale.h = 1.0/m_celMainSize.h;
253
254 if (int(editor->getCustomizationDelegate()
255 ->getPressedKeyAction(KeyContext::ScalingSelection) & KeyAction::MaintainAspectRatio)) {
256 m_celScale.w = m_celScale.h = std::max(m_celScale.w, m_celScale.h);
257 }
258
259 m_scaled = true;
260 break;
261 }
262 }
263
264 gfx::Point intOffset = intCelOffset();
265
266 for (size_t i=0; i<m_celList.size(); ++i) {
267 Cel* cel = m_celList[i];
268 gfx::RectF celBounds = m_celStarts[i];
269
270 if (cel->layer()->isReference()) {
271 celBounds.x += m_celOffset.x;
272 celBounds.y += m_celOffset.y;
273 m_moved = true;
274 if (m_scaled) {
275 celBounds.w *= m_celScale.w;
276 celBounds.h *= m_celScale.h;
277 }
278 cel->setBoundsF(celBounds);
279 }
280 else {
281 celBounds.x += intOffset.x;
282 celBounds.y += intOffset.y;
283 cel->setBounds(gfx::Rect(celBounds));
284 }
285 }
286
287 // Redraw the new cel position.
288 editor->invalidate();
289
290 // Redraw status bar with the new position of cels (without this the
291 // previous position before this onCommitMouseMove() is still
292 // displayed in the screen).
293 editor->updateStatusBar();
294}
295
296bool MovingCelState::onKeyDown(Editor* editor, KeyMessage* msg)
297{
298 // Do not call StandbyState::onKeyDown() so we don't start a
299 // straight line when we are moving the cel with Ctrl and Shift key
300 // is pressed.
301 //
302 // TODO maybe MovingCelState shouldn't be a StandbyState (the same
303 // for several other states)
304 return false;
305}
306
307bool MovingCelState::onUpdateStatusBar(Editor* editor)
308{
309 gfx::PointF pos = m_celOffset + m_cursorStart - gfx::PointF(editor->mainTilePosition());
310 gfx::RectF fullBounds = calcFullBounds();
311 std::string buf;
312
313 if (m_hasReference) {
314 buf = fmt::format(":pos: {:.2f} {:.2f}", pos.x, pos.y);
315 if (m_scaled && m_cel) {
316 buf += fmt::format(
317 " :start: {:.2f} {:.2f}"
318 " :size: {:.2f} {:.2f} [{:.2f}% {:.2f}%]",
319 m_cel->boundsF().x,
320 m_cel->boundsF().y,
321 m_celScale.w*m_celMainSize.w,
322 m_celScale.h*m_celMainSize.h,
323 100.0*m_celScale.w*m_celMainSize.w/m_cel->image()->width(),
324 100.0*m_celScale.h*m_celMainSize.h/m_cel->image()->height());
325 }
326 else {
327 buf += fmt::format(
328 " :start: {:.2f} {:.2f} :size: {:.2f} {:.2f}"
329 " :delta: {:.2f} {:.2f}",
330 fullBounds.x, fullBounds.y,
331 fullBounds.w, fullBounds.h,
332 m_celOffset.x, m_celOffset.y);
333 }
334 }
335 else {
336 gfx::Point intOffset = intCelOffset();
337 fullBounds.floor();
338 buf = fmt::format(
339 ":pos: {} {}"
340 " :start: {} {} :size: {} {}"
341 " :delta: {} {}",
342 int(pos.x), int(pos.y),
343 int(fullBounds.x), int(fullBounds.y),
344 int(fullBounds.w), int(fullBounds.h),
345 intOffset.x, intOffset.y);
346 }
347
348 StatusBar::instance()->setStatusText(0, buf);
349 return true;
350}
351
352gfx::Point MovingCelState::intCelOffset() const
353{
354 return gfx::Point(int(std::round(m_celOffset.x)),
355 int(std::round(m_celOffset.y)));
356}
357
358gfx::RectF MovingCelState::calcFullBounds() const
359{
360 gfx::RectF bounds;
361 for (Cel* cel : m_celList) {
362 if (cel->data()->hasBoundsF())
363 bounds |= cel->boundsF();
364 else
365 bounds |= gfx::RectF(cel->bounds()).floor();
366 }
367 return bounds;
368}
369
370bool MovingCelState::restoreCelStartPosition() const
371{
372 bool modified = false;
373
374 // Here we put back all cels into their original coordinates (so we
375 // can add the undo information from the start position).
376 for (size_t i=0; i<m_celList.size(); ++i) {
377 Cel* cel = m_celList[i];
378 const gfx::RectF& celStart = m_celStarts[i];
379
380 if (cel->layer()->isReference()) {
381 if (cel->boundsF() != celStart) {
382 cel->setBoundsF(celStart);
383 modified = true;
384 }
385 }
386 else {
387 if (cel->bounds() != gfx::Rect(celStart)) {
388 cel->setBounds(gfx::Rect(celStart));
389 modified = true;
390 }
391 }
392 }
393 return modified;
394}
395
396void MovingCelState::onBeforeCommandExecution(CommandExecutionEvent& ev)
397{
398 if (ev.command()->id() == CommandId::Undo() ||
399 ev.command()->id() == CommandId::Redo() ||
400 ev.command()->id() == CommandId::Cancel()) {
401 restoreCelStartPosition();
402 Doc* document = m_editor->document();
403 // Restore the mask visibility.
404 if (m_maskVisible) {
405 document->setMaskVisible(m_maskVisible);
406 document->generateMaskBoundaries();
407 }
408 m_editor->backToPreviousState();
409 m_editor->releaseMouse();
410 m_editor->invalidate();
411 }
412 ev.cancel();
413}
414
415} // namespace app
416