aseprite/src/app/commands/cmd_export_sprite_sheet.cpp

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