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 | |
43 | namespace app { |
44 | |
45 | using namespace ui; |
46 | |
47 | struct 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 | |
55 | class ImportSpriteSheetWindow : public app::gen::ImportSpriteSheet |
56 | , public SelectBoxDelegate { |
57 | public: |
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 | |
149 | protected: |
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 | |
257 | private: |
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 | |
396 | class ImportSpriteSheetCommand : public CommandWithNewParams<ImportSpriteSheetParams> { |
397 | public: |
398 | ImportSpriteSheetCommand(); |
399 | |
400 | protected: |
401 | virtual void onExecute(Context* context) override; |
402 | }; |
403 | |
404 | ImportSpriteSheetCommand::ImportSpriteSheetCommand() |
405 | : CommandWithNewParams(CommandId::ImportSpriteSheet(), CmdRecordableFlag) |
406 | { |
407 | } |
408 | |
409 | void 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 | |
561 | Command* CommandFactory::createImportSpriteSheetCommand() |
562 | { |
563 | return new ImportSpriteSheetCommand; |
564 | } |
565 | |
566 | } // namespace app |
567 | |