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="base_index" type="int" default="1" />
<option id="cache_compressed_tilesets" type="bool" default="true" /> <option id="cache_compressed_tilesets" type="bool" default="true" />
</section> </section>
<section id="tilemap">
<option id="show_delete_unused_tileset_alert" type="bool" default="true" />
</section>
</global> </global>
<tool> <tool>

View File

@ -55,6 +55,13 @@ Automatic Remap
||&OK||&Cancel" ||&OK||&Cancel"
END END
cannot_delete_all_layers = Error<<You cannot delete all layers.||&OK 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 cannot_file_overwrite_on_export = <<<END
Overwrite Warning Overwrite Warning
<<You cannot Export with the same name (overwrite the original file). <<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 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_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 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: image_format_alerts = Show options when saving files:
advanced_mode_alert = Show alert when we enter to Advanced Mode 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 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" /> pref="export_file.show_overwrite_files_alert" />
<check id="overwrite_files_on_export_sprite_sheet_alert" text="@.overwrite_files_on_export_sprite_sheet_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" /> 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" <check id="advanced_mode_alert" text="@.advanced_mode_alert"
pref="advanced_mode.show_alert" /> pref="advanced_mode.show_alert" />
<check id="invalid_fg_bg_color_alert" text="@.invalid_fg_bg_color_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) if (m_tilesetIndex == -1)
m_tilesetIndex = sprite->tilesets()->add(tileset); m_tilesetIndex = sprite->tilesets()->add(tileset);
else else
sprite->tilesets()->set(m_tilesetIndex, tileset); sprite->tilesets()->add(m_tilesetIndex, tileset);
sprite->incrementVersion(); sprite->incrementVersion();
sprite->tilesets()->incrementVersion(); sprite->tilesets()->incrementVersion();

View File

@ -11,20 +11,77 @@
#include "app/app.h" #include "app/app.h"
#include "app/commands/command.h" #include "app/commands/command.h"
#include "app/cmd/remove_tileset.h"
#include "app/context_access.h" #include "app/context_access.h"
#include "app/doc_api.h" #include "app/doc_api.h"
#include "app/i18n/strings.h" #include "app/i18n/strings.h"
#include "app/modules/gui.h" #include "app/modules/gui.h"
#include "app/tx.h" #include "app/tx.h"
#include "app/pref/preferences.h"
#include "app/ui/optional_alert.h"
#include "app/ui/status_bar.h" #include "app/ui/status_bar.h"
#include "doc/layer.h" #include "doc/layer.h"
#include "doc/layer_tilemap.h"
#include "doc/sprite.h" #include "doc/sprite.h"
#include "doc/tilesets.h"
#include "fmt/format.h" #include "fmt/format.h"
#include "ui/alert.h" #include "ui/alert.h"
#include "ui/widget.h" #include "ui/widget.h"
namespace app { 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 { class RemoveLayerCommand : public Command {
public: public:
RemoveLayerCommand(); RemoveLayerCommand();
@ -55,6 +112,11 @@ void RemoveLayerCommand::onExecute(Context* context)
{ {
Tx tx(writer.context(), "Remove Layer"); Tx tx(writer.context(), "Remove Layer");
DocApi api = document->getApi(tx); 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(); const Site* site = writer.site();
if (site->inTimeline() && if (site->inTimeline() &&
@ -73,6 +135,10 @@ void RemoveLayerCommand::onExecute(Context* context)
return; return;
} }
if (!continue_deleting_unused_tilesets(context, sprite, selLayers.toAllTilemaps(), tsiToDelete)) {
return;
}
for (Layer* layer : selLayers) { for (Layer* layer : selLayers) {
api.removeLayer(layer); api.removeLayer(layer);
} }
@ -84,10 +150,20 @@ void RemoveLayerCommand::onExecute(Context* context)
} }
Layer* layer = writer.layer(); Layer* layer = writer.layer();
if (layer->isTilemap() && !continue_deleting_unused_tilesets(context, sprite, {layer}, tsiToDelete)) {
return;
}
layerName = layer->name(); layerName = layer->name();
api.removeLayer(layer); api.removeLayer(layer);
} }
if (!tsiToDelete.empty()) {
for (tileset_index tsi : tsiToDelete) {
tx(new cmd::RemoveTileset(sprite, tsi));
}
}
tx.commit(); 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 void LayerGroup::getCels(CelList& cels) const
{ {
for (const Layer* layer : m_layers) for (const Layer* layer : m_layers)

View File

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

View File

@ -99,6 +99,24 @@ LayerList SelectedLayers::toBrowsableLayerList() const
return output; 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() void SelectedLayers::removeChildrenIfParentIsSelected()
{ {
SelectedLayers removeThese; SelectedLayers removeThese;

View File

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

View File

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

View File

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

View File

@ -10,6 +10,9 @@
#include "doc/tileset.h" #include "doc/tileset.h"
#include "doc/tilesets.h"
#include "doc/layer.h"
#include "doc/layer_tilemap.h"
#include "base/mem_utils.h" #include "base/mem_utils.h"
#include "doc/primitives.h" #include "doc/primitives.h"
#include "doc/remap.h" #include "doc/remap.h"
@ -406,4 +409,15 @@ TilesetHashTable& Tileset::hashTable()
return m_hash; 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 } // namespace doc

View File

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

View File

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