mirror of https://github.com/aseprite/aseprite.git
1398 lines
48 KiB
C++
1398 lines
48 KiB
C++
// Aseprite
|
|
// Copyright (C) 2019-2024 Igara Studio S.A.
|
|
// Copyright (C) 2001-2018 David Capello
|
|
//
|
|
// This program is distributed under the terms of
|
|
// the End-User License Agreement for Aseprite.
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
#include "config.h"
|
|
#endif
|
|
|
|
#include "app/app.h"
|
|
#include "app/commands/cmd_export_sprite_sheet.h"
|
|
#include "app/console.h"
|
|
#include "app/context.h"
|
|
#include "app/context_access.h"
|
|
#include "app/doc.h"
|
|
#include "app/doc_exporter.h"
|
|
#include "app/file/file.h"
|
|
#include "app/file_selector.h"
|
|
#include "app/filename_formatter.h"
|
|
#include "app/i18n/strings.h"
|
|
#include "app/job.h"
|
|
#include "app/modules/gui.h"
|
|
#include "app/pref/preferences.h"
|
|
#include "app/recent_files.h"
|
|
#include "app/restore_visible_layers.h"
|
|
#include "app/task.h"
|
|
#include "app/ui/editor/editor.h"
|
|
#include "app/ui/editor/navigate_state.h"
|
|
#include "app/ui/layer_frame_comboboxes.h"
|
|
#include "app/ui/optional_alert.h"
|
|
#include "app/ui/status_bar.h"
|
|
#include "app/ui/timeline/timeline.h"
|
|
#include "app/util/layer_utils.h"
|
|
#include "base/convert_to.h"
|
|
#include "base/fs.h"
|
|
#include "base/string.h"
|
|
#include "base/thread.h"
|
|
#include "doc/layer.h"
|
|
#include "doc/layer_tilemap.h"
|
|
#include "doc/tag.h"
|
|
#include "doc/tileset.h"
|
|
#include "doc/tilesets.h"
|
|
#include "fmt/format.h"
|
|
#include "ui/message.h"
|
|
#include "ui/system.h"
|
|
|
|
#include "export_sprite_sheet.xml.h"
|
|
|
|
#include <limits>
|
|
#include <string>
|
|
|
|
namespace app {
|
|
|
|
using namespace ui;
|
|
|
|
namespace {
|
|
|
|
enum Section {
|
|
kSectionLayout,
|
|
kSectionSprite,
|
|
kSectionBorders,
|
|
kSectionOutput,
|
|
};
|
|
|
|
enum Source {
|
|
kSource_Sprite,
|
|
kSource_SpriteGrid,
|
|
kSource_Tilesets,
|
|
};
|
|
|
|
enum ConstraintType {
|
|
kConstraintType_None,
|
|
kConstraintType_Cols,
|
|
kConstraintType_Rows,
|
|
kConstraintType_Width,
|
|
kConstraintType_Height,
|
|
kConstraintType_Size,
|
|
};
|
|
|
|
// Special key value used in default preferences to know if by default
|
|
// the user wants to generate texture and/or files.
|
|
static const char* kSpecifiedFilename = "**filename**";
|
|
|
|
bool ask_overwrite(const bool askFilename,
|
|
const std::string& filename,
|
|
const bool askDataname,
|
|
const std::string& dataname)
|
|
{
|
|
if ((askFilename && !filename.empty() && base::is_file(filename)) ||
|
|
(askDataname && !dataname.empty() && base::is_file(dataname))) {
|
|
std::string text;
|
|
|
|
if (base::is_file(filename) && askFilename)
|
|
text += "<<" + base::get_file_name(filename);
|
|
|
|
if (base::is_file(dataname) && askDataname)
|
|
text += "<<" + base::get_file_name(dataname);
|
|
|
|
const int ret = OptionalAlert::show(
|
|
Preferences::instance().spriteSheet.showOverwriteFilesAlert,
|
|
1, // Yes is the default option when the alert dialog is disabled
|
|
Strings::alerts_overwrite_files_on_export_sprite_sheet(text));
|
|
if (ret != 1)
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
ConstraintType constraint_type_from_params(const ExportSpriteSheetParams& params)
|
|
{
|
|
switch (params.type()) {
|
|
case app::SpriteSheetType::Rows:
|
|
if (params.width() > 0)
|
|
return kConstraintType_Width;
|
|
else if (params.columns() > 0)
|
|
return kConstraintType_Cols;
|
|
break;
|
|
case app::SpriteSheetType::Columns:
|
|
if (params.height() > 0)
|
|
return kConstraintType_Height;
|
|
else if (params.rows() > 0)
|
|
return kConstraintType_Rows;
|
|
break;
|
|
case app::SpriteSheetType::Packed:
|
|
if (params.width() > 0 && params.height() > 0)
|
|
return kConstraintType_Size;
|
|
else if (params.width() > 0)
|
|
return kConstraintType_Width;
|
|
else if (params.height() > 0)
|
|
return kConstraintType_Height;
|
|
break;
|
|
}
|
|
return kConstraintType_None;
|
|
}
|
|
|
|
void destroy_doc(Context* ctx, Doc* doc)
|
|
{
|
|
try {
|
|
DocDestroyer destroyer(ctx, doc, 500);
|
|
destroyer.destroyDocument();
|
|
}
|
|
catch (const LockedDocException& ex) {
|
|
Console::showException(ex);
|
|
}
|
|
}
|
|
|
|
void insert_layers_to_selected_layers(Layer* layer, SelectedLayers& selectedLayers)
|
|
{
|
|
if (layer->isGroup()) {
|
|
auto children = static_cast<LayerGroup*>(layer)->layers();
|
|
for (auto child : children)
|
|
insert_layers_to_selected_layers(child, selectedLayers);
|
|
}
|
|
else
|
|
selectedLayers.insert(layer);
|
|
}
|
|
|
|
Doc* generate_sprite_sheet_from_params(DocExporter& exporter,
|
|
Context* ctx,
|
|
const Site& site,
|
|
const ExportSpriteSheetParams& params,
|
|
const bool saveData,
|
|
base::task_token& token)
|
|
{
|
|
const app::SpriteSheetType type = params.type();
|
|
const int columns = params.columns();
|
|
const int rows = params.rows();
|
|
const int width = params.width();
|
|
const int height = params.height();
|
|
const std::string filename = params.textureFilename();
|
|
const std::string dataFilename = params.dataFilename();
|
|
const SpriteSheetDataFormat dataFormat = params.dataFormat();
|
|
const std::string filenameFormat = params.filenameFormat();
|
|
const std::string tagnameFormat = params.tagnameFormat();
|
|
const std::string layerName = params.layer();
|
|
const int layerIndex = params.layerIndex();
|
|
const std::string tagName = params.tag();
|
|
const int borderPadding = std::clamp(params.borderPadding(), 0, 100);
|
|
const int shapePadding = std::clamp(params.shapePadding(), 0, 100);
|
|
const int innerPadding = std::clamp(params.innerPadding(), 0, 100);
|
|
const bool trimSprite = params.trimSprite();
|
|
const bool trimCels = params.trim();
|
|
const bool trimByGrid = params.trimByGrid();
|
|
const bool extrude = params.extrude();
|
|
const bool ignoreEmpty = params.ignoreEmpty();
|
|
const bool mergeDuplicates = params.mergeDuplicates();
|
|
const bool splitLayers = params.splitLayers();
|
|
const bool splitTags = params.splitTags();
|
|
const bool splitGrid = params.splitGrid();
|
|
const bool listLayers = params.listLayers();
|
|
const bool listTags = params.listTags();
|
|
const bool listSlices = params.listSlices();
|
|
const bool fromTilesets = params.fromTilesets();
|
|
|
|
SelectedFrames selFrames;
|
|
Tag* tag = calculate_selected_frames(site, tagName, selFrames);
|
|
|
|
#ifdef _DEBUG
|
|
frame_t nframes = selFrames.size();
|
|
ASSERT(nframes > 0);
|
|
#endif
|
|
|
|
Doc* doc = const_cast<Doc*>(site.document());
|
|
const Sprite* sprite = site.sprite();
|
|
|
|
// If the user choose to render selected layers only, we can
|
|
// temporaly make them visible and hide the other ones.
|
|
RestoreVisibleLayers layersVisibility;
|
|
calculate_visible_layers(site, layerName, layerIndex, layersVisibility);
|
|
|
|
SelectedLayers selLayers;
|
|
if (layerName != kSelectedLayers) {
|
|
// TODO add a getLayerByName
|
|
int i = sprite->allLayersCount();
|
|
for (Layer* layer : sprite->allLayers()) {
|
|
i--;
|
|
if (get_layer_path(layer) == layerName && (layerIndex == -1 || layerIndex == i)) {
|
|
if (layer->isGroup())
|
|
insert_layers_to_selected_layers(layer, selLayers);
|
|
else
|
|
selLayers.insert(layer);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
exporter.reset();
|
|
|
|
// Use each tileset from tilemap layers as a sprite
|
|
if (fromTilesets) {
|
|
exporter.addTilesetsSamples(doc, !selLayers.empty() ? &selLayers : nullptr);
|
|
}
|
|
// Use the whole canvas as a sprite
|
|
else {
|
|
exporter.addDocumentSamples(doc,
|
|
tag,
|
|
splitLayers,
|
|
splitTags,
|
|
splitGrid,
|
|
!selLayers.empty() ? &selLayers : nullptr,
|
|
!selFrames.empty() ? &selFrames : nullptr);
|
|
}
|
|
|
|
if (saveData) {
|
|
if (!filename.empty())
|
|
exporter.setTextureFilename(filename);
|
|
if (!dataFilename.empty()) {
|
|
exporter.setDataFilename(dataFilename);
|
|
exporter.setDataFormat(dataFormat);
|
|
}
|
|
}
|
|
if (!filenameFormat.empty())
|
|
exporter.setFilenameFormat(filenameFormat);
|
|
|
|
if (!tagnameFormat.empty())
|
|
exporter.setTagnameFormat(tagnameFormat);
|
|
|
|
exporter.setTextureWidth(width);
|
|
exporter.setTextureHeight(height);
|
|
exporter.setTextureColumns(columns);
|
|
exporter.setTextureRows(rows);
|
|
exporter.setSpriteSheetType(type);
|
|
exporter.setBorderPadding(borderPadding);
|
|
exporter.setShapePadding(shapePadding);
|
|
exporter.setInnerPadding(innerPadding);
|
|
exporter.setTrimSprite(trimSprite);
|
|
exporter.setTrimCels(trimCels);
|
|
exporter.setTrimByGrid(trimByGrid);
|
|
exporter.setExtrude(extrude);
|
|
exporter.setSplitLayers(splitLayers);
|
|
exporter.setSplitTags(splitTags);
|
|
exporter.setIgnoreEmptyCels(ignoreEmpty);
|
|
exporter.setMergeDuplicates(mergeDuplicates);
|
|
if (listLayers)
|
|
exporter.setListLayers(true);
|
|
if (listTags)
|
|
exporter.setListTags(true);
|
|
if (listSlices)
|
|
exporter.setListSlices(true);
|
|
|
|
// We have to call exportSheet() while RestoreVisibleLayers is still
|
|
// alive. In this way we can export selected layers correctly if
|
|
// that option (kSelectedLayers) is selected.
|
|
return exporter.exportSheet(ctx, token);
|
|
}
|
|
|
|
std::unique_ptr<Doc> generate_sprite_sheet(DocExporter& exporter,
|
|
Context* ctx,
|
|
const Site& site,
|
|
const ExportSpriteSheetParams& params,
|
|
bool saveData,
|
|
base::task_token& token)
|
|
{
|
|
std::unique_ptr<Doc> newDocument(
|
|
generate_sprite_sheet_from_params(exporter, ctx, site, params, saveData, token));
|
|
if (!newDocument)
|
|
return nullptr;
|
|
|
|
// Setup a filename for the new document in case that user didn't
|
|
// save the file/specified one output filename.
|
|
if (params.textureFilename().empty()) {
|
|
std::string fn = site.document()->filename();
|
|
std::string ext = base::get_file_extension(fn);
|
|
if (!ext.empty())
|
|
ext.insert(0, 1, '.');
|
|
|
|
newDocument->setFilename(
|
|
base::join_path(base::get_file_path(fn), base::get_file_title(fn) + "-Sheet") + ext);
|
|
}
|
|
return newDocument;
|
|
}
|
|
|
|
class ExportSpriteSheetWindow : public app::gen::ExportSpriteSheet {
|
|
public:
|
|
ExportSpriteSheetWindow(DocExporter& exporter,
|
|
Site& site,
|
|
ExportSpriteSheetParams& params,
|
|
Preferences& pref)
|
|
: m_exporter(exporter)
|
|
, m_frontBuffer(std::make_shared<doc::ImageBuffer>())
|
|
, m_backBuffer(std::make_shared<doc::ImageBuffer>())
|
|
, m_site(site)
|
|
, m_sprite(site.sprite())
|
|
, m_editor(nullptr)
|
|
, m_genTimer(100, nullptr)
|
|
, m_executionID(0)
|
|
, m_filenameFormat(params.filenameFormat())
|
|
, m_tagnameFormat(params.tagnameFormat())
|
|
{
|
|
sectionTabs()->ItemChange.connect([this] { onChangeSection(); });
|
|
expandSections()->Click.connect([this] { onExpandSections(); });
|
|
closeSpriteSection()->Click.connect([this] { onCloseSection(kSectionSprite); });
|
|
closeBordersSection()->Click.connect([this] { onCloseSection(kSectionBorders); });
|
|
closeOutputSection()->Click.connect([this] { onCloseSection(kSectionOutput); });
|
|
|
|
static_assert(
|
|
(int)app::SpriteSheetType::None == 0 && (int)app::SpriteSheetType::Horizontal == 1 &&
|
|
(int)app::SpriteSheetType::Vertical == 2 && (int)app::SpriteSheetType::Rows == 3 &&
|
|
(int)app::SpriteSheetType::Columns == 4 && (int)app::SpriteSheetType::Packed == 5,
|
|
"SpriteSheetType enum changed");
|
|
|
|
sheetType()->addItem(Strings::export_sprite_sheet_type_horz());
|
|
sheetType()->addItem(Strings::export_sprite_sheet_type_vert());
|
|
sheetType()->addItem(Strings::export_sprite_sheet_type_rows());
|
|
sheetType()->addItem(Strings::export_sprite_sheet_type_cols());
|
|
sheetType()->addItem(Strings::export_sprite_sheet_type_pack());
|
|
{
|
|
int i;
|
|
if (params.type() != app::SpriteSheetType::None)
|
|
i = (int)params.type() - 1;
|
|
else
|
|
i = ((int)app::SpriteSheetType::Rows) - 1;
|
|
sheetType()->setSelectedItemIndex(i);
|
|
}
|
|
|
|
constraintType()->addItem(Strings::export_sprite_sheet_constraint_fixed_none());
|
|
constraintType()->addItem(Strings::export_sprite_sheet_constraint_fixed_cols());
|
|
constraintType()->addItem(Strings::export_sprite_sheet_constraint_fixed_rows());
|
|
constraintType()->addItem(Strings::export_sprite_sheet_constraint_fixed_width());
|
|
constraintType()->addItem(Strings::export_sprite_sheet_constraint_fixed_height());
|
|
constraintType()->addItem(Strings::export_sprite_sheet_constraint_fixed_size());
|
|
|
|
auto constraint = constraint_type_from_params(params);
|
|
constraintType()->setSelectedItemIndex(constraint);
|
|
switch (constraint) {
|
|
case kConstraintType_Cols: widthConstraint()->setTextf("%d", params.columns()); break;
|
|
case kConstraintType_Rows: heightConstraint()->setTextf("%d", params.rows()); break;
|
|
case kConstraintType_Width: widthConstraint()->setTextf("%d", params.width()); break;
|
|
case kConstraintType_Height: heightConstraint()->setTextf("%d", params.height()); break;
|
|
case kConstraintType_Size:
|
|
widthConstraint()->setTextf("%d", params.width());
|
|
heightConstraint()->setTextf("%d", params.height());
|
|
break;
|
|
}
|
|
|
|
static_assert(kSource_Sprite == 0 && kSource_SpriteGrid == 1 && kSource_Tilesets == 2,
|
|
"Source enum has changed");
|
|
source()->addItem(new ListItem("Sprite"));
|
|
source()->addItem(new ListItem("Sprite Grid"));
|
|
source()->addItem(new ListItem("Tilesets"));
|
|
if (params.splitGrid())
|
|
source()->setSelectedItemIndex(int(kSource_SpriteGrid));
|
|
else if (params.fromTilesets())
|
|
source()->setSelectedItemIndex(int(kSource_Tilesets));
|
|
|
|
fill_layers_combobox(m_sprite, layers(), params.layer(), params.layerIndex());
|
|
|
|
fill_frames_combobox(m_sprite, frames(), params.tag());
|
|
|
|
openGenerated()->setSelected(params.openGenerated());
|
|
trimSpriteEnabled()->setSelected(params.trimSprite());
|
|
trimEnabled()->setSelected(params.trim());
|
|
trimContainer()->setVisible(trimSpriteEnabled()->isSelected() || trimEnabled()->isSelected());
|
|
gridTrimEnabled()->setSelected(
|
|
(trimSpriteEnabled()->isSelected() || trimEnabled()->isSelected()) && params.trimByGrid());
|
|
extrudeEnabled()->setSelected(params.extrude());
|
|
mergeDups()->setSelected(params.mergeDuplicates());
|
|
ignoreEmpty()->setSelected(params.ignoreEmpty());
|
|
|
|
borderPadding()->setTextf("%d", params.borderPadding());
|
|
shapePadding()->setTextf("%d", params.shapePadding());
|
|
innerPadding()->setTextf("%d", params.innerPadding());
|
|
|
|
imageFilename()->setFilename(params.textureFilename());
|
|
imageEnabled()->setSelected(!imageFilename()->fullFilename().empty());
|
|
imageFilename()->setVisible(imageEnabled()->isSelected());
|
|
|
|
dataFilename()->setFilename(params.dataFilename());
|
|
dataEnabled()->setSelected(!dataFilename()->fullFilename().empty());
|
|
dataFormat()->setSelectedItemIndex(int(params.dataFormat()));
|
|
splitLayers()->setSelected(params.splitLayers());
|
|
splitTags()->setSelected(params.splitTags());
|
|
listLayers()->setSelected(params.listLayers());
|
|
listTags()->setSelected(params.listTags());
|
|
listSlices()->setSelected(params.listSlices());
|
|
|
|
updateDefaultDataFilenameFormat();
|
|
updateDefaultDataTagnameFormat();
|
|
updateDataFields();
|
|
|
|
std::string base = site.document()->filename();
|
|
const std::string basePath = (base::get_file_path(base).empty() ? base::get_current_path() :
|
|
base::get_file_path(base));
|
|
base = base::join_path(basePath, base::get_file_title(base));
|
|
|
|
imageFilename()->setDocFilename(base);
|
|
dataFilename()->setDocFilename(base);
|
|
if (imageFilename()->fullFilename().empty() ||
|
|
imageFilename()->fullFilename() == kSpecifiedFilename) {
|
|
std::string defExt = pref.spriteSheet.defaultExtension();
|
|
|
|
if (base::utf8_icmp(base::get_file_extension(site.document()->filename()), defExt) == 0)
|
|
imageFilename()->setFilename(base + "-sheet." + defExt);
|
|
else
|
|
imageFilename()->setFilename(base + "." + defExt);
|
|
}
|
|
|
|
if (dataFilename()->fullFilename().empty() ||
|
|
imageFilename()->fullFilename() == kSpecifiedFilename)
|
|
dataFilename()->setFilename(base + ".json");
|
|
|
|
exportButton()->Click.connect([this] { onExport(); });
|
|
sheetType()->Change.connect([this] { onSheetTypeChange(); });
|
|
constraintType()->Change.connect([this] { onConstraintTypeChange(); });
|
|
widthConstraint()->Change.connect([this] { generatePreview(); });
|
|
heightConstraint()->Change.connect([this] { generatePreview(); });
|
|
borderPadding()->Change.connect([this] { generatePreview(); });
|
|
shapePadding()->Change.connect([this] { generatePreview(); });
|
|
innerPadding()->Change.connect([this] { generatePreview(); });
|
|
extrudeEnabled()->Click.connect([this] { generatePreview(); });
|
|
mergeDups()->Click.connect([this] { generatePreview(); });
|
|
ignoreEmpty()->Click.connect([this] { generatePreview(); });
|
|
|
|
imageEnabled()->Click.connect(
|
|
[this] { onOutputFieldEnabledChange(imageFilename(), imageEnabled()->isSelected()); });
|
|
imageFilename()->SelectOutputFile.connect(
|
|
[this]() -> std::string { return onFilenameBrowse(imageFilename()); });
|
|
imageFilename()->Change.connect([this] { resize(); });
|
|
dataEnabled()->Click.connect(
|
|
[this] { onOutputFieldEnabledChange(dataFilename(), dataEnabled()->isSelected()); });
|
|
dataFilename()->SelectOutputFile.connect(
|
|
[this]() -> std::string { return onFilenameBrowse(dataFilename()); });
|
|
dataFilename()->Change.connect([this] { resize(); });
|
|
|
|
trimSpriteEnabled()->Click.connect([this] { onTrimEnabledChange(); });
|
|
trimEnabled()->Click.connect([this] { onTrimEnabledChange(); });
|
|
gridTrimEnabled()->Click.connect([this] { generatePreview(); });
|
|
source()->Change.connect([this] { generatePreview(); });
|
|
layers()->Change.connect([this] { generatePreview(); });
|
|
splitLayers()->Click.connect([this] { onSplitLayersOrFrames(); });
|
|
splitTags()->Click.connect([this] { onSplitLayersOrFrames(); });
|
|
frames()->Change.connect([this] { generatePreview(); });
|
|
dataFilenameFormat()->Change.connect([this] { onDataFilenameFormatChange(); });
|
|
dataTagnameFormat()->Change.connect([this] { onDataTagnameFormatChange(); });
|
|
openGenerated()->Click.connect([this] { onOpenGeneratedChange(); });
|
|
preview()->Click.connect([this] { generatePreview(); });
|
|
m_genTimer.Tick.connect([this] { onGenTimerTick(); });
|
|
|
|
// Select tabs
|
|
{
|
|
const std::string s = pref.spriteSheet.sections();
|
|
const bool layout = (s.find("layout") != std::string::npos);
|
|
const bool sprite = (s.find("sprite") != std::string::npos);
|
|
const bool borders = (s.find("borders") != std::string::npos);
|
|
const bool output = (s.find("output") != std::string::npos);
|
|
sectionTabs()->getItem(kSectionLayout)->setSelected(layout || (!sprite & !borders && !output));
|
|
sectionTabs()->getItem(kSectionSprite)->setSelected(sprite);
|
|
sectionTabs()->getItem(kSectionBorders)->setSelected(borders);
|
|
sectionTabs()->getItem(kSectionOutput)->setSelected(output);
|
|
}
|
|
|
|
onChangeSection();
|
|
onSheetTypeChange();
|
|
imageFilename()->onUpdateText();
|
|
dataFilename()->onUpdateText();
|
|
updateExportButton();
|
|
|
|
preview()->setSelected(pref.spriteSheet.preview());
|
|
generatePreview();
|
|
|
|
remapWindow();
|
|
centerWindow();
|
|
load_window_pos(this, "ExportSpriteSheet");
|
|
}
|
|
|
|
~ExportSpriteSheetWindow()
|
|
{
|
|
cancelGenTask();
|
|
if (m_spriteSheet) {
|
|
auto ctx = UIContext::instance();
|
|
ctx->setActiveDocument(m_site.document());
|
|
|
|
destroy_doc(ctx, m_spriteSheet.release());
|
|
}
|
|
}
|
|
|
|
std::string selectedSectionsString() const
|
|
{
|
|
const bool layout = sectionTabs()->getItem(kSectionLayout)->isSelected();
|
|
const bool sprite = sectionTabs()->getItem(kSectionSprite)->isSelected();
|
|
const bool borders = sectionTabs()->getItem(kSectionBorders)->isSelected();
|
|
const bool output = sectionTabs()->getItem(kSectionOutput)->isSelected();
|
|
return fmt::format("{} {} {} {}",
|
|
(layout ? "layout" : ""),
|
|
(sprite ? "sprite" : ""),
|
|
(borders ? "borders" : ""),
|
|
(output ? "output" : ""));
|
|
}
|
|
|
|
bool ok() const { return closer() == exportButton(); }
|
|
|
|
void updateParams(ExportSpriteSheetParams& params)
|
|
{
|
|
params.type(spriteSheetTypeValue());
|
|
params.columns(columnsValue());
|
|
params.rows(rowsValue());
|
|
params.width(widthValue());
|
|
params.height(heightValue());
|
|
params.textureFilename(filenameValue());
|
|
params.dataFilename(dataFilenameValue());
|
|
params.dataFormat(dataFormatValue());
|
|
params.filenameFormat(filenameFormatValue());
|
|
params.tagnameFormat(tagnameFormatValue());
|
|
params.borderPadding(borderPaddingValue());
|
|
params.shapePadding(shapePaddingValue());
|
|
params.innerPadding(innerPaddingValue());
|
|
params.trimSprite(trimSpriteValue());
|
|
params.trim(trimValue());
|
|
params.trimByGrid(trimByGridValue());
|
|
params.extrude(extrudeValue());
|
|
params.mergeDuplicates(mergeDupsValue());
|
|
params.ignoreEmpty(ignoreEmptyValue());
|
|
params.openGenerated(openGeneratedValue());
|
|
params.layer(layerValue());
|
|
params.layerIndex(layerIndex());
|
|
params.tag(tagValue());
|
|
params.splitLayers(splitLayersValue());
|
|
params.splitTags(splitTagsValue());
|
|
params.listLayers(listLayersValue());
|
|
params.listTags(listTagsValue());
|
|
params.listSlices(listSlicesValue());
|
|
params.splitGrid(source()->getSelectedItemIndex() == int(kSource_SpriteGrid));
|
|
params.fromTilesets(source()->getSelectedItemIndex() == int(kSource_Tilesets));
|
|
}
|
|
|
|
private:
|
|
bool onProcessMessage(ui::Message* msg) override
|
|
{
|
|
switch (msg->type()) {
|
|
case kCloseMessage: save_window_pos(this, "ExportSpriteSheet"); break;
|
|
}
|
|
return Window::onProcessMessage(msg);
|
|
}
|
|
|
|
void onBroadcastMouseMessage(const gfx::Point& screenPos, WidgetsList& targets) override
|
|
{
|
|
Window::onBroadcastMouseMessage(screenPos, targets);
|
|
|
|
// Add the editor as receptor of mouse events too.
|
|
if (m_editor)
|
|
targets.push_back(View::getView(m_editor));
|
|
}
|
|
|
|
void onChangeSection()
|
|
{
|
|
panel()->showAllChildren();
|
|
|
|
const bool layout = sectionTabs()->getItem(kSectionLayout)->isSelected();
|
|
const bool sprite = sectionTabs()->getItem(kSectionSprite)->isSelected();
|
|
const bool borders = sectionTabs()->getItem(kSectionBorders)->isSelected();
|
|
const bool output = sectionTabs()->getItem(kSectionOutput)->isSelected();
|
|
|
|
sectionLayout()->setVisible(layout);
|
|
sectionSpriteSeparator()->setVisible(sprite && layout);
|
|
sectionSprite()->setVisible(sprite);
|
|
sectionBordersSeparator()->setVisible(borders && (layout || sprite));
|
|
sectionBorders()->setVisible(borders);
|
|
sectionOutputSeparator()->setVisible(output && (layout || sprite || borders));
|
|
sectionOutput()->setVisible(output);
|
|
|
|
resize();
|
|
}
|
|
|
|
void onExpandSections()
|
|
{
|
|
sectionTabs()->getItem(kSectionLayout)->setSelected(true);
|
|
sectionTabs()->getItem(kSectionSprite)->setSelected(true);
|
|
sectionTabs()->getItem(kSectionBorders)->setSelected(true);
|
|
sectionTabs()->getItem(kSectionOutput)->setSelected(true);
|
|
onChangeSection();
|
|
}
|
|
|
|
void onCloseSection(const Section section)
|
|
{
|
|
if (sectionTabs()->countSelectedItems() > 1)
|
|
sectionTabs()->getItem(section)->setSelected(false);
|
|
onChangeSection();
|
|
}
|
|
|
|
app::SpriteSheetType spriteSheetTypeValue() const
|
|
{
|
|
return (app::SpriteSheetType)(sheetType()->getSelectedItemIndex() + 1);
|
|
}
|
|
|
|
int columnsValue() const
|
|
{
|
|
if (spriteSheetTypeValue() == app::SpriteSheetType::Rows &&
|
|
constraintType()->getSelectedItemIndex() == (int)kConstraintType_Cols) {
|
|
return widthConstraint()->textInt();
|
|
}
|
|
else
|
|
return 0;
|
|
}
|
|
|
|
int rowsValue() const
|
|
{
|
|
if (spriteSheetTypeValue() == app::SpriteSheetType::Columns &&
|
|
constraintType()->getSelectedItemIndex() == (int)kConstraintType_Rows) {
|
|
return heightConstraint()->textInt();
|
|
}
|
|
else
|
|
return 0;
|
|
}
|
|
|
|
int widthValue() const
|
|
{
|
|
if ((spriteSheetTypeValue() == app::SpriteSheetType::Rows ||
|
|
spriteSheetTypeValue() == app::SpriteSheetType::Packed) &&
|
|
(constraintType()->getSelectedItemIndex() == (int)kConstraintType_Width ||
|
|
constraintType()->getSelectedItemIndex() == (int)kConstraintType_Size)) {
|
|
return widthConstraint()->textInt();
|
|
}
|
|
else
|
|
return 0;
|
|
}
|
|
|
|
int heightValue() const
|
|
{
|
|
if ((spriteSheetTypeValue() == app::SpriteSheetType::Columns ||
|
|
spriteSheetTypeValue() == app::SpriteSheetType::Packed) &&
|
|
(constraintType()->getSelectedItemIndex() == (int)kConstraintType_Height ||
|
|
constraintType()->getSelectedItemIndex() == (int)kConstraintType_Size)) {
|
|
return heightConstraint()->textInt();
|
|
}
|
|
else
|
|
return 0;
|
|
}
|
|
|
|
std::string filenameValue() const
|
|
{
|
|
if (imageEnabled()->isSelected())
|
|
return imageFilename()->fullFilename();
|
|
else
|
|
return std::string();
|
|
}
|
|
|
|
std::string dataFilenameValue() const
|
|
{
|
|
if (dataEnabled()->isSelected())
|
|
return dataFilename()->fullFilename();
|
|
else
|
|
return std::string();
|
|
}
|
|
|
|
std::string filenameFormatValue() const
|
|
{
|
|
if (!m_filenameFormat.empty() && m_filenameFormat != m_filenameFormatDefault)
|
|
return m_filenameFormat;
|
|
else
|
|
return std::string();
|
|
}
|
|
|
|
std::string tagnameFormatValue() const
|
|
{
|
|
if (!m_tagnameFormat.empty() && m_tagnameFormat != m_tagnameFormatDefault)
|
|
return m_tagnameFormat;
|
|
else
|
|
return std::string();
|
|
}
|
|
|
|
SpriteSheetDataFormat dataFormatValue() const
|
|
{
|
|
if (dataEnabled()->isSelected())
|
|
return SpriteSheetDataFormat(dataFormat()->getSelectedItemIndex());
|
|
else
|
|
return SpriteSheetDataFormat::Default;
|
|
}
|
|
|
|
int borderPaddingValue() const
|
|
{
|
|
int value = borderPadding()->textInt();
|
|
return std::clamp(value, 0, 100);
|
|
}
|
|
|
|
int shapePaddingValue() const
|
|
{
|
|
int value = shapePadding()->textInt();
|
|
return std::clamp(value, 0, 100);
|
|
}
|
|
|
|
int innerPaddingValue() const
|
|
{
|
|
int value = innerPadding()->textInt();
|
|
return std::clamp(value, 0, 100);
|
|
}
|
|
|
|
bool trimSpriteValue() const { return trimSpriteEnabled()->isSelected(); }
|
|
|
|
bool trimValue() const { return trimEnabled()->isSelected(); }
|
|
|
|
bool trimByGridValue() const { return gridTrimEnabled()->isSelected(); }
|
|
|
|
bool extrudeValue() const { return extrudeEnabled()->isSelected(); }
|
|
|
|
bool extrudePadding() const { return (extrudeValue() ? 1 : 0); }
|
|
|
|
bool mergeDupsValue() const { return mergeDups()->isSelected(); }
|
|
|
|
bool ignoreEmptyValue() const { return ignoreEmpty()->isSelected(); }
|
|
|
|
bool openGeneratedValue() const { return openGenerated()->isSelected(); }
|
|
|
|
std::string layerValue() const { return layers()->getValue(); }
|
|
|
|
int layerIndex() const
|
|
{
|
|
int i = layers()->getSelectedItemIndex() - kLayersComboboxExtraInitialItems;
|
|
return i < 0 ? -1 : i;
|
|
}
|
|
|
|
std::string tagValue() const { return frames()->getValue(); }
|
|
|
|
bool splitLayersValue() const { return splitLayers()->isSelected(); }
|
|
|
|
bool splitTagsValue() const { return splitTags()->isSelected(); }
|
|
|
|
bool splitGridValue() const
|
|
{
|
|
return (source()->getSelectedItemIndex() == int(kSource_SpriteGrid));
|
|
}
|
|
|
|
bool listLayersValue() const { return listLayers()->isSelected(); }
|
|
|
|
bool listTagsValue() const { return listTags()->isSelected(); }
|
|
|
|
bool listSlicesValue() const { return listSlices()->isSelected(); }
|
|
|
|
void onExport()
|
|
{
|
|
if (!ask_overwrite(imageFilename()->askOverwrite(),
|
|
filenameValue(),
|
|
dataFilename()->askOverwrite(),
|
|
dataFilenameValue()))
|
|
return;
|
|
|
|
closeWindow(exportButton());
|
|
}
|
|
|
|
void onSheetTypeChange()
|
|
{
|
|
for (int i = 1; i < constraintType()->getItemCount(); ++i)
|
|
constraintType()->getItem(i)->setVisible(false);
|
|
|
|
mergeDups()->setEnabled(true);
|
|
|
|
const ConstraintType selectConstraint =
|
|
(ConstraintType)constraintType()->getSelectedItemIndex();
|
|
switch (spriteSheetTypeValue()) {
|
|
case app::SpriteSheetType::Horizontal:
|
|
case app::SpriteSheetType::Vertical:
|
|
constraintType()->setSelectedItemIndex(kConstraintType_None);
|
|
break;
|
|
case app::SpriteSheetType::Rows:
|
|
constraintType()->getItem(kConstraintType_Cols)->setVisible(true);
|
|
constraintType()->getItem(kConstraintType_Width)->setVisible(true);
|
|
if (selectConstraint != kConstraintType_None && selectConstraint != kConstraintType_Cols &&
|
|
selectConstraint != kConstraintType_Width)
|
|
constraintType()->setSelectedItemIndex(kConstraintType_None);
|
|
break;
|
|
case app::SpriteSheetType::Columns:
|
|
constraintType()->getItem(kConstraintType_Rows)->setVisible(true);
|
|
constraintType()->getItem(kConstraintType_Height)->setVisible(true);
|
|
if (selectConstraint != kConstraintType_None && selectConstraint != kConstraintType_Rows &&
|
|
selectConstraint != kConstraintType_Height)
|
|
constraintType()->setSelectedItemIndex(kConstraintType_None);
|
|
break;
|
|
case app::SpriteSheetType::Packed:
|
|
constraintType()->getItem(kConstraintType_Width)->setVisible(true);
|
|
constraintType()->getItem(kConstraintType_Height)->setVisible(true);
|
|
constraintType()->getItem(kConstraintType_Size)->setVisible(true);
|
|
if (selectConstraint != kConstraintType_None && selectConstraint != kConstraintType_Width &&
|
|
selectConstraint != kConstraintType_Height &&
|
|
selectConstraint != kConstraintType_Size) {
|
|
constraintType()->setSelectedItemIndex(kConstraintType_None);
|
|
}
|
|
mergeDups()->setSelected(true);
|
|
mergeDups()->setEnabled(false);
|
|
break;
|
|
}
|
|
onConstraintTypeChange();
|
|
}
|
|
|
|
void onConstraintTypeChange()
|
|
{
|
|
bool withWidth = false;
|
|
bool withHeight = false;
|
|
switch ((ConstraintType)constraintType()->getSelectedItemIndex()) {
|
|
case kConstraintType_Cols:
|
|
withWidth = true;
|
|
widthConstraint()->setSuffix("");
|
|
break;
|
|
case kConstraintType_Rows:
|
|
withHeight = true;
|
|
heightConstraint()->setSuffix("");
|
|
break;
|
|
case kConstraintType_Width:
|
|
withWidth = true;
|
|
widthConstraint()->setSuffix("px");
|
|
break;
|
|
case kConstraintType_Height:
|
|
withHeight = true;
|
|
heightConstraint()->setSuffix("px");
|
|
break;
|
|
case kConstraintType_Size:
|
|
withWidth = true;
|
|
withHeight = true;
|
|
widthConstraint()->setSuffix("px");
|
|
heightConstraint()->setSuffix("px");
|
|
break;
|
|
}
|
|
widthConstraint()->setVisible(withWidth);
|
|
heightConstraint()->setVisible(withHeight);
|
|
resize();
|
|
generatePreview();
|
|
}
|
|
|
|
std::string onFilenameBrowse(FilenameField* const field)
|
|
{
|
|
const std::string& title = (field == dataFilename() ?
|
|
Strings::export_sprite_sheet_save_json_title() :
|
|
Strings::export_sprite_sheet_save_title());
|
|
// TODO hardcoded "json" extension
|
|
const base::paths json = { "json" };
|
|
base::paths exts = (field == imageFilename() ? get_writable_extensions() : json);
|
|
base::paths newFilename;
|
|
|
|
if (!app::show_file_selector(title,
|
|
field->fullFilename(),
|
|
exts,
|
|
FileSelectorType::Save,
|
|
newFilename))
|
|
return std::string();
|
|
|
|
ASSERT(!newFilename.empty());
|
|
return newFilename.front();
|
|
}
|
|
|
|
void onOutputFieldEnabledChange(FilenameField* const field, bool visible)
|
|
{
|
|
field->setAskOverwrite(true);
|
|
field->setVisible(visible);
|
|
if (field == dataFilename())
|
|
updateDataFields();
|
|
|
|
updateExportButton();
|
|
resize();
|
|
}
|
|
|
|
void onTrimEnabledChange()
|
|
{
|
|
trimContainer()->setVisible(trimSpriteEnabled()->isSelected() || trimEnabled()->isSelected());
|
|
resize();
|
|
generatePreview();
|
|
}
|
|
|
|
void onSplitLayersOrFrames()
|
|
{
|
|
updateDefaultDataFilenameFormat();
|
|
updateDefaultDataTagnameFormat();
|
|
generatePreview();
|
|
}
|
|
|
|
void onDataFilenameFormatChange()
|
|
{
|
|
m_filenameFormat = dataFilenameFormat()->text();
|
|
if (m_filenameFormat.empty())
|
|
updateDefaultDataFilenameFormat();
|
|
}
|
|
|
|
void onDataTagnameFormatChange()
|
|
{
|
|
m_tagnameFormat = dataTagnameFormat()->text();
|
|
if (m_tagnameFormat.empty())
|
|
updateDefaultDataTagnameFormat();
|
|
}
|
|
|
|
void onOpenGeneratedChange() { updateExportButton(); }
|
|
|
|
void resize() { expandWindow(sizeHint()); }
|
|
|
|
void updateExportButton()
|
|
{
|
|
exportButton()->setEnabled(imageEnabled()->isSelected() || dataEnabled()->isSelected() ||
|
|
openGenerated()->isSelected());
|
|
}
|
|
|
|
void updateDefaultDataFilenameFormat()
|
|
{
|
|
m_filenameFormatDefault = get_default_filename_format_for_sheet(
|
|
m_site.document()->filename(),
|
|
m_site.document()->sprite()->totalFrames() > 0,
|
|
splitLayersValue(),
|
|
splitTagsValue());
|
|
|
|
if (m_filenameFormat.empty()) {
|
|
dataFilenameFormat()->setText(m_filenameFormatDefault);
|
|
}
|
|
else {
|
|
dataFilenameFormat()->setText(m_filenameFormat);
|
|
}
|
|
}
|
|
|
|
void updateDefaultDataTagnameFormat()
|
|
{
|
|
m_tagnameFormatDefault = get_default_tagname_format_for_sheet();
|
|
|
|
if (m_tagnameFormat.empty()) {
|
|
dataTagnameFormat()->setText(m_tagnameFormatDefault);
|
|
}
|
|
else {
|
|
dataTagnameFormat()->setText(m_tagnameFormat);
|
|
}
|
|
}
|
|
|
|
void updateDataFields()
|
|
{
|
|
bool state = dataEnabled()->isSelected();
|
|
dataFilename()->setVisible(state);
|
|
dataMeta()->setVisible(state);
|
|
dataFormatsPlaceholder()->setVisible(state);
|
|
}
|
|
|
|
void onGenTimerTick()
|
|
{
|
|
if (!m_genTask) {
|
|
m_genTimer.stop();
|
|
setText(Strings::export_sprite_sheet_title());
|
|
return;
|
|
}
|
|
setText(fmt::format("{} ({} {}%)",
|
|
Strings::export_sprite_sheet_title(),
|
|
Strings::export_sprite_sheet_preview(),
|
|
int(100.0f * m_genTask->progress())));
|
|
}
|
|
|
|
void generatePreview()
|
|
{
|
|
cancelGenTask();
|
|
|
|
if (!preview()->isSelected()) {
|
|
if (m_spriteSheet) {
|
|
auto ctx = UIContext::instance();
|
|
ctx->setActiveDocument(m_site.document());
|
|
|
|
destroy_doc(ctx, m_spriteSheet.release());
|
|
m_editor = nullptr;
|
|
}
|
|
return;
|
|
}
|
|
|
|
ASSERT(m_genTask == nullptr);
|
|
|
|
ExportSpriteSheetParams params;
|
|
updateParams(params);
|
|
|
|
std::unique_ptr<Task> task(new Task);
|
|
task->run(
|
|
[this, params](base::task_token& token) { generateSpriteSheetOnBackground(params, token); });
|
|
m_genTask = std::move(task);
|
|
m_genTimer.start();
|
|
onGenTimerTick();
|
|
}
|
|
|
|
void generateSpriteSheetOnBackground(const ExportSpriteSheetParams& params,
|
|
base::task_token& token)
|
|
{
|
|
// Sometimes (more often on Linux) the back buffer is still being
|
|
// used by the new document after
|
|
// generateSpriteSheetOnBackground() and before
|
|
// openGeneratedSpriteSheet(). In this case the use counter is 3
|
|
// which means that 2 or more openGeneratedSpriteSheet() are
|
|
// queued in the laf-os events queue. In this case we just create
|
|
// a new back buffer and the old one will be discarded by
|
|
// openGeneratedSpriteSheet() when m_executionID != executionID.
|
|
if (m_backBuffer.use_count() > 2) {
|
|
auto ptr = std::make_shared<doc::ImageBuffer>();
|
|
m_backBuffer.swap(ptr);
|
|
}
|
|
m_exporter.setDocImageBuffer(m_backBuffer);
|
|
|
|
ASSERT(m_backBuffer.use_count() == 2);
|
|
|
|
// Create a non-UI context to avoid showing UI dialogs for
|
|
// GifOptions or JpegOptions from the background thread.
|
|
Context tmpCtx;
|
|
|
|
Doc* newDocument =
|
|
generate_sprite_sheet(m_exporter, &tmpCtx, m_site, params, false, token).release();
|
|
if (!newDocument)
|
|
return;
|
|
|
|
if (token.canceled()) {
|
|
destroy_doc(&tmpCtx, newDocument);
|
|
return;
|
|
}
|
|
|
|
++m_executionID;
|
|
int executionID = m_executionID;
|
|
|
|
tmpCtx.documents().remove(newDocument);
|
|
|
|
ui::execute_from_ui_thread(
|
|
[this, newDocument, executionID] { openGeneratedSpriteSheet(newDocument, executionID); });
|
|
}
|
|
|
|
void openGeneratedSpriteSheet(Doc* newDocument, int executionID)
|
|
{
|
|
auto context = UIContext::instance();
|
|
|
|
if (!isVisible() ||
|
|
// Other openGeneratedSpriteSheet() is queued and we are the
|
|
// old one. IN this case the newDocument contains a back
|
|
// buffer (ImageBufferPtr) that will be discarded.
|
|
m_executionID != executionID) {
|
|
destroy_doc(context, newDocument);
|
|
return;
|
|
}
|
|
|
|
// Was the preview unselected when we were generating the preview?
|
|
if (!preview()->isSelected())
|
|
return;
|
|
|
|
// Now the "m_frontBuffer" is the current "m_backBuffer" which was
|
|
// used by the generator to create the "newDocument", in the next
|
|
// iteration we'll use the "m_backBuffer" to re-generate the
|
|
// sprite sheet (while the document being displayed in the Editor
|
|
// will use the m_frontBuffer).
|
|
m_frontBuffer.swap(m_backBuffer);
|
|
|
|
if (!m_spriteSheet) {
|
|
m_spriteSheet.reset(newDocument);
|
|
m_spriteSheet->setInhibitBackup(true);
|
|
m_spriteSheet->setContext(context);
|
|
|
|
m_editor = context->getEditorFor(m_spriteSheet.get());
|
|
if (m_editor) {
|
|
m_editor->setState(EditorStatePtr(new NavigateState));
|
|
m_editor->setDefaultScroll();
|
|
}
|
|
}
|
|
else {
|
|
// Replace old cel with the new one
|
|
auto spriteSheetLay = static_cast<LayerImage*>(m_spriteSheet->sprite()->root()->firstLayer());
|
|
auto newDocLay = static_cast<LayerImage*>(newDocument->sprite()->root()->firstLayer());
|
|
Cel* oldCel = m_spriteSheet->sprite()->firstLayer()->cel(0);
|
|
Cel* newCel = newDocument->sprite()->firstLayer()->cel(0);
|
|
|
|
spriteSheetLay->removeCel(oldCel);
|
|
delete oldCel;
|
|
|
|
newDocLay->removeCel(newCel);
|
|
spriteSheetLay->addCel(newCel);
|
|
|
|
// Update sprite sheet size
|
|
m_spriteSheet->sprite()->setSize(newDocument->sprite()->width(),
|
|
newDocument->sprite()->height());
|
|
|
|
m_spriteSheet->notifyGeneralUpdate();
|
|
|
|
destroy_doc(context, newDocument);
|
|
}
|
|
|
|
waitGenTaskAndDelete();
|
|
}
|
|
|
|
void cancelGenTask()
|
|
{
|
|
if (m_genTask) {
|
|
m_genTask->cancel();
|
|
waitGenTaskAndDelete();
|
|
}
|
|
}
|
|
|
|
void waitGenTaskAndDelete()
|
|
{
|
|
if (m_genTask) {
|
|
if (!m_genTask->completed()) {
|
|
while (!m_genTask->completed())
|
|
base::this_thread::sleep_for(0.01);
|
|
}
|
|
m_genTask.reset();
|
|
}
|
|
}
|
|
|
|
DocExporter& m_exporter;
|
|
doc::ImageBufferPtr m_frontBuffer; // ImageBuffer in the preview ImageBuffer
|
|
doc::ImageBufferPtr m_backBuffer; // ImageBuffer in the generator
|
|
Site& m_site;
|
|
Sprite* m_sprite;
|
|
std::unique_ptr<Doc> m_spriteSheet;
|
|
Editor* m_editor;
|
|
std::unique_ptr<Task> m_genTask;
|
|
ui::Timer m_genTimer;
|
|
int m_executionID;
|
|
std::string m_filenameFormat;
|
|
std::string m_filenameFormatDefault;
|
|
std::string m_tagnameFormat;
|
|
std::string m_tagnameFormatDefault;
|
|
};
|
|
|
|
class ExportSpriteSheetJob : public Job {
|
|
public:
|
|
ExportSpriteSheetJob(DocExporter& exporter,
|
|
const Site& site,
|
|
const ExportSpriteSheetParams& params,
|
|
const bool showProgress)
|
|
: Job(Strings::export_sprite_sheet_generating(), showProgress)
|
|
, m_exporter(exporter)
|
|
, m_site(site)
|
|
, m_params(params)
|
|
{
|
|
}
|
|
|
|
std::unique_ptr<Doc> releaseDoc() { return std::move(m_doc); }
|
|
|
|
private:
|
|
void onJob() override
|
|
{
|
|
// Create a non-UI context to avoid showing UI dialogs for
|
|
// GifOptions or JpegOptions from the background thread.
|
|
Context tmpCtx;
|
|
|
|
m_doc = generate_sprite_sheet(m_exporter, &tmpCtx, m_site, m_params, true, m_token);
|
|
|
|
if (m_doc)
|
|
tmpCtx.documents().remove(m_doc.get());
|
|
}
|
|
|
|
void onMonitoringTick() override
|
|
{
|
|
Job::onMonitoringTick();
|
|
if (isCanceled())
|
|
m_token.cancel();
|
|
else {
|
|
jobProgress(m_token.progress());
|
|
}
|
|
}
|
|
|
|
DocExporter& m_exporter;
|
|
base::task_token m_token;
|
|
const Site& m_site;
|
|
const ExportSpriteSheetParams& m_params;
|
|
std::unique_ptr<Doc> m_doc;
|
|
};
|
|
|
|
} // anonymous namespace
|
|
|
|
ExportSpriteSheetCommand::ExportSpriteSheetCommand(const char* id) : CommandWithNewParams(id)
|
|
{
|
|
}
|
|
|
|
bool ExportSpriteSheetCommand::onEnabled(Context* context)
|
|
{
|
|
return context->checkFlags(ContextFlags::ActiveDocumentIsWritable);
|
|
}
|
|
|
|
void ExportSpriteSheetCommand::onExecute(Context* context)
|
|
{
|
|
Site site = context->activeSite();
|
|
auto& params = this->params();
|
|
DocExporter exporter;
|
|
|
|
Doc* document = site.document();
|
|
DocumentPreferences& docPref(Preferences::instance().document(document));
|
|
|
|
// Show UI if the user specified it explicitly (params.ui=true) or
|
|
// the sprite sheet type wasn't specified.
|
|
const bool showUI = (context->isUIAvailable() && params.ui() &&
|
|
(params.ui.isSet() || !params.type.isSet()));
|
|
|
|
// Copy document preferences to undefined params
|
|
{
|
|
auto& defPref = (docPref.spriteSheet.defined() ? docPref :
|
|
Preferences::instance().document(nullptr));
|
|
if (!params.type.isSet()) {
|
|
params.type(defPref.spriteSheet.type());
|
|
if (!params.columns.isSet())
|
|
params.columns(defPref.spriteSheet.columns());
|
|
if (!params.rows.isSet())
|
|
params.rows(defPref.spriteSheet.rows());
|
|
if (!params.width.isSet())
|
|
params.width(defPref.spriteSheet.width());
|
|
if (!params.height.isSet())
|
|
params.height(defPref.spriteSheet.height());
|
|
if (!params.textureFilename.isSet())
|
|
params.textureFilename(defPref.spriteSheet.textureFilename());
|
|
if (!params.dataFilename.isSet())
|
|
params.dataFilename(defPref.spriteSheet.dataFilename());
|
|
if (!params.dataFormat.isSet())
|
|
params.dataFormat(defPref.spriteSheet.dataFormat());
|
|
if (!params.filenameFormat.isSet())
|
|
params.filenameFormat(defPref.spriteSheet.filenameFormat());
|
|
if (!params.tagnameFormat.isSet())
|
|
params.tagnameFormat(defPref.spriteSheet.tagnameFormat());
|
|
if (!params.borderPadding.isSet())
|
|
params.borderPadding(defPref.spriteSheet.borderPadding());
|
|
if (!params.shapePadding.isSet())
|
|
params.shapePadding(defPref.spriteSheet.shapePadding());
|
|
if (!params.innerPadding.isSet())
|
|
params.innerPadding(defPref.spriteSheet.innerPadding());
|
|
if (!params.trimSprite.isSet())
|
|
params.trimSprite(defPref.spriteSheet.trimSprite());
|
|
if (!params.trim.isSet())
|
|
params.trim(defPref.spriteSheet.trim());
|
|
if (!params.trimByGrid.isSet())
|
|
params.trimByGrid(defPref.spriteSheet.trimByGrid());
|
|
if (!params.extrude.isSet())
|
|
params.extrude(defPref.spriteSheet.extrude());
|
|
if (!params.mergeDuplicates.isSet())
|
|
params.mergeDuplicates(defPref.spriteSheet.mergeDuplicates());
|
|
if (!params.ignoreEmpty.isSet())
|
|
params.ignoreEmpty(defPref.spriteSheet.ignoreEmpty());
|
|
if (!params.openGenerated.isSet())
|
|
params.openGenerated(defPref.spriteSheet.openGenerated());
|
|
if (!params.layer.isSet())
|
|
params.layer(defPref.spriteSheet.layer());
|
|
if (!params.layerIndex.isSet())
|
|
params.layerIndex(defPref.spriteSheet.layerIndex());
|
|
if (!params.tag.isSet())
|
|
params.tag(defPref.spriteSheet.frameTag());
|
|
if (!params.splitLayers.isSet())
|
|
params.splitLayers(defPref.spriteSheet.splitLayers());
|
|
if (!params.splitTags.isSet())
|
|
params.splitTags(defPref.spriteSheet.splitTags());
|
|
if (!params.splitGrid.isSet())
|
|
params.splitGrid(defPref.spriteSheet.splitGrid());
|
|
if (!params.listLayers.isSet())
|
|
params.listLayers(defPref.spriteSheet.listLayers());
|
|
if (!params.listTags.isSet())
|
|
params.listTags(defPref.spriteSheet.listFrameTags());
|
|
if (!params.listSlices.isSet())
|
|
params.listSlices(defPref.spriteSheet.listSlices());
|
|
}
|
|
}
|
|
|
|
bool askOverwrite = params.askOverwrite();
|
|
if (showUI) {
|
|
auto& pref = Preferences::instance();
|
|
|
|
ExportSpriteSheetWindow window(exporter, site, params, pref);
|
|
window.openWindowInForeground();
|
|
|
|
// Save global sprite sheet generation settings anyway (even if
|
|
// the user cancel the dialog, the global settings are stored).
|
|
pref.spriteSheet.preview(window.preview()->isSelected());
|
|
pref.spriteSheet.sections(window.selectedSectionsString());
|
|
|
|
if (!window.ok())
|
|
return;
|
|
|
|
window.updateParams(params);
|
|
docPref.spriteSheet.defined(true);
|
|
docPref.spriteSheet.type(params.type());
|
|
docPref.spriteSheet.columns(params.columns());
|
|
docPref.spriteSheet.rows(params.rows());
|
|
docPref.spriteSheet.width(params.width());
|
|
docPref.spriteSheet.height(params.height());
|
|
docPref.spriteSheet.textureFilename(params.textureFilename());
|
|
docPref.spriteSheet.dataFilename(params.dataFilename());
|
|
docPref.spriteSheet.dataFormat(params.dataFormat());
|
|
docPref.spriteSheet.filenameFormat(params.filenameFormat());
|
|
docPref.spriteSheet.tagnameFormat(params.tagnameFormat());
|
|
docPref.spriteSheet.borderPadding(params.borderPadding());
|
|
docPref.spriteSheet.shapePadding(params.shapePadding());
|
|
docPref.spriteSheet.innerPadding(params.innerPadding());
|
|
docPref.spriteSheet.trimSprite(params.trimSprite());
|
|
docPref.spriteSheet.trim(params.trim());
|
|
docPref.spriteSheet.trimByGrid(params.trimByGrid());
|
|
docPref.spriteSheet.extrude(params.extrude());
|
|
docPref.spriteSheet.mergeDuplicates(params.mergeDuplicates());
|
|
docPref.spriteSheet.ignoreEmpty(params.ignoreEmpty());
|
|
docPref.spriteSheet.openGenerated(params.openGenerated());
|
|
docPref.spriteSheet.layer(params.layer());
|
|
docPref.spriteSheet.layerIndex(params.layerIndex());
|
|
docPref.spriteSheet.frameTag(params.tag());
|
|
docPref.spriteSheet.splitLayers(params.splitLayers());
|
|
docPref.spriteSheet.splitTags(params.splitTags());
|
|
docPref.spriteSheet.splitGrid(params.splitGrid());
|
|
docPref.spriteSheet.listLayers(params.listLayers());
|
|
docPref.spriteSheet.listFrameTags(params.listTags());
|
|
docPref.spriteSheet.listSlices(params.listSlices());
|
|
|
|
// Default preferences for future sprites
|
|
DocumentPreferences& defPref(Preferences::instance().document(nullptr));
|
|
defPref.spriteSheet = docPref.spriteSheet;
|
|
defPref.spriteSheet.defined(false);
|
|
if (!defPref.spriteSheet.textureFilename().empty())
|
|
defPref.spriteSheet.textureFilename.setValueAndDefault(kSpecifiedFilename);
|
|
if (!defPref.spriteSheet.dataFilename().empty())
|
|
defPref.spriteSheet.dataFilename.setValueAndDefault(kSpecifiedFilename);
|
|
defPref.save();
|
|
|
|
askOverwrite = false; // Already asked in the ExportSpriteSheetWindow
|
|
}
|
|
|
|
if (context->isUIAvailable() && askOverwrite) {
|
|
if (!ask_overwrite(true, params.textureFilename(), true, params.dataFilename()))
|
|
return; // Do not overwrite
|
|
}
|
|
|
|
exporter.setDocImageBuffer(std::make_shared<doc::ImageBuffer>());
|
|
std::unique_ptr<Doc> newDocument;
|
|
if (context->isUIAvailable()) {
|
|
ExportSpriteSheetJob job(exporter,
|
|
site,
|
|
params,
|
|
// Progress bar can be disabled with ui=false
|
|
params.ui());
|
|
job.startJob();
|
|
job.waitJob();
|
|
|
|
newDocument = job.releaseDoc();
|
|
if (!newDocument)
|
|
return;
|
|
|
|
StatusBar* statusbar = StatusBar::instance();
|
|
if (statusbar)
|
|
statusbar->showTip(1000, Strings::export_sprite_sheet_generated());
|
|
|
|
// Save the exported sprite sheet as a recent file
|
|
if (newDocument->isAssociatedToFile() && should_add_file_to_recents(context, params)) {
|
|
App::instance()->recentFiles()->addRecentFile(newDocument->filename());
|
|
}
|
|
|
|
// Copy background and grid preferences
|
|
DocumentPreferences& newDocPref(Preferences::instance().document(newDocument.get()));
|
|
newDocPref.bg = docPref.bg;
|
|
newDocPref.grid = docPref.grid;
|
|
newDocPref.pixelGrid = docPref.pixelGrid;
|
|
Preferences::instance().removeDocument(newDocument.get());
|
|
}
|
|
else {
|
|
base::task_token token;
|
|
newDocument = generate_sprite_sheet(exporter, context, site, params, true, token);
|
|
if (!newDocument)
|
|
return;
|
|
}
|
|
|
|
ASSERT(newDocument);
|
|
|
|
if (params.openGenerated()) {
|
|
newDocument->setContext(context);
|
|
newDocument.release();
|
|
}
|
|
else {
|
|
destroy_doc(context, newDocument.release());
|
|
}
|
|
}
|
|
|
|
Command* CommandFactory::createExportSpriteSheetCommand()
|
|
{
|
|
return new ExportSpriteSheetCommand;
|
|
}
|
|
|
|
} // namespace app
|