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/cmd_export_sprite_sheet.h"
14#include "app/context.h"
15#include "app/context_access.h"
16#include "app/doc.h"
17#include "app/doc_exporter.h"
18#include "app/file/file.h"
19#include "app/file_selector.h"
20#include "app/filename_formatter.h"
21#include "app/i18n/strings.h"
22#include "app/job.h"
23#include "app/modules/editors.h"
24#include "app/modules/gui.h"
25#include "app/pref/preferences.h"
26#include "app/recent_files.h"
27#include "app/restore_visible_layers.h"
28#include "app/task.h"
29#include "app/ui/editor/editor.h"
30#include "app/ui/editor/navigate_state.h"
31#include "app/ui/layer_frame_comboboxes.h"
32#include "app/ui/optional_alert.h"
33#include "app/ui/status_bar.h"
34#include "app/ui/timeline/timeline.h"
35#include "base/convert_to.h"
36#include "base/fs.h"
37#include "base/string.h"
38#include "base/thread.h"
39#include "doc/layer.h"
40#include "doc/layer_tilemap.h"
41#include "doc/tag.h"
42#include "doc/tileset.h"
43#include "doc/tilesets.h"
44#include "fmt/format.h"
45#include "ui/message.h"
46#include "ui/system.h"
47
48#include "export_sprite_sheet.xml.h"
49
50#include <limits>
51#include <sstream>
52
53namespace app {
54
55using namespace ui;
56
57namespace {
58
59#ifdef ENABLE_UI
60
61enum Section {
62 kSectionLayout,
63 kSectionSprite,
64 kSectionBorders,
65 kSectionOutput,
66};
67
68enum Source {
69 kSource_Sprite,
70 kSource_SpriteGrid,
71 kSource_Tilesets,
72};
73
74enum ConstraintType {
75 kConstraintType_None,
76 kConstraintType_Cols,
77 kConstraintType_Rows,
78 kConstraintType_Width,
79 kConstraintType_Height,
80 kConstraintType_Size,
81};
82
83// Special key value used in default preferences to know if by default
84// the user wants to generate texture and/or files.
85static const char* kSpecifiedFilename = "**filename**";
86
87bool ask_overwrite(const bool askFilename, const std::string& filename,
88 const bool askDataname, const std::string& dataname)
89{
90 if ((askFilename &&
91 !filename.empty() &&
92 base::is_file(filename)) ||
93 (askDataname &&
94 !dataname.empty() &&
95 base::is_file(dataname))) {
96 std::stringstream text;
97
98 if (base::is_file(filename))
99 text << "<<" << base::get_file_name(filename).c_str();
100
101 if (base::is_file(dataname))
102 text << "<<" << base::get_file_name(dataname).c_str();
103
104 const int ret =
105 OptionalAlert::show(
106 Preferences::instance().spriteSheet.showOverwriteFilesAlert,
107 1, // Yes is the default option when the alert dialog is disabled
108 fmt::format(Strings::alerts_overwrite_files_on_export_sprite_sheet(),
109 text.str()));
110 if (ret != 1)
111 return false;
112 }
113 return true;
114}
115
116ConstraintType constraint_type_from_params(const ExportSpriteSheetParams& params)
117{
118 switch (params.type()) {
119 case app::SpriteSheetType::Rows:
120 if (params.width() > 0)
121 return kConstraintType_Width;
122 else if (params.columns() > 0)
123 return kConstraintType_Cols;
124 break;
125 case app::SpriteSheetType::Columns:
126 if (params.height() > 0)
127 return kConstraintType_Height;
128 else if (params.rows() > 0)
129 return kConstraintType_Rows;
130 break;
131 case app::SpriteSheetType::Packed:
132 if (params.width() > 0 && params.height() > 0)
133 return kConstraintType_Size;
134 else if (params.width() > 0)
135 return kConstraintType_Width;
136 else if (params.height() > 0)
137 return kConstraintType_Height;
138 break;
139 }
140 return kConstraintType_None;
141}
142
143#endif // ENABLE_UI
144
145Doc* generate_sprite_sheet_from_params(
146 DocExporter& exporter,
147 Context* ctx,
148 const Site& site,
149 const ExportSpriteSheetParams& params,
150 const bool saveData,
151 base::task_token& token)
152{
153 const app::SpriteSheetType type = params.type();
154 const int columns = params.columns();
155 const int rows = params.rows();
156 const int width = params.width();
157 const int height = params.height();
158 const std::string filename = params.textureFilename();
159 const std::string dataFilename = params.dataFilename();
160 const SpriteSheetDataFormat dataFormat = params.dataFormat();
161 const std::string filenameFormat = params.filenameFormat();
162 const std::string layerName = params.layer();
163 const int layerIndex = params.layerIndex();
164 const std::string tagName = params.tag();
165 const int borderPadding = std::clamp(params.borderPadding(), 0, 100);
166 const int shapePadding = std::clamp(params.shapePadding(), 0, 100);
167 const int innerPadding = std::clamp(params.innerPadding(), 0, 100);
168 const bool trimSprite = params.trimSprite();
169 const bool trimCels = params.trim();
170 const bool trimByGrid = params.trimByGrid();
171 const bool extrude = params.extrude();
172 const bool ignoreEmpty = params.ignoreEmpty();
173 const bool mergeDuplicates = params.mergeDuplicates();
174 const bool splitLayers = params.splitLayers();
175 const bool splitTags = params.splitTags();
176 const bool splitGrid = params.splitGrid();
177 const bool listLayers = params.listLayers();
178 const bool listTags = params.listTags();
179 const bool listSlices = params.listSlices();
180 const bool fromTilesets = params.fromTilesets();
181
182 SelectedFrames selFrames;
183 Tag* tag = calculate_selected_frames(site, tagName, selFrames);
184
185#ifdef _DEBUG
186 frame_t nframes = selFrames.size();
187 ASSERT(nframes > 0);
188#endif
189
190 Doc* doc = const_cast<Doc*>(site.document());
191 const Sprite* sprite = site.sprite();
192
193 // If the user choose to render selected layers only, we can
194 // temporaly make them visible and hide the other ones.
195 RestoreVisibleLayers layersVisibility;
196 calculate_visible_layers(site, layerName, layerIndex, layersVisibility);
197
198 SelectedLayers selLayers;
199 if (layerName != kSelectedLayers) {
200 // TODO add a getLayerByName
201 int i = sprite->allLayersCount();
202 for (const Layer* layer : sprite->allLayers()) {
203 i--;
204 if (layer->name() == layerName && (layerIndex == -1 ||
205 layerIndex == i)) {
206 selLayers.insert(const_cast<Layer*>(layer));
207 break;
208 }
209 }
210 }
211
212 exporter.reset();
213
214 // Use each tileset from tilemap layers as a sprite
215 if (fromTilesets) {
216 exporter.addTilesetsSamples(
217 doc,
218 !selLayers.empty() ? &selLayers: nullptr);
219 }
220 // Use the whole canvas as a sprite
221 else {
222 exporter.addDocumentSamples(
223 doc, tag, splitLayers, splitTags, splitGrid,
224 !selLayers.empty() ? &selLayers: nullptr,
225 !selFrames.empty() ? &selFrames: nullptr);
226 }
227
228 if (saveData) {
229 if (!filename.empty())
230 exporter.setTextureFilename(filename);
231 if (!dataFilename.empty()) {
232 exporter.setDataFilename(dataFilename);
233 exporter.setDataFormat(dataFormat);
234 }
235 }
236 if (!filenameFormat.empty())
237 exporter.setFilenameFormat(filenameFormat);
238
239 exporter.setTextureWidth(width);
240 exporter.setTextureHeight(height);
241 exporter.setTextureColumns(columns);
242 exporter.setTextureRows(rows);
243 exporter.setSpriteSheetType(type);
244 exporter.setBorderPadding(borderPadding);
245 exporter.setShapePadding(shapePadding);
246 exporter.setInnerPadding(innerPadding);
247 exporter.setTrimSprite(trimSprite);
248 exporter.setTrimCels(trimCels);
249 exporter.setTrimByGrid(trimByGrid);
250 exporter.setExtrude(extrude);
251 exporter.setSplitLayers(splitLayers);
252 exporter.setSplitTags(splitTags);
253 exporter.setIgnoreEmptyCels(ignoreEmpty);
254 exporter.setMergeDuplicates(mergeDuplicates);
255 if (listLayers) exporter.setListLayers(true);
256 if (listTags) exporter.setListTags(true);
257 if (listSlices) exporter.setListSlices(true);
258
259 // We have to call exportSheet() while RestoreVisibleLayers is still
260 // alive. In this way we can export selected layers correctly if
261 // that option (kSelectedLayers) is selected.
262 return exporter.exportSheet(ctx, token);
263}
264
265std::unique_ptr<Doc> generate_sprite_sheet(
266 DocExporter& exporter,
267 Context* ctx,
268 const Site& site,
269 const ExportSpriteSheetParams& params,
270 bool saveData,
271 base::task_token& token)
272{
273 std::unique_ptr<Doc> newDocument(
274 generate_sprite_sheet_from_params(exporter, ctx, site, params, saveData, token));
275 if (!newDocument)
276 return nullptr;
277
278 // Setup a filename for the new document in case that user didn't
279 // save the file/specified one output filename.
280 if (params.textureFilename().empty()) {
281 std::string fn = site.document()->filename();
282 std::string ext = base::get_file_extension(fn);
283 if (!ext.empty())
284 ext.insert(0, 1, '.');
285
286 newDocument->setFilename(
287 base::join_path(base::get_file_path(fn),
288 base::get_file_title(fn) + "-Sheet") + ext);
289 }
290 return newDocument;
291}
292
293#if ENABLE_UI
294
295class ExportSpriteSheetWindow : public app::gen::ExportSpriteSheet {
296public:
297 ExportSpriteSheetWindow(DocExporter& exporter,
298 Site& site,
299 ExportSpriteSheetParams& params,
300 Preferences& pref)
301 : m_exporter(exporter)
302 , m_frontBuffer(std::make_shared<doc::ImageBuffer>())
303 , m_backBuffer(std::make_shared<doc::ImageBuffer>())
304 , m_site(site)
305 , m_sprite(site.sprite())
306 , m_filenameAskOverwrite(true)
307 , m_dataFilenameAskOverwrite(true)
308 , m_editor(nullptr)
309 , m_genTimer(100, nullptr)
310 , m_executionID(0)
311 , m_filenameFormat(params.filenameFormat())
312 {
313 sectionTabs()->ItemChange.connect([this]{ onChangeSection(); });
314 expandSections()->Click.connect([this]{ onExpandSections(); });
315 closeSpriteSection()->Click.connect([this]{ onCloseSection(kSectionSprite); });
316 closeBordersSection()->Click.connect([this]{ onCloseSection(kSectionBorders); });
317 closeOutputSection()->Click.connect([this]{ onCloseSection(kSectionOutput); });
318
319 static_assert(
320 (int)app::SpriteSheetType::None == 0 &&
321 (int)app::SpriteSheetType::Horizontal == 1 &&
322 (int)app::SpriteSheetType::Vertical == 2 &&
323 (int)app::SpriteSheetType::Rows == 3 &&
324 (int)app::SpriteSheetType::Columns == 4 &&
325 (int)app::SpriteSheetType::Packed == 5,
326 "SpriteSheetType enum changed");
327
328 sheetType()->addItem(Strings::export_sprite_sheet_type_horz());
329 sheetType()->addItem(Strings::export_sprite_sheet_type_vert());
330 sheetType()->addItem(Strings::export_sprite_sheet_type_rows());
331 sheetType()->addItem(Strings::export_sprite_sheet_type_cols());
332 sheetType()->addItem(Strings::export_sprite_sheet_type_pack());
333 {
334 int i;
335 if (params.type() != app::SpriteSheetType::None)
336 i = (int)params.type()-1;
337 else
338 i = ((int)app::SpriteSheetType::Rows)-1;
339 sheetType()->setSelectedItemIndex(i);
340 }
341
342 constraintType()->addItem(Strings::export_sprite_sheet_constraint_fixed_none());
343 constraintType()->addItem(Strings::export_sprite_sheet_constraint_fixed_cols());
344 constraintType()->addItem(Strings::export_sprite_sheet_constraint_fixed_rows());
345 constraintType()->addItem(Strings::export_sprite_sheet_constraint_fixed_width());
346 constraintType()->addItem(Strings::export_sprite_sheet_constraint_fixed_height());
347 constraintType()->addItem(Strings::export_sprite_sheet_constraint_fixed_size());
348
349 auto constraint = constraint_type_from_params(params);
350 constraintType()->setSelectedItemIndex(constraint);
351 switch (constraint) {
352 case kConstraintType_Cols:
353 widthConstraint()->setTextf("%d", params.columns());
354 break;
355 case kConstraintType_Rows:
356 heightConstraint()->setTextf("%d", params.rows());
357 break;
358 case kConstraintType_Width:
359 widthConstraint()->setTextf("%d", params.width());
360 break;
361 case kConstraintType_Height:
362 heightConstraint()->setTextf("%d", params.height());
363 break;
364 case kConstraintType_Size:
365 widthConstraint()->setTextf("%d", params.width());
366 heightConstraint()->setTextf("%d", params.height());
367 break;
368 }
369
370 static_assert(kSource_Sprite == 0 &&
371 kSource_SpriteGrid == 1 &&
372 kSource_Tilesets == 2,
373 "Source enum has changed");
374 source()->addItem(new ListItem("Sprite"));
375 source()->addItem(new ListItem("Sprite Grid"));
376 source()->addItem(new ListItem("Tilesets"));
377 if (params.splitGrid())
378 source()->setSelectedItemIndex(int(kSource_SpriteGrid));
379 else if (params.fromTilesets())
380 source()->setSelectedItemIndex(int(kSource_Tilesets));
381
382 fill_layers_combobox(
383 m_sprite, layers(), params.layer(), params.layerIndex());
384
385 fill_frames_combobox(
386 m_sprite, frames(), params.tag());
387
388 openGenerated()->setSelected(params.openGenerated());
389 trimSpriteEnabled()->setSelected(params.trimSprite());
390 trimEnabled()->setSelected(params.trim());
391 trimContainer()->setVisible(trimSpriteEnabled()->isSelected() ||
392 trimEnabled()->isSelected());
393 gridTrimEnabled()->setSelected((trimSpriteEnabled()->isSelected() ||
394 trimEnabled()->isSelected()) &&
395 params.trimByGrid());
396 extrudeEnabled()->setSelected(params.extrude());
397 mergeDups()->setSelected(params.mergeDuplicates());
398 ignoreEmpty()->setSelected(params.ignoreEmpty());
399
400 borderPadding()->setTextf("%d", params.borderPadding());
401 shapePadding()->setTextf("%d", params.shapePadding());
402 innerPadding()->setTextf("%d", params.innerPadding());
403
404 m_filename = params.textureFilename();
405 imageEnabled()->setSelected(!m_filename.empty());
406 imageFilename()->setVisible(imageEnabled()->isSelected());
407
408 m_dataFilename = params.dataFilename();
409 dataEnabled()->setSelected(!m_dataFilename.empty());
410 dataFormat()->setSelectedItemIndex(int(params.dataFormat()));
411 splitLayers()->setSelected(params.splitLayers());
412 splitTags()->setSelected(params.splitTags());
413 listLayers()->setSelected(params.listLayers());
414 listTags()->setSelected(params.listTags());
415 listSlices()->setSelected(params.listSlices());
416
417 updateDefaultDataFilenameFormat();
418 updateDataFields();
419
420 std::string base = site.document()->filename();
421 base = base::join_path(base::get_file_path(base), base::get_file_title(base));
422
423 if (m_filename.empty() ||
424 m_filename == kSpecifiedFilename) {
425 std::string defExt = pref.spriteSheet.defaultExtension();
426
427 if (base::utf8_icmp(base::get_file_extension(site.document()->filename()), defExt) == 0)
428 m_filename = base + "-sheet." + defExt;
429 else
430 m_filename = base + "." + defExt;
431 }
432
433 if (m_dataFilename.empty() ||
434 m_dataFilename == kSpecifiedFilename)
435 m_dataFilename = base + ".json";
436
437 exportButton()->Click.connect([this]{ onExport(); });
438 sheetType()->Change.connect([this]{ onSheetTypeChange(); });
439 constraintType()->Change.connect([this]{ onConstraintTypeChange(); });
440 widthConstraint()->Change.connect([this]{ generatePreview(); });
441 heightConstraint()->Change.connect([this]{ generatePreview(); });
442 borderPadding()->Change.connect([this]{ generatePreview(); });
443 shapePadding()->Change.connect([this]{ generatePreview(); });
444 innerPadding()->Change.connect([this]{ generatePreview(); });
445 extrudeEnabled()->Click.connect([this]{ generatePreview(); });
446 mergeDups()->Click.connect([this]{ generatePreview(); });
447 ignoreEmpty()->Click.connect([this]{ generatePreview(); });
448 imageEnabled()->Click.connect([this]{ onImageEnabledChange(); });
449 imageFilename()->Click.connect([this]{ onImageFilename(); });
450 dataEnabled()->Click.connect([this]{ onDataEnabledChange(); });
451 dataFilename()->Click.connect([this]{ onDataFilename(); });
452 trimSpriteEnabled()->Click.connect([this]{ onTrimEnabledChange(); });
453 trimEnabled()->Click.connect([this]{ onTrimEnabledChange(); });
454 gridTrimEnabled()->Click.connect([this]{ generatePreview(); });
455 source()->Change.connect([this]{ generatePreview(); });
456 layers()->Change.connect([this]{ generatePreview(); });
457 splitLayers()->Click.connect([this]{ onSplitLayersOrFrames(); });
458 splitTags()->Click.connect([this]{ onSplitLayersOrFrames(); });
459 frames()->Change.connect([this]{ generatePreview(); });
460 dataFilenameFormat()->Change.connect([this]{ onDataFilenameFormatChange(); });
461 openGenerated()->Click.connect([this]{ onOpenGeneratedChange(); });
462 preview()->Click.connect([this]{ generatePreview(); });
463 m_genTimer.Tick.connect([this]{ onGenTimerTick(); });
464
465 // Select tabs
466 {
467 const std::string s = pref.spriteSheet.sections();
468 const bool layout = (s.find("layout") != std::string::npos);
469 const bool sprite = (s.find("sprite") != std::string::npos);
470 const bool borders = (s.find("borders") != std::string::npos);
471 const bool output = (s.find("output") != std::string::npos);
472 sectionTabs()->getItem(kSectionLayout)->setSelected(layout || (!sprite & !borders && !output));
473 sectionTabs()->getItem(kSectionSprite)->setSelected(sprite);
474 sectionTabs()->getItem(kSectionBorders)->setSelected(borders);
475 sectionTabs()->getItem(kSectionOutput)->setSelected(output);
476 }
477
478 onChangeSection();
479 onSheetTypeChange();
480 onFileNamesChange();
481 updateExportButton();
482
483 preview()->setSelected(pref.spriteSheet.preview());
484 generatePreview();
485
486 remapWindow();
487 centerWindow();
488 load_window_pos(this, "ExportSpriteSheet");
489 }
490
491 ~ExportSpriteSheetWindow() {
492 cancelGenTask();
493 if (m_spriteSheet) {
494 auto ctx = UIContext::instance();
495 ctx->setActiveDocument(m_site.document());
496
497 DocDestroyer destroyer(ctx, m_spriteSheet.release(), 100);
498 destroyer.destroyDocument();
499 }
500 }
501
502 std::string selectedSectionsString() const {
503 const bool layout = sectionTabs()->getItem(kSectionLayout)->isSelected();
504 const bool sprite = sectionTabs()->getItem(kSectionSprite)->isSelected();
505 const bool borders = sectionTabs()->getItem(kSectionBorders)->isSelected();
506 const bool output = sectionTabs()->getItem(kSectionOutput)->isSelected();
507 return
508 fmt::format("{} {} {} {}",
509 (layout ? "layout": ""),
510 (sprite ? "sprite": ""),
511 (borders ? "borders": ""),
512 (output ? "output": ""));
513 }
514
515 bool ok() const {
516 return closer() == exportButton();
517 }
518
519 void updateParams(ExportSpriteSheetParams& params) {
520 params.type (spriteSheetTypeValue());
521 params.columns (columnsValue());
522 params.rows (rowsValue());
523 params.width (widthValue());
524 params.height (heightValue());
525 params.textureFilename (filenameValue());
526 params.dataFilename (dataFilenameValue());
527 params.dataFormat (dataFormatValue());
528 params.filenameFormat (filenameFormatValue());
529 params.borderPadding (borderPaddingValue());
530 params.shapePadding (shapePaddingValue());
531 params.innerPadding (innerPaddingValue());
532 params.trimSprite (trimSpriteValue());
533 params.trim (trimValue());
534 params.trimByGrid (trimByGridValue());
535 params.extrude (extrudeValue());
536 params.mergeDuplicates (mergeDupsValue());
537 params.ignoreEmpty (ignoreEmptyValue());
538 params.openGenerated (openGeneratedValue());
539 params.layer (layerValue());
540 params.layerIndex (layerIndex());
541 params.tag (tagValue());
542 params.splitLayers (splitLayersValue());
543 params.splitTags (splitTagsValue());
544 params.listLayers (listLayersValue());
545 params.listTags (listTagsValue());
546 params.listSlices (listSlicesValue());
547 params.splitGrid (source()->getSelectedItemIndex() == int(kSource_SpriteGrid));
548 params.fromTilesets (source()->getSelectedItemIndex() == int(kSource_Tilesets));
549 }
550
551private:
552
553 bool onProcessMessage(ui::Message* msg) override {
554 switch (msg->type()) {
555 case kCloseMessage:
556 save_window_pos(this, "ExportSpriteSheet");
557 break;
558 }
559 return Window::onProcessMessage(msg);
560 }
561
562 void onBroadcastMouseMessage(const gfx::Point& screenPos,
563 WidgetsList& targets) override {
564 Window::onBroadcastMouseMessage(screenPos, targets);
565
566 // Add the editor as receptor of mouse events too.
567 if (m_editor)
568 targets.push_back(View::getView(m_editor));
569 }
570
571 void onChangeSection() {
572 panel()->showAllChildren();
573
574 const bool layout = sectionTabs()->getItem(kSectionLayout)->isSelected();
575 const bool sprite = sectionTabs()->getItem(kSectionSprite)->isSelected();
576 const bool borders = sectionTabs()->getItem(kSectionBorders)->isSelected();
577 const bool output = sectionTabs()->getItem(kSectionOutput)->isSelected();
578
579 sectionLayout()->setVisible(layout);
580 sectionSpriteSeparator()->setVisible(sprite && layout);
581 sectionSprite()->setVisible(sprite);
582 sectionBordersSeparator()->setVisible(borders && (layout || sprite));
583 sectionBorders()->setVisible(borders);
584 sectionOutputSeparator()->setVisible(output && (layout || sprite || borders));
585 sectionOutput()->setVisible(output);
586
587 resize();
588 }
589
590 void onExpandSections() {
591 sectionTabs()->getItem(kSectionLayout)->setSelected(true);
592 sectionTabs()->getItem(kSectionSprite)->setSelected(true);
593 sectionTabs()->getItem(kSectionBorders)->setSelected(true);
594 sectionTabs()->getItem(kSectionOutput)->setSelected(true);
595 onChangeSection();
596 }
597
598 void onCloseSection(const Section section) {
599 if (sectionTabs()->countSelectedItems() > 1)
600 sectionTabs()->getItem(section)->setSelected(false);
601 onChangeSection();
602 }
603
604 app::SpriteSheetType spriteSheetTypeValue() const {
605 return (app::SpriteSheetType)(sheetType()->getSelectedItemIndex()+1);
606 }
607
608 int columnsValue() const {
609 if (spriteSheetTypeValue() == app::SpriteSheetType::Rows &&
610 constraintType()->getSelectedItemIndex() == (int)kConstraintType_Cols) {
611 return widthConstraint()->textInt();
612 }
613 else
614 return 0;
615 }
616
617 int rowsValue() const {
618 if (spriteSheetTypeValue() == app::SpriteSheetType::Columns &&
619 constraintType()->getSelectedItemIndex() == (int)kConstraintType_Rows) {
620 return heightConstraint()->textInt();
621 }
622 else
623 return 0;
624 }
625
626 int widthValue() const {
627 if ((spriteSheetTypeValue() == app::SpriteSheetType::Rows ||
628 spriteSheetTypeValue() == app::SpriteSheetType::Packed) &&
629 (constraintType()->getSelectedItemIndex() == (int)kConstraintType_Width ||
630 constraintType()->getSelectedItemIndex() == (int)kConstraintType_Size)) {
631 return widthConstraint()->textInt();
632 }
633 else
634 return 0;
635 }
636
637 int heightValue() const {
638 if ((spriteSheetTypeValue() == app::SpriteSheetType::Columns ||
639 spriteSheetTypeValue() == app::SpriteSheetType::Packed) &&
640 (constraintType()->getSelectedItemIndex() == (int)kConstraintType_Height ||
641 constraintType()->getSelectedItemIndex() == (int)kConstraintType_Size)) {
642 return heightConstraint()->textInt();
643 }
644 else
645 return 0;
646 }
647
648 std::string filenameValue() const {
649 if (imageEnabled()->isSelected())
650 return m_filename;
651 else
652 return std::string();
653 }
654
655 std::string dataFilenameValue() const {
656 if (dataEnabled()->isSelected())
657 return m_dataFilename;
658 else
659 return std::string();
660 }
661
662 std::string filenameFormatValue() const {
663 if (!m_filenameFormat.empty() &&
664 m_filenameFormat != m_filenameFormatDefault)
665 return m_filenameFormat;
666 else
667 return std::string();
668 }
669
670 SpriteSheetDataFormat dataFormatValue() const {
671 if (dataEnabled()->isSelected())
672 return SpriteSheetDataFormat(dataFormat()->getSelectedItemIndex());
673 else
674 return SpriteSheetDataFormat::Default;
675 }
676
677 int borderPaddingValue() const {
678 int value = borderPadding()->textInt();
679 return std::clamp(value, 0, 100);
680 }
681
682 int shapePaddingValue() const {
683 int value = shapePadding()->textInt();
684 return std::clamp(value, 0, 100);
685 }
686
687 int innerPaddingValue() const {
688 int value = innerPadding()->textInt();
689 return std::clamp(value, 0, 100);
690 }
691
692 bool trimSpriteValue() const {
693 return trimSpriteEnabled()->isSelected();
694 }
695
696 bool trimValue() const {
697 return trimEnabled()->isSelected();
698 }
699
700 bool trimByGridValue() const {
701 return gridTrimEnabled()->isSelected();
702 }
703
704 bool extrudeValue() const {
705 return extrudeEnabled()->isSelected();
706 }
707
708 bool extrudePadding() const {
709 return (extrudeValue() ? 1: 0);
710 }
711
712 bool mergeDupsValue() const {
713 return mergeDups()->isSelected();
714 }
715
716 bool ignoreEmptyValue() const {
717 return ignoreEmpty()->isSelected();
718 }
719
720 bool openGeneratedValue() const {
721 return openGenerated()->isSelected();
722 }
723
724 std::string layerValue() const {
725 return layers()->getValue();
726 }
727
728 int layerIndex() const {
729 int i = layers()->getSelectedItemIndex() - kLayersComboboxExtraInitialItems;
730 return i < 0 ? -1 : i;
731 }
732
733 std::string tagValue() const {
734 return frames()->getValue();
735 }
736
737 bool splitLayersValue() const {
738 return splitLayers()->isSelected();
739 }
740
741 bool splitTagsValue() const {
742 return splitTags()->isSelected();
743 }
744
745 bool splitGridValue() const {
746 return (source()->getSelectedItemIndex() == int(kSource_SpriteGrid));
747 }
748
749 bool listLayersValue() const {
750 return listLayers()->isSelected();
751 }
752
753 bool listTagsValue() const {
754 return listTags()->isSelected();
755 }
756
757 bool listSlicesValue() const {
758 return listSlices()->isSelected();
759 }
760
761 void onExport() {
762 if (!ask_overwrite(m_filenameAskOverwrite, filenameValue(),
763 m_dataFilenameAskOverwrite, dataFilenameValue()))
764 return;
765
766 closeWindow(exportButton());
767 }
768
769 void onSheetTypeChange() {
770 for (int i=1; i<constraintType()->getItemCount(); ++i)
771 constraintType()->getItem(i)->setVisible(false);
772
773 mergeDups()->setEnabled(true);
774
775 const ConstraintType selectConstraint =
776 (ConstraintType)constraintType()->getSelectedItemIndex();
777 switch (spriteSheetTypeValue()) {
778 case app::SpriteSheetType::Horizontal:
779 case app::SpriteSheetType::Vertical:
780 constraintType()->setSelectedItemIndex(kConstraintType_None);
781 break;
782 case app::SpriteSheetType::Rows:
783 constraintType()->getItem(kConstraintType_Cols)->setVisible(true);
784 constraintType()->getItem(kConstraintType_Width)->setVisible(true);
785 if (selectConstraint != kConstraintType_None &&
786 selectConstraint != kConstraintType_Cols &&
787 selectConstraint != kConstraintType_Width)
788 constraintType()->setSelectedItemIndex(kConstraintType_None);
789 break;
790 case app::SpriteSheetType::Columns:
791 constraintType()->getItem(kConstraintType_Rows)->setVisible(true);
792 constraintType()->getItem(kConstraintType_Height)->setVisible(true);
793 if (selectConstraint != kConstraintType_None &&
794 selectConstraint != kConstraintType_Rows &&
795 selectConstraint != kConstraintType_Height)
796 constraintType()->setSelectedItemIndex(kConstraintType_None);
797 break;
798 case app::SpriteSheetType::Packed:
799 constraintType()->getItem(kConstraintType_Width)->setVisible(true);
800 constraintType()->getItem(kConstraintType_Height)->setVisible(true);
801 constraintType()->getItem(kConstraintType_Size)->setVisible(true);
802 if (selectConstraint != kConstraintType_None &&
803 selectConstraint != kConstraintType_Width &&
804 selectConstraint != kConstraintType_Height &&
805 selectConstraint != kConstraintType_Size) {
806 constraintType()->setSelectedItemIndex(kConstraintType_None);
807 }
808 mergeDups()->setSelected(true);
809 mergeDups()->setEnabled(false);
810 break;
811 }
812 onConstraintTypeChange();
813 }
814
815 void onConstraintTypeChange() {
816 bool withWidth = false;
817 bool withHeight = false;
818 switch ((ConstraintType)constraintType()->getSelectedItemIndex()) {
819 case kConstraintType_Cols:
820 withWidth = true;
821 widthConstraint()->setSuffix("");
822 break;
823 case kConstraintType_Rows:
824 withHeight = true;
825 heightConstraint()->setSuffix("");
826 break;
827 case kConstraintType_Width:
828 withWidth = true;
829 widthConstraint()->setSuffix("px");
830 break;
831 case kConstraintType_Height:
832 withHeight = true;
833 heightConstraint()->setSuffix("px");
834 break;
835 case kConstraintType_Size:
836 withWidth = true;
837 withHeight = true;
838 widthConstraint()->setSuffix("px");
839 heightConstraint()->setSuffix("px");
840 break;
841 }
842 widthConstraint()->setVisible(withWidth);
843 heightConstraint()->setVisible(withHeight);
844 resize();
845 generatePreview();
846 }
847
848 void onFileNamesChange() {
849 imageFilename()->setText(base::get_file_name(m_filename));
850 dataFilename()->setText(base::get_file_name(m_dataFilename));
851 resize();
852 }
853
854 void onImageFilename() {
855 base::paths newFilename;
856 if (!app::show_file_selector(
857 Strings::export_sprite_sheet_save_title(), m_filename,
858 get_writable_extensions(),
859 FileSelectorType::Save, newFilename))
860 return;
861
862 ASSERT(!newFilename.empty());
863
864 m_filename = newFilename.front();
865 m_filenameAskOverwrite = false; // Already asked in file selector
866 onFileNamesChange();
867 }
868
869 void onImageEnabledChange() {
870 m_filenameAskOverwrite = true;
871
872 imageFilename()->setVisible(imageEnabled()->isSelected());
873 updateExportButton();
874 resize();
875 }
876
877 void onDataFilename() {
878 // TODO hardcoded "json" extension
879 base::paths exts = { "json" };
880 base::paths newFilename;
881 if (!app::show_file_selector(
882 Strings::export_sprite_sheet_save_json_title(),
883 m_dataFilename,
884 exts,
885 FileSelectorType::Save,
886 newFilename))
887 return;
888
889 ASSERT(!newFilename.empty());
890
891 m_dataFilename = newFilename.front();
892 m_dataFilenameAskOverwrite = false; // Already asked in file selector
893 onFileNamesChange();
894 }
895
896 void onDataEnabledChange() {
897 m_dataFilenameAskOverwrite = true;
898
899 updateDataFields();
900 updateExportButton();
901 resize();
902 }
903
904 void onTrimEnabledChange() {
905 trimContainer()->setVisible(
906 trimSpriteEnabled()->isSelected() ||
907 trimEnabled()->isSelected());
908 resize();
909 generatePreview();
910 }
911
912 void onSplitLayersOrFrames() {
913 updateDefaultDataFilenameFormat();
914 generatePreview();
915 }
916
917 void onDataFilenameFormatChange() {
918 m_filenameFormat = dataFilenameFormat()->text();
919 if (m_filenameFormat.empty())
920 updateDefaultDataFilenameFormat();
921 }
922
923 void onOpenGeneratedChange() {
924 updateExportButton();
925 }
926
927 void resize() {
928 expandWindow(sizeHint());
929 }
930
931 void updateExportButton() {
932 exportButton()->setEnabled(
933 imageEnabled()->isSelected() ||
934 dataEnabled()->isSelected() ||
935 openGenerated()->isSelected());
936 }
937
938 void updateDefaultDataFilenameFormat() {
939 m_filenameFormatDefault =
940 get_default_filename_format_for_sheet(
941 m_site.document()->filename(),
942 m_site.document()->sprite()->totalFrames() > 0,
943 splitLayersValue(),
944 splitTagsValue());
945
946 if (m_filenameFormat.empty()) {
947 dataFilenameFormat()->setText(m_filenameFormatDefault);
948 }
949 else {
950 dataFilenameFormat()->setText(m_filenameFormat);
951 }
952 }
953
954 void updateDataFields() {
955 bool state = dataEnabled()->isSelected();
956 dataFilename()->setVisible(state);
957 dataMeta()->setVisible(state);
958 dataFilenameFormatPlaceholder()->setVisible(state);
959 }
960
961 void onGenTimerTick() {
962 if (!m_genTask) {
963 m_genTimer.stop();
964 setText(Strings::export_sprite_sheet_title());
965 return;
966 }
967 setText(
968 fmt::format(
969 "{} ({} {}%)",
970 Strings::export_sprite_sheet_title(),
971 Strings::export_sprite_sheet_preview(),
972 int(100.0f * m_genTask->progress())));
973 }
974
975 void generatePreview() {
976 cancelGenTask();
977
978 if (!preview()->isSelected()) {
979 if (m_spriteSheet) {
980 auto ctx = UIContext::instance();
981 ctx->setActiveDocument(m_site.document());
982
983 DocDestroyer destroyer(ctx, m_spriteSheet.release(), 100);
984 destroyer.destroyDocument();
985 m_editor = nullptr;
986 }
987 return;
988 }
989
990 ASSERT(m_genTask == nullptr);
991
992 ExportSpriteSheetParams params;
993 updateParams(params);
994
995 std::unique_ptr<Task> task(new Task);
996 task->run(
997 [this, params](base::task_token& token){
998 generateSpriteSheetOnBackground(params, token);
999 });
1000 m_genTask = std::move(task);
1001 m_genTimer.start();
1002 onGenTimerTick();
1003 }
1004
1005 void generateSpriteSheetOnBackground(const ExportSpriteSheetParams& params,
1006 base::task_token& token) {
1007 // Sometimes (more often on Linux) the back buffer is still being
1008 // used by the new document after
1009 // generateSpriteSheetOnBackground() and before
1010 // openGeneratedSpriteSheet(). In this case the use counter is 3
1011 // which means that 2 or more openGeneratedSpriteSheet() are
1012 // queued in the laf-os events queue. In this case we just create
1013 // a new back buffer and the old one will be discarded by
1014 // openGeneratedSpriteSheet() when m_executionID != executionID.
1015 if (m_backBuffer.use_count() > 2) {
1016 auto ptr = std::make_shared<doc::ImageBuffer>();
1017 m_backBuffer.swap(ptr);
1018 }
1019 m_exporter.setDocImageBuffer(m_backBuffer);
1020
1021 ASSERT(m_backBuffer.use_count() == 2);
1022
1023 // Create a non-UI context to avoid showing UI dialogs for
1024 // GifOptions or JpegOptions from the background thread.
1025 Context tmpCtx;
1026
1027 Doc* newDocument =
1028 generate_sprite_sheet(
1029 m_exporter, &tmpCtx, m_site, params, false, token)
1030 .release();
1031 if (!newDocument)
1032 return;
1033
1034 if (token.canceled()) {
1035 DocDestroyer destroyer(&tmpCtx, newDocument, 100);
1036 destroyer.destroyDocument();
1037 return;
1038 }
1039
1040 ++m_executionID;
1041 int executionID = m_executionID;
1042
1043 tmpCtx.documents().remove(newDocument);
1044
1045 ui::execute_from_ui_thread(
1046 [this, newDocument, executionID]{
1047 openGeneratedSpriteSheet(newDocument, executionID);
1048 });
1049 }
1050
1051 void openGeneratedSpriteSheet(Doc* newDocument, int executionID) {
1052 auto context = UIContext::instance();
1053
1054 if (!isVisible() ||
1055 // Other openGeneratedSpriteSheet() is queued and we are the
1056 // old one. IN this case the newDocument contains a back
1057 // buffer (ImageBufferPtr) that will be discarded.
1058 m_executionID != executionID) {
1059 DocDestroyer destroyer(context, newDocument, 100);
1060 destroyer.destroyDocument();
1061 return;
1062 }
1063
1064 // Was the preview unselected when we were generating the preview?
1065 if (!preview()->isSelected())
1066 return;
1067
1068 // Now the "m_frontBuffer" is the current "m_backBuffer" which was
1069 // used by the generator to create the "newDocument", in the next
1070 // iteration we'll use the "m_backBuffer" to re-generate the
1071 // sprite sheet (while the document being displayed in the Editor
1072 // will use the m_frontBuffer).
1073 m_frontBuffer.swap(m_backBuffer);
1074
1075 if (!m_spriteSheet) {
1076 m_spriteSheet.reset(newDocument);
1077 m_spriteSheet->setInhibitBackup(true);
1078 m_spriteSheet->setContext(context);
1079
1080 m_editor = context->getEditorFor(m_spriteSheet.get());
1081 if (m_editor) {
1082 m_editor->setState(EditorStatePtr(new NavigateState));
1083 m_editor->setDefaultScroll();
1084 }
1085 }
1086 else {
1087 // Replace old cel with the new one
1088 auto spriteSheetLay = static_cast<LayerImage*>(m_spriteSheet->sprite()->root()->firstLayer());
1089 auto newDocLay = static_cast<LayerImage*>(newDocument->sprite()->root()->firstLayer());
1090 Cel* oldCel = m_spriteSheet->sprite()->firstLayer()->cel(0);
1091 Cel* newCel = newDocument->sprite()->firstLayer()->cel(0);
1092
1093 spriteSheetLay->removeCel(oldCel);
1094 delete oldCel;
1095
1096 newDocLay->removeCel(newCel);
1097 spriteSheetLay->addCel(newCel);
1098
1099 // Update sprite sheet size
1100 m_spriteSheet->sprite()->setSize(
1101 newDocument->sprite()->width(),
1102 newDocument->sprite()->height());
1103
1104 m_spriteSheet->notifyGeneralUpdate();
1105
1106 DocDestroyer destroyer(context, newDocument, 100);
1107 destroyer.destroyDocument();
1108 }
1109
1110 waitGenTaskAndDelete();
1111 }
1112
1113 void cancelGenTask() {
1114 if (m_genTask) {
1115 m_genTask->cancel();
1116 waitGenTaskAndDelete();
1117 }
1118 }
1119
1120 void waitGenTaskAndDelete() {
1121 if (m_genTask) {
1122 if (!m_genTask->completed()) {
1123 while (!m_genTask->completed())
1124 base::this_thread::sleep_for(0.01);
1125 }
1126 m_genTask.reset();
1127 }
1128 }
1129
1130 DocExporter& m_exporter;
1131 doc::ImageBufferPtr m_frontBuffer; // ImageBuffer in the preview ImageBuffer
1132 doc::ImageBufferPtr m_backBuffer; // ImageBuffer in the generator
1133 Site& m_site;
1134 Sprite* m_sprite;
1135 std::string m_filename;
1136 std::string m_dataFilename;
1137 bool m_filenameAskOverwrite;
1138 bool m_dataFilenameAskOverwrite;
1139 std::unique_ptr<Doc> m_spriteSheet;
1140 Editor* m_editor;
1141 std::unique_ptr<Task> m_genTask;
1142 ui::Timer m_genTimer;
1143 int m_executionID;
1144 std::string m_filenameFormat;
1145 std::string m_filenameFormatDefault;
1146};
1147
1148class ExportSpriteSheetJob : public Job {
1149public:
1150 ExportSpriteSheetJob(
1151 DocExporter& exporter,
1152 const Site& site,
1153 const ExportSpriteSheetParams& params)
1154 : Job(Strings::export_sprite_sheet_generating().c_str())
1155 , m_exporter(exporter)
1156 , m_site(site)
1157 , m_params(params) { }
1158
1159 std::unique_ptr<Doc> releaseDoc() { return std::move(m_doc); }
1160
1161private:
1162 void onJob() override {
1163 // Create a non-UI context to avoid showing UI dialogs for
1164 // GifOptions or JpegOptions from the background thread.
1165 Context tmpCtx;
1166
1167 m_doc = generate_sprite_sheet(
1168 m_exporter, &tmpCtx, m_site, m_params, true, m_token);
1169
1170 if (m_doc)
1171 tmpCtx.documents().remove(m_doc.get());
1172 }
1173
1174 void onMonitoringTick() override {
1175 Job::onMonitoringTick();
1176 if (isCanceled())
1177 m_token.cancel();
1178 else {
1179 jobProgress(m_token.progress());
1180 }
1181 }
1182
1183 DocExporter& m_exporter;
1184 base::task_token m_token;
1185 const Site& m_site;
1186 const ExportSpriteSheetParams& m_params;
1187 std::unique_ptr<Doc> m_doc;
1188};
1189
1190#endif // ENABLE_UI
1191
1192} // anonymous namespace
1193
1194ExportSpriteSheetCommand::ExportSpriteSheetCommand(const char* id)
1195 : CommandWithNewParams(id, CmdRecordableFlag)
1196{
1197}
1198
1199bool ExportSpriteSheetCommand::onEnabled(Context* context)
1200{
1201 return context->checkFlags(ContextFlags::ActiveDocumentIsWritable);
1202}
1203
1204void ExportSpriteSheetCommand::onExecute(Context* context)
1205{
1206 Site site = context->activeSite();
1207 auto& params = this->params();
1208 DocExporter exporter;
1209
1210#ifdef ENABLE_UI
1211 // TODO if we use this line when !ENABLE_UI,
1212 // Preferences::~Preferences() crashes on Linux when it wants to
1213 // save the document preferences. It looks like
1214 // Preferences::onRemoveDocument() is not called for some documents
1215 // and when the Preferences::m_docs collection is iterated to save
1216 // all DocumentPreferences, it accesses an invalid Doc* pointer (an
1217 // already removed/deleted document).
1218 Doc* document = site.document();
1219 DocumentPreferences& docPref(Preferences::instance().document(document));
1220
1221 // Show UI if the user specified it explicitly (params.ui=true) or
1222 // the sprite sheet type wasn't specified.
1223 const bool showUI = (context->isUIAvailable() && params.ui() &&
1224 (params.ui.isSet() || !params.type.isSet()));
1225
1226 // Copy document preferences to undefined params
1227 {
1228 auto& defPref = (docPref.spriteSheet.defined() ? docPref: Preferences::instance().document(nullptr));
1229 if (!params.type.isSet()) {
1230 params.type(defPref.spriteSheet.type());
1231 if (!params.columns.isSet()) params.columns( defPref.spriteSheet.columns());
1232 if (!params.rows.isSet()) params.rows( defPref.spriteSheet.rows());
1233 if (!params.width.isSet()) params.width( defPref.spriteSheet.width());
1234 if (!params.height.isSet()) params.height( defPref.spriteSheet.height());
1235 if (!params.textureFilename.isSet()) params.textureFilename( defPref.spriteSheet.textureFilename());
1236 if (!params.dataFilename.isSet()) params.dataFilename( defPref.spriteSheet.dataFilename());
1237 if (!params.dataFormat.isSet()) params.dataFormat( defPref.spriteSheet.dataFormat());
1238 if (!params.filenameFormat.isSet()) params.filenameFormat( defPref.spriteSheet.filenameFormat());
1239 if (!params.borderPadding.isSet()) params.borderPadding( defPref.spriteSheet.borderPadding());
1240 if (!params.shapePadding.isSet()) params.shapePadding( defPref.spriteSheet.shapePadding());
1241 if (!params.innerPadding.isSet()) params.innerPadding( defPref.spriteSheet.innerPadding());
1242 if (!params.trimSprite.isSet()) params.trimSprite( defPref.spriteSheet.trimSprite());
1243 if (!params.trim.isSet()) params.trim( defPref.spriteSheet.trim());
1244 if (!params.trimByGrid.isSet()) params.trimByGrid( defPref.spriteSheet.trimByGrid());
1245 if (!params.extrude.isSet()) params.extrude( defPref.spriteSheet.extrude());
1246 if (!params.mergeDuplicates.isSet()) params.mergeDuplicates( defPref.spriteSheet.mergeDuplicates());
1247 if (!params.ignoreEmpty.isSet()) params.ignoreEmpty( defPref.spriteSheet.ignoreEmpty());
1248 if (!params.openGenerated.isSet()) params.openGenerated( defPref.spriteSheet.openGenerated());
1249 if (!params.layer.isSet()) params.layer( defPref.spriteSheet.layer());
1250 if (!params.layerIndex.isSet()) params.layerIndex( defPref.spriteSheet.layerIndex());
1251 if (!params.tag.isSet()) params.tag( defPref.spriteSheet.frameTag());
1252 if (!params.splitLayers.isSet()) params.splitLayers( defPref.spriteSheet.splitLayers());
1253 if (!params.splitTags.isSet()) params.splitTags( defPref.spriteSheet.splitTags());
1254 if (!params.splitGrid.isSet()) params.splitGrid( defPref.spriteSheet.splitGrid());
1255 if (!params.listLayers.isSet()) params.listLayers( defPref.spriteSheet.listLayers());
1256 if (!params.listTags.isSet()) params.listTags( defPref.spriteSheet.listFrameTags());
1257 if (!params.listSlices.isSet()) params.listSlices( defPref.spriteSheet.listSlices());
1258 }
1259 }
1260
1261 bool askOverwrite = params.askOverwrite();
1262 if (showUI) {
1263 auto& pref = Preferences::instance();
1264
1265 ExportSpriteSheetWindow window(exporter, site, params, pref);
1266 window.openWindowInForeground();
1267
1268 // Save global sprite sheet generation settings anyway (even if
1269 // the user cancel the dialog, the global settings are stored).
1270 pref.spriteSheet.preview(window.preview()->isSelected());
1271 pref.spriteSheet.sections(window.selectedSectionsString());
1272
1273 if (!window.ok())
1274 return;
1275
1276 window.updateParams(params);
1277 docPref.spriteSheet.defined(true);
1278 docPref.spriteSheet.type (params.type());
1279 docPref.spriteSheet.columns (params.columns());
1280 docPref.spriteSheet.rows (params.rows());
1281 docPref.spriteSheet.width (params.width());
1282 docPref.spriteSheet.height (params.height());
1283 docPref.spriteSheet.textureFilename (params.textureFilename());
1284 docPref.spriteSheet.dataFilename (params.dataFilename());
1285 docPref.spriteSheet.dataFormat (params.dataFormat());
1286 docPref.spriteSheet.filenameFormat (params.filenameFormat());
1287 docPref.spriteSheet.borderPadding (params.borderPadding());
1288 docPref.spriteSheet.shapePadding (params.shapePadding());
1289 docPref.spriteSheet.innerPadding (params.innerPadding());
1290 docPref.spriteSheet.trimSprite (params.trimSprite());
1291 docPref.spriteSheet.trim (params.trim());
1292 docPref.spriteSheet.trimByGrid (params.trimByGrid());
1293 docPref.spriteSheet.extrude (params.extrude());
1294 docPref.spriteSheet.mergeDuplicates (params.mergeDuplicates());
1295 docPref.spriteSheet.ignoreEmpty (params.ignoreEmpty());
1296 docPref.spriteSheet.openGenerated (params.openGenerated());
1297 docPref.spriteSheet.layer (params.layer());
1298 docPref.spriteSheet.layerIndex (params.layerIndex());
1299 docPref.spriteSheet.frameTag (params.tag());
1300 docPref.spriteSheet.splitLayers (params.splitLayers());
1301 docPref.spriteSheet.splitTags (params.splitTags());
1302 docPref.spriteSheet.splitGrid (params.splitGrid());
1303 docPref.spriteSheet.listLayers (params.listLayers());
1304 docPref.spriteSheet.listFrameTags (params.listTags());
1305 docPref.spriteSheet.listSlices (params.listSlices());
1306
1307 // Default preferences for future sprites
1308 DocumentPreferences& defPref(Preferences::instance().document(nullptr));
1309 defPref.spriteSheet = docPref.spriteSheet;
1310 defPref.spriteSheet.defined(false);
1311 if (!defPref.spriteSheet.textureFilename().empty())
1312 defPref.spriteSheet.textureFilename.setValueAndDefault(kSpecifiedFilename);
1313 if (!defPref.spriteSheet.dataFilename().empty())
1314 defPref.spriteSheet.dataFilename.setValueAndDefault(kSpecifiedFilename);
1315 defPref.save();
1316
1317 askOverwrite = false; // Already asked in the ExportSpriteSheetWindow
1318 }
1319
1320 if (context->isUIAvailable() && askOverwrite) {
1321 if (!ask_overwrite(true, params.textureFilename(),
1322 true, params.dataFilename()))
1323 return; // Do not overwrite
1324 }
1325#endif
1326
1327 exporter.setDocImageBuffer(std::make_shared<doc::ImageBuffer>());
1328 std::unique_ptr<Doc> newDocument;
1329#ifdef ENABLE_UI
1330 if (context->isUIAvailable()) {
1331 ExportSpriteSheetJob job(exporter, site, params);
1332 job.startJob();
1333 job.waitJob();
1334
1335 newDocument = job.releaseDoc();
1336 if (!newDocument)
1337 return;
1338
1339 StatusBar* statusbar = StatusBar::instance();
1340 if (statusbar)
1341 statusbar->showTip(1000, Strings::export_sprite_sheet_generated());
1342
1343 // Save the exported sprite sheet as a recent file
1344 if (newDocument->isAssociatedToFile())
1345 App::instance()->recentFiles()->addRecentFile(newDocument->filename());
1346
1347 // Copy background and grid preferences
1348 DocumentPreferences& newDocPref(
1349 Preferences::instance().document(newDocument.get()));
1350 newDocPref.bg = docPref.bg;
1351 newDocPref.grid = docPref.grid;
1352 newDocPref.pixelGrid = docPref.pixelGrid;
1353 Preferences::instance().removeDocument(newDocument.get());
1354 }
1355 else
1356#endif
1357 {
1358 base::task_token token;
1359 newDocument = generate_sprite_sheet(
1360 exporter, context, site, params, true, token);
1361 if (!newDocument)
1362 return;
1363 }
1364
1365 ASSERT(newDocument);
1366
1367 if (params.openGenerated()) {
1368 newDocument->setContext(context);
1369 newDocument.release();
1370 }
1371 else {
1372 DocDestroyer destroyer(context, newDocument.release(), 100);
1373 destroyer.destroyDocument();
1374 }
1375}
1376
1377Command* CommandFactory::createExportSpriteSheetCommand()
1378{
1379 return new ExportSpriteSheetCommand;
1380}
1381
1382} // namespace app
1383