Delete unused tilesets after deleting tilemaps (fix #3876)

This commit is contained in:
Martín Capello 2023-06-07 10:37:39 -03:00 committed by David Capello
parent 872267dc9b
commit 5bc432f289
15 changed files with 287 additions and 9 deletions

View File

@ -429,6 +429,9 @@
<option id="base_index" type="int" default="1" />
<option id="cache_compressed_tilesets" type="bool" default="true" />
</section>
<section id="tilemap">
<option id="show_delete_unused_tileset_alert" type="bool" default="true" />
</section>
</global>
<tool>

View File

@ -55,6 +55,13 @@ Automatic Remap
||&OK||&Cancel"
END
cannot_delete_all_layers = Error<<You cannot delete all layers.||&OK
deleting_tilemaps_will_delete_tilesets = <<<END
Warning
<<Deleting the following layers will delete their tilesets:
<<'{0}'
<<Do you want to continue anyway?
||&OK||&Cancel
END
cannot_file_overwrite_on_export = <<<END
Overwrite Warning
<<You cannot Export with the same name (overwrite the original file).
@ -1698,6 +1705,7 @@ file_format_doesnt_support_alert = Show warning when saving a file with unsuppor
export_animation_in_sequence_alert = Show warning when saving an animation as a sequence of static images
overwrite_files_on_export_alert = Show warning when overwriting files on File > Export
overwrite_files_on_export_sprite_sheet_alert = Show warning when overwriting files on Export Sprite Sheet
delete_tilemap_delete_unused_tileset_alert = Show warning when deleting a tilemap will delete its unused tileset
image_format_alerts = Show options when saving files:
advanced_mode_alert = Show alert when we enter to Advanced Mode
invalid_fg_bg_color_alert = Show alert when drawing with index out of palette bounds

View File

@ -458,6 +458,8 @@
pref="export_file.show_overwrite_files_alert" />
<check id="overwrite_files_on_export_sprite_sheet_alert" text="@.overwrite_files_on_export_sprite_sheet_alert"
pref="sprite_sheet.show_overwrite_files_alert" />
<check id="delete_tilemap_delete_unused_tileset_alert" text="@.delete_tilemap_delete_unused_tileset_alert"
pref="tilemap.show_delete_unused_tileset_alert" />
<check id="advanced_mode_alert" text="@.advanced_mode_alert"
pref="advanced_mode.show_alert" />
<check id="invalid_fg_bg_color_alert" text="@.invalid_fg_bg_color_alert"

View File

@ -78,7 +78,7 @@ void AddTileset::addTileset(doc::Tileset* tileset)
if (m_tilesetIndex == -1)
m_tilesetIndex = sprite->tilesets()->add(tileset);
else
sprite->tilesets()->set(m_tilesetIndex, tileset);
sprite->tilesets()->add(m_tilesetIndex, tileset);
sprite->incrementVersion();
sprite->tilesets()->incrementVersion();

View File

@ -11,20 +11,77 @@
#include "app/app.h"
#include "app/commands/command.h"
#include "app/cmd/remove_tileset.h"
#include "app/context_access.h"
#include "app/doc_api.h"
#include "app/i18n/strings.h"
#include "app/modules/gui.h"
#include "app/tx.h"
#include "app/pref/preferences.h"
#include "app/ui/optional_alert.h"
#include "app/ui/status_bar.h"
#include "doc/layer.h"
#include "doc/layer_tilemap.h"
#include "doc/sprite.h"
#include "doc/tilesets.h"
#include "fmt/format.h"
#include "ui/alert.h"
#include "ui/widget.h"
namespace app {
// Calculates the list of unused tileset indexes (returned in tsiToDelete parameter)
// once the layers specified are removed.
// Also, if the UI is available, shows a warning message about the deletion of unused
// tilesets.
// This function returns true in any of the following:
// - There won't be deletion of tilesets, this means tsiToDelete is empty.
// - The user accepts continuing despite the warning.
// - There is no UI available.
static bool continue_deleting_unused_tilesets(
Context* ctx, Sprite* sprite, const LayerList layers,
std::set<tileset_index, std::greater<tileset_index>>& tsiToDelete)
{
std::vector<LayerTilemap*> tilemaps;
std::map<doc::tileset_index, int> timesSelected;
std::string layerNames;
for (auto layer : layers) {
if (layer->isTilemap()) {
auto tilemap = static_cast<LayerTilemap*>(layer);
timesSelected[tilemap->tilesetIndex()]++;
tilemaps.push_back(tilemap);
}
}
for (auto tilemap : tilemaps) {
auto ts = sprite->tilesets()->get(tilemap->tilesetIndex());
if (ts->tilemapsCount() == timesSelected[tilemap->tilesetIndex()]) {
tsiToDelete.insert(tilemap->tilesetIndex());
layerNames += tilemap->name() + ", ";
}
}
#ifdef ENABLE_UI
// Just continue if UI is not available.
if (!ctx->isUIAvailable())
return true;
// Remove last ", "
if (!layerNames.empty()) {
layerNames = layerNames.substr(0, layerNames.length() - 2);
}
std::string message;
if (tsiToDelete.size() >= 1)
message = fmt::format(Strings::alerts_deleting_tilemaps_will_delete_tilesets(), layerNames);
return tsiToDelete.empty() ||
app::OptionalAlert::show(
Preferences::instance().tilemap.showDeleteUnusedTilesetAlert, 1, message) == 1;
#else
return true;
#endif
}
class RemoveLayerCommand : public Command {
public:
RemoveLayerCommand();
@ -55,6 +112,11 @@ void RemoveLayerCommand::onExecute(Context* context)
{
Tx tx(writer.context(), "Remove Layer");
DocApi api = document->getApi(tx);
// We need to remove all the tilesets after the tilemaps are deleted
// and in descending tileset index order, otherwise the tileset indexes
// get mixed up. This is the reason we use a tileset_index set with
// the std::greater Compare.
std::set<tileset_index, std::greater<tileset_index>> tsiToDelete;
const Site* site = writer.site();
if (site->inTimeline() &&
@ -73,6 +135,10 @@ void RemoveLayerCommand::onExecute(Context* context)
return;
}
if (!continue_deleting_unused_tilesets(context, sprite, selLayers.toAllTilemaps(), tsiToDelete)) {
return;
}
for (Layer* layer : selLayers) {
api.removeLayer(layer);
}
@ -84,10 +150,20 @@ void RemoveLayerCommand::onExecute(Context* context)
}
Layer* layer = writer.layer();
if (layer->isTilemap() && !continue_deleting_unused_tilesets(context, sprite, {layer}, tsiToDelete)) {
return;
}
layerName = layer->name();
api.removeLayer(layer);
}
if (!tsiToDelete.empty()) {
for (tileset_index tsi : tsiToDelete) {
tx(new cmd::RemoveTileset(sprite, tsi));
}
}
tx.commit();
}

View File

@ -529,6 +529,17 @@ void LayerGroup::allBrowsableLayers(LayerList& list) const
}
}
void LayerGroup::allTilemaps(LayerList& list) const
{
for (Layer* child : m_layers) {
if (child->isGroup())
static_cast<LayerGroup*>(child)->allTilemaps(list);
if (child->isTilemap())
list.push_back(child);
}
}
void LayerGroup::getCels(CelList& cels) const
{
for (const Layer* layer : m_layers)

View File

@ -211,6 +211,7 @@ namespace doc {
void allVisibleLayers(LayerList& list) const;
void allVisibleReferenceLayers(LayerList& list) const;
void allBrowsableLayers(LayerList& list) const;
void allTilemaps(LayerList& list) const;
void getCels(CelList& cels) const override;
void displaceFrames(frame_t fromThis, frame_t delta) override;

View File

@ -99,6 +99,24 @@ LayerList SelectedLayers::toBrowsableLayerList() const
return output;
}
LayerList SelectedLayers::toAllTilemaps() const
{
LayerList output;
if (empty())
return output;
for (Layer* layer : *this) {
if (layer->isGroup()) {
auto group = static_cast<LayerGroup*>(layer);
group->allTilemaps(output);
} else if (layer->isTilemap())
output.push_back(layer);
}
return output;
}
void SelectedLayers::removeChildrenIfParentIsSelected()
{
SelectedLayers removeThese;

View File

@ -41,6 +41,7 @@ namespace doc {
bool hasSameParent() const;
LayerList toBrowsableLayerList() const;
LayerList toAllLayersList() const;
LayerList toAllTilemaps() const;
void removeChildrenIfParentIsSelected();
void expandCollapsedGroups();

View File

@ -736,6 +736,13 @@ LayerList Sprite::allBrowsableLayers() const
return list;
}
LayerList Sprite::allTilemaps() const
{
LayerList list;
m_root->allTilemaps(list);
return list;
}
CelsRange Sprite::cels() const
{
SelectedFrames selFrames;

View File

@ -209,6 +209,7 @@ namespace doc {
LayerList allVisibleLayers() const;
LayerList allVisibleReferenceLayers() const;
LayerList allBrowsableLayers() const;
LayerList allTilemaps() const;
CelsRange cels() const;
CelsRange cels(frame_t frame) const;

View File

@ -10,6 +10,9 @@
#include "doc/tileset.h"
#include "doc/tilesets.h"
#include "doc/layer.h"
#include "doc/layer_tilemap.h"
#include "base/mem_utils.h"
#include "doc/primitives.h"
#include "doc/remap.h"
@ -406,4 +409,15 @@ TilesetHashTable& Tileset::hashTable()
return m_hash;
}
int Tileset::tilemapsCount() const {
auto tsi = sprite()->tilesets()->getIndex(this);
int count = 0;
for (auto layer : sprite()->allLayers()) {
if (layer->isTilemap() && static_cast<LayerTilemap*>(layer)->tilesetIndex() == tsi) {
count++;
}
}
return count;
}
} // namespace doc

View File

@ -131,6 +131,9 @@ namespace doc {
// have to regenerate the empty tile with that new mask color.
void notifyRegenerateEmptyTile();
// Returns the number of tilemap layers that are referencing this tileset.
int tilemapsCount() const;
#ifdef _DEBUG
void assertValidHashTable();
#endif

View File

@ -8,6 +8,8 @@
#define DOC_TILESETS_H_INCLUDED
#pragma once
#include "doc/layer_tilemap.h"
#include "doc/sprite.h"
#include "doc/tileset.h"
#include <vector>
@ -40,7 +42,7 @@ namespace doc {
return nullptr;
}
tileset_index getIndex(Tileset *tileset) {
tileset_index getIndex(const Tileset *tileset) {
for (tileset_index i = 0; i < size(); ++i) {
if (m_tilesets[i] == tileset) {
return i;
@ -55,23 +57,44 @@ namespace doc {
m_tilesets[tsi] = tileset;
}
void add(const tileset_index tsi, Tileset* tileset) {
if (tsi >= m_tilesets.size()) {
m_tilesets.push_back(tileset);
}
else {
m_tilesets.insert(m_tilesets.begin()+tsi, tileset);
// Update tileset indexes of the affected tilemaps. We have to shift the indexes
// for all the tilemaps pointing to a tileset index equals or greater than the added one.
shiftTilesetIndexes(tileset->sprite(), tsi, 1);
}
}
void erase(const tileset_index tsi) {
// Do not m_tilesets.erase() the tileset so other tilesets
// indexes/IDs are kept intact.
// When tsi is the last one, other tilemaps tilesets
// indexes are not affected.
if (tsi == size()-1) {
m_tilesets.erase(--m_tilesets.end());
}
else {
// TODO Should we keep the empty slot? Or should we update all
// indexes (even from external files?). Having a nullptr
// tileset in the sprite adds a lot of complexity (each
// for-loop must check the tileset)
m_tilesets[tsi] = nullptr;
auto ts = m_tilesets[tsi];
m_tilesets.erase(m_tilesets.begin()+tsi);
// Update tileset indexes of the affected tilemaps. We have to shift the indexes
// for all the tilemaps pointing to a tileset index greater than the deleted one.
shiftTilesetIndexes(ts->sprite(), tsi+1, -1);
}
}
private:
Array m_tilesets;
void shiftTilesetIndexes(Sprite *sprite, tileset_index pos, int n) {
for (auto layer : sprite->allTilemaps()) {
auto tilemap = static_cast<LayerTilemap*>(layer);
if (tilemap->tilesetIndex() >= pos) {
tilemap->setTilesetIndex(tilemap->tilesetIndex()+n);
}
}
}
};
} // namespace doc

View File

@ -1117,3 +1117,113 @@ do
expect_eq(app.activeLayer.cels[1].image.width, 1) -- width in tilemap terms
expect_eq(app.activeLayer.cels[1].image.height, 2) -- height in tilemap terms
end
----------------------------------------------------------------------
-- Tests removal of unused tilesets when deleting tilemaps
----------------------------------------------------------------------
do
local spr = Sprite(32, 32, ColorMode.INDEXED)
assert(spr.layers[1].isImage)
assert(not spr.layers[1].isTilemap)
-- Create some tilemaps
app.command.NewLayer{ tilemap=true }
assert(#spr.layers == 2)
local tilemapLay1 = spr.layers[2]
assert(tilemapLay1.isImage)
assert(tilemapLay1.isTilemap)
assert(#spr.tilesets == 1)
assert(spr.tilesets[1] == tilemapLay1.tileset)
app.command.NewLayer{ tilemap=true }
assert(#spr.layers == 3)
local tilemapLay2 = spr.layers[3]
assert(tilemapLay2.isImage)
assert(tilemapLay2.isTilemap)
assert(#spr.tilesets == 2)
assert(spr.tilesets[2] == tilemapLay2.tileset)
app.command.NewLayer{ tilemap=true }
assert(#spr.layers == 4)
local tilemapLay3 = spr.layers[4]
assert(tilemapLay3.isImage)
assert(tilemapLay3.isTilemap)
assert(#spr.tilesets == 3)
assert(spr.tilesets[3] == tilemapLay3.tileset)
-- Remove tilemap 2 and check that a tilemap was removed and
-- tilesets of remaining tilemaps are correct.
app.range.layers = { tilemapLay2 }
app.command.RemoveLayer()
assert(#spr.layers == 3)
assert(#spr.tilesets == 2)
assert(spr.tilesets[1] == tilemapLay1.tileset)
assert(spr.tilesets[2] == tilemapLay3.tileset)
-- Undo tilemap removal and check that it goes back to
-- previous state.
app.undo()
assert(#spr.layers == 4)
assert(#spr.tilesets == 3)
assert(spr.tilesets[1] == tilemapLay1.tileset)
assert(spr.tilesets[2] == tilemapLay2.tileset)
assert(spr.tilesets[3] == tilemapLay3.tileset)
-- Try removing 2 tilemaps now
app.range.layers = { tilemapLay1, tilemapLay2 }
app.command.RemoveLayer()
assert(#spr.layers == 2)
assert(#spr.tilesets == 1)
assert(spr.tilesets[1] == tilemapLay3.tileset)
app.undo()
assert(#spr.layers == 4)
assert(#spr.tilesets == 3)
assert(spr.tilesets[1] == tilemapLay1.tileset)
assert(spr.tilesets[2] == tilemapLay2.tileset)
assert(spr.tilesets[3] == tilemapLay3.tileset)
-- Assign same tileset to tilemap 1 and tilemap 3.
local oldTilemapLay3Tileset = tilemapLay3.tileset
tilemapLay3.tileset = tilemapLay1.tileset
-- We have to manually delete tilemap 3 tileset because
-- assigning a different tileset doesn't check for/remove
-- unused tilesets (TODO: should we add this?)
spr:deleteTileset(oldTilemapLay3Tileset)
assert(#spr.tilesets == 2)
assert(spr.tilesets[1] == tilemapLay1.tileset)
assert(spr.tilesets[2] == tilemapLay2.tileset)
assert(spr.tilesets[1] == tilemapLay3.tileset)
-- Remove tilemap 1 and check that no tileset was removed.
app.range.layers = { tilemapLay1 }
app.command.RemoveLayer()
assert(#spr.layers == 3)
assert(#spr.tilesets == 2)
assert(spr.tilesets[2] == tilemapLay2.tileset)
assert(spr.tilesets[1] == tilemapLay3.tileset)
-- Remove tilemap 3 and check that the tileset was removed now.
app.range.layers = { tilemapLay3 }
app.command.RemoveLayer()
assert(#spr.layers == 2)
assert(#spr.tilesets == 1)
assert(spr.tilesets[1] == tilemapLay2.tileset)
-- Undo all
app.undo()
app.undo()
app.undo()
-- Manually re-assign its tileset to tilemap 3.
tilemapLay3.tileset = spr.tilesets[3]
assert(#spr.layers == 4)
assert(#spr.tilesets == 3)
assert(spr.tilesets[1] == tilemapLay1.tileset)
assert(spr.tilesets[2] == tilemapLay2.tileset)
assert(spr.tilesets[3] == tilemapLay3.tileset)
end