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 | |
53 | namespace app { |
54 | |
55 | using namespace ui; |
56 | |
57 | namespace { |
58 | |
59 | #ifdef ENABLE_UI |
60 | |
61 | enum Section { |
62 | kSectionLayout, |
63 | kSectionSprite, |
64 | kSectionBorders, |
65 | kSectionOutput, |
66 | }; |
67 | |
68 | enum Source { |
69 | kSource_Sprite, |
70 | kSource_SpriteGrid, |
71 | kSource_Tilesets, |
72 | }; |
73 | |
74 | enum 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. |
85 | static const char* kSpecifiedFilename = "**filename**" ; |
86 | |
87 | bool 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 | |
116 | ConstraintType 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 | |
145 | Doc* 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 | |
265 | std::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 | |
295 | class ExportSpriteSheetWindow : public app::gen::ExportSpriteSheet { |
296 | public: |
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 | |
551 | private: |
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 | |
1148 | class ExportSpriteSheetJob : public Job { |
1149 | public: |
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 | |
1161 | private: |
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 | |
1194 | ExportSpriteSheetCommand::ExportSpriteSheetCommand(const char* id) |
1195 | : CommandWithNewParams(id, CmdRecordableFlag) |
1196 | { |
1197 | } |
1198 | |
1199 | bool ExportSpriteSheetCommand::onEnabled(Context* context) |
1200 | { |
1201 | return context->checkFlags(ContextFlags::ActiveDocumentIsWritable); |
1202 | } |
1203 | |
1204 | void 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 | |
1377 | Command* CommandFactory::createExportSpriteSheetCommand() |
1378 | { |
1379 | return new ExportSpriteSheetCommand; |
1380 | } |
1381 | |
1382 | } // namespace app |
1383 | |