1// Aseprite
2// Copyright (C) 2019-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/app.h"
13#include "app/commands/command.h"
14#include "app/commands/commands.h"
15#include "app/commands/new_params.h"
16#include "app/context.h"
17#include "app/context_access.h"
18#include "app/doc_access.h"
19#include "app/doc_api.h"
20#include "app/i18n/strings.h"
21#include "app/modules/editors.h"
22#include "app/modules/gui.h"
23#include "app/modules/palettes.h"
24#include "app/pref/preferences.h"
25#include "app/tx.h"
26#include "app/ui/drop_down_button.h"
27#include "app/ui/editor/editor.h"
28#include "app/ui/editor/editor_decorator.h"
29#include "app/ui/editor/select_box_state.h"
30#include "app/ui/editor/standby_state.h"
31#include "app/ui/workspace.h"
32#include "doc/cel.h"
33#include "doc/image.h"
34#include "doc/layer.h"
35#include "doc/palette.h"
36#include "doc/primitives.h"
37#include "doc/sprite.h"
38#include "render/render.h"
39#include "ui/ui.h"
40
41#include "import_sprite_sheet.xml.h"
42
43namespace app {
44
45using namespace ui;
46
47struct ImportSpriteSheetParams : public NewParams {
48 Param<bool> ui { this, true, "ui" };
49 Param<app::SpriteSheetType> type { this, app::SpriteSheetType::None, "type" };
50 Param<gfx::Rect> frameBounds { this, gfx::Rect(0, 0, 0, 0), "frameBounds" };
51 Param<gfx::Size> padding { this, gfx::Size(0, 0), "padding" };
52 Param<bool> partialTiles { this, false, "partialTiles" };
53};
54
55class ImportSpriteSheetWindow : public app::gen::ImportSpriteSheet
56 , public SelectBoxDelegate {
57public:
58 ImportSpriteSheetWindow(Context* context)
59 : m_context(context)
60 , m_document(NULL)
61 , m_editor(NULL)
62 , m_fileOpened(false)
63 , m_docPref(nullptr) {
64 import()->setEnabled(false);
65
66 static_assert(
67 (int)app::SpriteSheetType::Horizontal == 1 &&
68 (int)app::SpriteSheetType::Vertical == 2 &&
69 (int)app::SpriteSheetType::Rows == 3 &&
70 (int)app::SpriteSheetType::Columns == 4,
71 "SpriteSheetType enum changed");
72
73 sheetType()->addItem(Strings::import_sprite_sheet_type_horz());
74 sheetType()->addItem(Strings::import_sprite_sheet_type_vert());
75 sheetType()->addItem(Strings::import_sprite_sheet_type_rows());
76 sheetType()->addItem(Strings::import_sprite_sheet_type_cols());
77 sheetType()->setSelectedItemIndex((int)app::SpriteSheetType::Rows-1);
78
79 sheetType()->Change.connect([this]{ onSheetTypeChange(); });
80 x()->Change.connect([this]{ onEntriesChange(); });
81 y()->Change.connect([this]{ onEntriesChange(); });
82 width()->Change.connect([this]{ onEntriesChange(); });
83 height()->Change.connect([this]{ onEntriesChange(); });
84 paddingEnabled()->Click.connect([this]{ onPaddingEnabledChange(); });
85 horizontalPadding()->Change.connect([this]{ onEntriesChange(); });
86 verticalPadding()->Change.connect([this]{ onEntriesChange(); });
87 partialTiles()->Click.connect([this]{ onEntriesChange(); });
88 selectFile()->Click.connect([this]{ onSelectFile(); });
89
90 remapWindow();
91 centerWindow();
92 load_window_pos(this, "ImportSpriteSheet");
93
94 if (m_context->activeDocument()) {
95 selectActiveDocument();
96 m_fileOpened = false;
97 }
98
99 onPaddingEnabledChange();
100 }
101
102 ~ImportSpriteSheetWindow() {
103 releaseEditor();
104 }
105
106 SpriteSheetType sheetTypeValue() const {
107 return (app::SpriteSheetType)(sheetType()->getSelectedItemIndex()+1);
108 }
109
110 bool partialTilesValue() const {
111 return partialTiles()->isSelected();
112 }
113
114 bool paddingEnabledValue() const {
115 return paddingEnabled()->isSelected();
116 }
117
118 bool ok() const {
119 return closer() == import();
120 }
121
122 Doc* document() const {
123 return m_document;
124 }
125
126 DocumentPreferences* docPref() const {
127 return m_docPref;
128 }
129
130 gfx::Rect frameBounds() const {
131 return m_rect;
132 }
133
134 gfx::Size paddingThickness() const {
135 return m_padding;
136 }
137
138 void updateParams(ImportSpriteSheetParams& params) {
139 params.type(sheetTypeValue());
140 params.frameBounds(frameBounds());
141 params.partialTiles(partialTilesValue());
142
143 if (paddingEnabledValue())
144 params.padding(paddingThickness());
145 else
146 params.padding(gfx::Size(0, 0));
147 }
148
149protected:
150
151 void onSheetTypeChange() {
152 updateGridState();
153 }
154
155 void onSelectFile() {
156 Doc* oldActiveDocument = m_context->activeDocument();
157 Command* openFile = Commands::instance()->byId(CommandId::OpenFile());
158 Params params;
159 params.set("filename", "");
160 m_context->executeCommand(openFile, params);
161
162 // The user have selected another document.
163 if (oldActiveDocument != m_context->activeDocument()) {
164 selectActiveDocument();
165 m_fileOpened = true;
166 }
167 }
168
169 gfx::Rect getRectFromEntries() {
170 int w = width()->textInt();
171 int h = height()->textInt();
172
173 return gfx::Rect(
174 x()->textInt(),
175 y()->textInt(),
176 std::max<int>(1, w),
177 std::max<int>(1, h));
178 }
179
180 gfx::Size getPaddingFromEntries() {
181 int padW = horizontalPadding()->textInt();
182 int padH = verticalPadding()->textInt();
183 if (padW < 0)
184 padW = 0;
185 if (padH < 0)
186 padH = 0;
187 return gfx::Size(padW, padH);
188 }
189
190 void onEntriesChange() {
191 m_rect = getRectFromEntries();
192 m_padding = getPaddingFromEntries();
193
194 // Redraw new rulers position
195 if (m_editor) {
196 EditorStatePtr state = m_editor->getState();
197 SelectBoxState* boxState = dynamic_cast<SelectBoxState*>(state.get());
198 boxState->setBoxBounds(m_rect);
199 boxState->setPaddingBounds(m_padding);
200 if (partialTilesValue())
201 boxState->setFlag(SelectBoxState::Flags::IncludePartialTiles);
202 else
203 boxState->clearFlag(SelectBoxState::Flags::IncludePartialTiles);
204 m_editor->invalidate();
205 }
206 }
207
208 bool onProcessMessage(ui::Message* msg) override {
209 switch (msg->type()) {
210 case kCloseMessage:
211 save_window_pos(this, "ImportSpriteSheet");
212 break;
213 }
214 return Window::onProcessMessage(msg);
215 }
216
217 void onBroadcastMouseMessage(const gfx::Point& screenPos,
218 WidgetsList& targets) override {
219 Window::onBroadcastMouseMessage(screenPos, targets);
220
221 // Add the editor as receptor of mouse events too.
222 if (m_editor)
223 targets.push_back(View::getView(m_editor));
224 }
225
226 // SelectBoxDelegate impleentation
227 void onChangeRectangle(const gfx::Rect& rect) override {
228 m_rect = rect;
229
230 x()->setTextf("%d", m_rect.x);
231 y()->setTextf("%d", m_rect.y);
232 width()->setTextf("%d", m_rect.w);
233 height()->setTextf("%d", m_rect.h);
234 }
235
236 void onChangePadding(const gfx::Size& padding) override {
237 if (paddingEnabled()->isSelected()) {
238 m_padding = padding;
239 if (padding.w < 0)
240 m_padding.w = 0;
241 if (padding.h < 0)
242 m_padding.h = 0;
243 horizontalPadding()->setTextf("%d", m_padding.w);
244 verticalPadding()->setTextf("%d", m_padding.h);
245 }
246 else {
247 m_padding = gfx::Size(0, 0);
248 horizontalPadding()->setTextf("%d", 0);
249 verticalPadding()->setTextf("%d", 0);
250 }
251 }
252
253 std::string onGetContextBarHelp() override {
254 return Strings::import_sprite_sheet_context_bar_help();
255 }
256
257private:
258 void selectActiveDocument() {
259 Doc* oldDocument = m_document;
260 m_document = m_context->activeDocument();
261
262 // If the user already have selected a file, we have to destroy
263 // that file in order to select the new one.
264 if (oldDocument) {
265 releaseEditor();
266
267 if (m_fileOpened) {
268 DocDestroyer destroyer(m_context, oldDocument, 100);
269 destroyer.destroyDocument();
270 }
271 }
272
273 captureEditor();
274
275 import()->setEnabled(m_document ? true: false);
276
277 if (m_document) {
278 m_docPref = &Preferences::instance().document(m_document);
279
280 if (m_docPref->importSpriteSheet.type() >= app::SpriteSheetType::Horizontal &&
281 m_docPref->importSpriteSheet.type() <= app::SpriteSheetType::Columns)
282 sheetType()->setSelectedItemIndex((int)m_docPref->importSpriteSheet.type()-1);
283 else
284 sheetType()->setSelectedItemIndex((int)app::SpriteSheetType::Rows-1);
285
286 gfx::Rect defBounds = m_docPref->importSpriteSheet.bounds();
287 if (defBounds.isEmpty()) {
288 if (m_document->isMaskVisible())
289 defBounds = m_document->mask()->bounds();
290 else
291 defBounds = m_document->sprite()->gridBounds();
292 }
293 onChangeRectangle(defBounds);
294
295 gfx::Size defPaddingBounds = m_docPref->importSpriteSheet.paddingBounds();
296 if (defPaddingBounds.w < 0 || defPaddingBounds.h < 0)
297 defPaddingBounds = gfx::Size(0, 0);
298 onChangePadding(defPaddingBounds);
299
300 paddingEnabled()->setSelected(m_docPref->importSpriteSheet.paddingEnabled());
301 partialTiles()->setSelected(m_docPref->importSpriteSheet.partialTiles());
302 onEntriesChange();
303 onPaddingEnabledChange();
304 }
305 }
306
307 void captureEditor() {
308 ASSERT(m_editor == NULL);
309
310 if (m_document && !m_editor) {
311 m_rect = getRectFromEntries();
312 m_padding = getPaddingFromEntries();
313 m_editor = current_editor;
314 m_editorState.reset(
315 new SelectBoxState(
316 this, m_rect,
317 SelectBoxState::Flags(
318 int(SelectBoxState::Flags::Rulers) |
319 int(SelectBoxState::Flags::Grid) |
320 int(SelectBoxState::Flags::DarkOutside) |
321 int(SelectBoxState::Flags::PaddingRulers)
322 )));
323
324 m_editor->setState(m_editorState);
325 updateGridState();
326 }
327 }
328
329 void updateGridState() {
330 if (!m_editorState)
331 return;
332
333 int flags = (int)static_cast<SelectBoxState*>(m_editorState.get())->getFlags();
334 flags = flags & ~((int)SelectBoxState::Flags::HGrid | (int)SelectBoxState::Flags::VGrid);
335 switch (sheetTypeValue()) {
336 case SpriteSheetType::Horizontal:
337 flags |= int(SelectBoxState::Flags::HGrid);
338 break;
339 case SpriteSheetType::Vertical:
340 flags |= int(SelectBoxState::Flags::VGrid);
341 break;
342 case SpriteSheetType::Rows:
343 case SpriteSheetType::Columns:
344 flags |= int(SelectBoxState::Flags::Grid);
345 break;
346 }
347
348 static_cast<SelectBoxState*>(m_editorState.get())->setFlags(SelectBoxState::Flags(flags));
349 m_editor->invalidate();
350 }
351
352 void releaseEditor() {
353 if (m_editor) {
354 m_editor->backToPreviousState();
355 m_editor = NULL;
356 }
357 }
358
359 void onPaddingEnabledChange() {
360 const bool state = paddingEnabled()->isSelected();
361 horizontalPaddingLabel()->setVisible(state);
362 horizontalPadding()->setVisible(state);
363 verticalPaddingLabel()->setVisible(state);
364 verticalPadding()->setVisible(state);
365 if (m_docPref) {
366 if (state)
367 onChangePadding(m_docPref->importSpriteSheet.paddingBounds());
368 else {
369 m_docPref->importSpriteSheet.paddingBounds(m_padding);
370 onChangePadding(gfx::Size(0, 0));
371 }
372 }
373
374 onEntriesChange();
375 resize();
376 }
377
378 void resize() {
379 expandWindow(sizeHint());
380 }
381
382 Context* m_context;
383 Doc* m_document;
384 Editor* m_editor;
385 EditorStatePtr m_editorState;
386 gfx::Rect m_rect;
387 gfx::Size m_padding;
388
389 // True if the user has been opened the file (instead of selecting
390 // the current document).
391 bool m_fileOpened;
392
393 DocumentPreferences* m_docPref;
394};
395
396class ImportSpriteSheetCommand : public CommandWithNewParams<ImportSpriteSheetParams> {
397public:
398 ImportSpriteSheetCommand();
399
400protected:
401 virtual void onExecute(Context* context) override;
402};
403
404ImportSpriteSheetCommand::ImportSpriteSheetCommand()
405 : CommandWithNewParams(CommandId::ImportSpriteSheet(), CmdRecordableFlag)
406{
407}
408
409void ImportSpriteSheetCommand::onExecute(Context* context)
410{
411 Doc* document;
412 auto& params = this->params();
413
414#ifdef ENABLE_UI
415 if (context->isUIAvailable() && params.ui()) {
416 // TODO use params as input values for the ImportSpriteSheetWindow
417
418 ImportSpriteSheetWindow window(context);
419 window.openWindowInForeground();
420 if (!window.ok())
421 return;
422
423 document = window.document();
424 if (!document)
425 return;
426
427 window.updateParams(params);
428
429 DocumentPreferences* docPref = window.docPref();
430 docPref->importSpriteSheet.type(params.type());
431 docPref->importSpriteSheet.bounds(params.frameBounds());
432 docPref->importSpriteSheet.partialTiles(params.partialTiles());
433 docPref->importSpriteSheet.paddingBounds(params.padding());
434 docPref->importSpriteSheet.paddingEnabled(window.paddingEnabledValue());
435 }
436 else // We import the sprite sheet from the active document if there is no UI
437#endif
438 {
439 document = context->activeDocument();
440 if (!document)
441 return;
442 }
443
444 // The list of frames imported from the sheet
445 std::vector<ImageRef> animation;
446
447 try {
448 Sprite* sprite = document->sprite();
449 frame_t currentFrame = context->activeSite().frame();
450 gfx::Rect frameBounds = params.frameBounds();
451 const gfx::Size padding = params.padding();
452 render::Render render;
453 render.setNewBlend(Preferences::instance().experimental.newBlend());
454
455 if (frameBounds.isEmpty())
456 frameBounds = sprite->bounds();
457
458 // Each sprite in the sheet
459 std::vector<gfx::Rect> tileRects;
460 int widthStop = sprite->width();
461 int heightStop = sprite->height();
462 if (params.partialTiles()) {
463 widthStop += frameBounds.w-1;
464 heightStop += frameBounds.h-1;
465 }
466
467 switch (params.type()) {
468 case app::SpriteSheetType::Horizontal:
469 for (int x=frameBounds.x; x+frameBounds.w<=widthStop; x+=frameBounds.w+padding.w) {
470 tileRects.push_back(gfx::Rect(x, frameBounds.y, frameBounds.w, frameBounds.h));
471 }
472 break;
473 case app::SpriteSheetType::Vertical:
474 for (int y=frameBounds.y; y+frameBounds.h<=heightStop; y+=frameBounds.h+padding.h) {
475 tileRects.push_back(gfx::Rect(frameBounds.x, y, frameBounds.w, frameBounds.h));
476 }
477 break;
478 case app::SpriteSheetType::Rows:
479 for (int y=frameBounds.y; y+frameBounds.h<=heightStop; y+=frameBounds.h+padding.h) {
480 for (int x=frameBounds.x; x+frameBounds.w<=widthStop; x+=frameBounds.w+padding.w) {
481 tileRects.push_back(gfx::Rect(x, y, frameBounds.w, frameBounds.h));
482 }
483 }
484 break;
485 case app::SpriteSheetType::Columns:
486 for (int x=frameBounds.x; x+frameBounds.w<=widthStop; x+=frameBounds.w+padding.w) {
487 for (int y=frameBounds.y; y+frameBounds.h<=heightStop; y+=frameBounds.h+padding.h) {
488 tileRects.push_back(gfx::Rect(x, y, frameBounds.w, frameBounds.h));
489 }
490 }
491 break;
492 }
493
494 // As first step, we cut each tile and add them into "animation" list.
495 for (const auto& tileRect : tileRects) {
496 ImageRef resultImage(
497 Image::create(
498 sprite->pixelFormat(), tileRect.w, tileRect.h));
499
500 // Render the portion of sheet.
501 render.renderSprite(
502 resultImage.get(), sprite, currentFrame,
503 gfx::Clip(0, 0, tileRect));
504
505 animation.push_back(resultImage);
506 }
507
508 if (animation.size() == 0) {
509 Alert::show(Strings::alerts_empty_rect_importing_sprite_sheet());
510 return;
511 }
512
513 // The following steps modify the sprite, so we wrap all
514 // operations in a undo-transaction.
515 ContextWriter writer(context);
516 Tx tx(
517 writer.context(), Strings::import_sprite_sheet_title(), ModifyDocument);
518 DocApi api = document->getApi(tx);
519
520 // Add the layer in the sprite.
521 LayerImage* resultLayer =
522 api.newLayer(sprite->root(), Strings::import_sprite_sheet_layer_name());
523
524 // Add all frames+cels to the new layer
525 for (size_t i=0; i<animation.size(); ++i) {
526 // Create the cel.
527 std::unique_ptr<Cel> resultCel(new Cel(frame_t(i), animation[i]));
528
529 // Add the cel in the layer.
530 api.addCel(resultLayer, resultCel.get());
531 resultCel.release();
532 }
533
534 // Copy the list of layers (because we will modify it in the iteration).
535 LayerList layers = sprite->root()->layers();
536
537 // Remove all other layers
538 for (Layer* child : layers) {
539 if (child != resultLayer)
540 api.removeLayer(child);
541 }
542
543 // Change the number of frames
544 api.setTotalFrames(sprite, frame_t(animation.size()));
545
546 // Set the size of the sprite to the tile size.
547 api.setSpriteSize(sprite, frameBounds.w, frameBounds.h);
548
549 tx.commit();
550 }
551 catch (...) {
552 throw;
553 }
554
555#ifdef ENABLE_UI
556 if (context->isUIAvailable())
557 update_screen_for_document(document);
558#endif
559}
560
561Command* CommandFactory::createImportSpriteSheetCommand()
562{
563 return new ImportSpriteSheetCommand;
564}
565
566} // namespace app
567