From 44437e0d1f2285f82ae76ec7da806a738f4645d1 Mon Sep 17 00:00:00 2001
From: David Capello <david@igarastudio.com>
Date: Tue, 23 Feb 2021 11:32:08 -0300
Subject: [PATCH] Add new ConvertLayer command ("Layer > Convert To" menu item)

Now we have an easy way to convert between:
  Background <-> Layers <-> Tilemaps

Deprecated (they are kept only for backward compatibility):
  BackgroundFromLayer
  LayerFromBackground
---
 data/gui.xml                       |  28 +++-
 data/strings/en.ini                |  11 +-
 src/app/CMakeLists.txt             |   3 +-
 src/app/commands/commands_list.h   |   3 +-
 src/app/commands/convert_layer.cpp | 258 +++++++++++++++++++++++++++++
 src/app/ui/app_menuitem.cpp        |  10 +-
 6 files changed, 299 insertions(+), 14 deletions(-)
 create mode 100644 src/app/commands/convert_layer.cpp

diff --git a/data/gui.xml b/data/gui.xml
index 5fcfbc638..3f738b245 100644
--- a/data/gui.xml
+++ b/data/gui.xml
@@ -840,9 +840,17 @@
           </item>
         </menu>
         <item command="RemoveLayer" text="@.layer_delete_layer" group="layer_remove" />
-	<menu text="@.layer_convert">
-          <item command="BackgroundFromLayer" text="@.layer_background_from_layer" />
-          <item command="LayerFromBackground" text="@.layer_layer_from_background" group="layer_background" />
+        <menu text="@.layer_convert_to">
+          <item command="ConvertLayer" text="@.layer_convert_to_background">
+            <param name="to" value="background" />
+          </item>
+          <item command="ConvertLayer" text="@.layer_convert_to_layer">
+            <param name="to" value="layer" />
+          </item>
+          <separator />
+          <item command="ConvertLayer" text="@.layer_convert_to_tilemap">
+            <param name="to" value="tilemap" />
+          </item>
         </menu>
         <separator />
         <item command="DuplicateLayer" text="@.layer_duplicate" group="layer_duplicate" />
@@ -1018,9 +1026,17 @@
         <param name="group" value="true" />
       </item>
       <item command="RemoveLayer" text="@main_menu.layer_delete_layer" />
-      <menu text="@main_menu.layer_convert">
-        <item command="BackgroundFromLayer" text="@main_menu.layer_background_from_layer" />
-        <item command="LayerFromBackground" text="@main_menu.layer_layer_from_background" group="layer_popup_background" />
+      <menu text="@main_menu.layer_convert_to">
+        <item command="ConvertLayer" text="@main_menu.layer_convert_to_background">
+          <param name="to" value="background" />
+        </item>
+        <item command="ConvertLayer" text="@main_menu.layer_convert_to_layer">
+          <param name="to" value="layer" />
+        </item>
+        <separator />
+        <item command="ConvertLayer" text="@main_menu.layer_convert_to_tilemap">
+          <param name="to" value="tilemap" />
+        </item>
       </menu>
       <separator />
       <item command="DuplicateLayer" text="@main_menu.layer_duplicate" />
diff --git a/data/strings/en.ini b/data/strings/en.ini
index d5c182551..00e7039d4 100644
--- a/data/strings/en.ini
+++ b/data/strings/en.ini
@@ -273,6 +273,10 @@ ClearCel = Clear Cel
 ClearRecentFiles = Clear Recent Files
 CloseAllFiles = Close All Files
 CloseFile = Close File
+ConvertLayer = Convert Layer
+ConvertLayer_Background = Convert to Background
+ConvertLayer_Layer = Convert to Transparent Layer
+ConvertLayer_Tilemap = Convert to Tilemap
 ColorCurve = Color Curve
 ColorQuantization = Create Palette from Current Sprite (Color Quantization)
 ContiguousFill = Switch Contiguous Fill
@@ -925,9 +929,10 @@ layer_new_layer_via_cut = New Layer via Cu&t
 layer_new_reference_layer_from_file = New &Reference Layer from File
 layer_new_tilemap_layer = New Tilemap Layer
 layer_delete_layer = Delete Laye&r
-layer_convert = Conv&ert
-layer_background_from_layer = &Background from Layer
-layer_layer_from_background = &Layer from Background
+layer_convert_to = Conv&ert To...
+layer_convert_to_background = &Background
+layer_convert_to_layer = &Layer
+layer_convert_to_tilemap = &Tilemap
 layer_duplicate = &Duplicate
 layer_merge_down = &Merge Down
 layer_flatten = &Flatten
diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt
index 33d981ad2..a97dba6e5 100644
--- a/src/app/CMakeLists.txt
+++ b/src/app/CMakeLists.txt
@@ -1,5 +1,5 @@
 # Aseprite
-# Copyright (C) 2018-2020  Igara Studio S.A.
+# Copyright (C) 2018-2021  Igara Studio S.A.
 # Copyright (C) 2001-2018  David Capello
 
 # Generate a ui::Widget for each widget in a XML file
@@ -535,6 +535,7 @@ add_library(app-lib
   commands/cmd_undo.cpp
   commands/command.cpp
   commands/commands.cpp
+  commands/convert_layer.cpp
   commands/filters/cmd_brightness_contrast.cpp
   commands/filters/cmd_color_curve.cpp
   commands/filters/cmd_convolution_matrix.cpp
diff --git a/src/app/commands/commands_list.h b/src/app/commands/commands_list.h
index 8e6556ea4..7725c4769 100644
--- a/src/app/commands/commands_list.h
+++ b/src/app/commands/commands_list.h
@@ -1,5 +1,5 @@
 // Aseprite
-// Copyright (C) 2018-2020  Igara Studio S.A.
+// Copyright (C) 2018-2021  Igara Studio S.A.
 // Copyright (C) 2001-2018  David Capello
 //
 // This program is distributed under the terms of
@@ -14,6 +14,7 @@ FOR_EACH_COMMAND(CelOpacity)
 FOR_EACH_COMMAND(ChangePixelFormat)
 FOR_EACH_COMMAND(ColorCurve)
 FOR_EACH_COMMAND(ColorQuantization)
+FOR_EACH_COMMAND(ConvertLayer)
 FOR_EACH_COMMAND(ConvolutionMatrix)
 FOR_EACH_COMMAND(CopyColors)
 FOR_EACH_COMMAND(CopyTiles)
diff --git a/src/app/commands/convert_layer.cpp b/src/app/commands/convert_layer.cpp
new file mode 100644
index 000000000..922a8ede0
--- /dev/null
+++ b/src/app/commands/convert_layer.cpp
@@ -0,0 +1,258 @@
+// Aseprite
+// Copyright (C) 2021  Igara Studio S.A.
+//
+// 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/cmd/add_cel.h"
+#include "app/cmd/add_layer.h"
+#include "app/cmd/add_tileset.h"
+#include "app/cmd/background_from_layer.h"
+#include "app/cmd/copy_cel.h"
+#include "app/cmd/layer_from_background.h"
+#include "app/cmd/remove_layer.h"
+#include "app/commands/command.h"
+#include "app/commands/new_params.h"
+#include "app/context_access.h"
+#include "app/i18n/strings.h"
+#include "app/modules/gui.h"
+#include "app/tx.h"
+#include "app/util/cel_ops.h"
+#include "doc/grid.h"
+#include "doc/layer.h"
+#include "doc/layer_tilemap.h"
+#include "doc/tileset.h"
+
+#ifdef ENABLE_SCRIPTING
+#include "app/script/luacpp.h"
+#endif
+
+#include <map>
+
+namespace app {
+
+enum class ConvertLayerParam { None, Background, Layer, Tilemap };
+
+template<>
+void Param<ConvertLayerParam>::fromString(const std::string& value)
+{
+  if (value == "background")
+    setValue(ConvertLayerParam::Background);
+  else if (value == "layer")
+    setValue(ConvertLayerParam::Layer);
+  else if (value == "tilemap")
+    setValue(ConvertLayerParam::Tilemap);
+  else
+    setValue(ConvertLayerParam::None);
+}
+
+#ifdef ENABLE_SCRIPTING
+template<>
+void Param<ConvertLayerParam>::fromLua(lua_State* L, int index)
+{
+  fromString(lua_tostring(L, index));
+}
+#endif // ENABLE_SCRIPTING
+
+struct ConvertLayerParams : public NewParams {
+  Param<ConvertLayerParam> to { this, ConvertLayerParam::None, "to" };
+};
+
+class ConvertLayerCommand : public CommandWithNewParams<ConvertLayerParams> {
+public:
+  ConvertLayerCommand();
+
+private:
+  bool onEnabled(Context* context) override;
+  void onExecute(Context* context) override;
+  std::string onGetFriendlyName() const override;
+
+  void copyCels(Tx& tx,
+                Layer* srcLayer,
+                Layer* newLayer);
+};
+
+ConvertLayerCommand::ConvertLayerCommand()
+  : CommandWithNewParams<ConvertLayerParams>(CommandId::ConvertLayer(), CmdRecordableFlag)
+{
+}
+
+bool ConvertLayerCommand::onEnabled(Context* ctx)
+{
+  if (!ctx->checkFlags(ContextFlags::ActiveDocumentIsWritable |
+                       ContextFlags::HasActiveSprite |
+                       ContextFlags::HasActiveLayer |
+                       ContextFlags::ActiveLayerIsVisible |
+                       ContextFlags::ActiveLayerIsEditable))
+    return false;
+
+  // TODO add support to convert reference layers into regular layers or tilemaps
+  if (ctx->checkFlags(ContextFlags::ActiveLayerIsReference))
+    return false;
+
+  switch (params().to()) {
+
+    case ConvertLayerParam::Background:
+      return
+        // Doesn't have a background layer
+        !ctx->checkFlags(ContextFlags::HasBackgroundLayer) &&
+        // Convert a regular layer or tilemap into background
+        ctx->checkFlags(ContextFlags::ActiveLayerIsImage) &&
+        // TODO add support for background tliemaps
+        !ctx->checkFlags(ContextFlags::ActiveLayerIsTilemap);
+
+    case ConvertLayerParam::Layer:
+      return
+        // Convert a background layer into a transparent layer
+        ctx->checkFlags(ContextFlags::ActiveLayerIsImage |
+                        ContextFlags::ActiveLayerIsBackground) ||
+        // or a tilemap into a regular layer
+        ctx->checkFlags(ContextFlags::ActiveLayerIsTilemap);
+
+    case ConvertLayerParam::Tilemap:
+      return
+        ctx->checkFlags(ContextFlags::ActiveLayerIsImage) &&
+        !ctx->checkFlags(ContextFlags::ActiveLayerIsTilemap) &&
+        // TODO add support for background tliemaps
+        !ctx->checkFlags(ContextFlags::ActiveLayerIsBackground);
+
+    default:
+      return false;
+  }
+}
+
+void ConvertLayerCommand::onExecute(Context* ctx)
+{
+  ContextWriter writer(ctx);
+  Doc* document(writer.document());
+  {
+    Tx tx(ctx, friendlyName());
+    Site site = ctx->activeSite();
+    Sprite* sprite = site.sprite();
+    Layer* srcLayer = site.layer();
+
+    switch (params().to()) {
+
+      case ConvertLayerParam::Background:
+        // Layer -> Background
+        if (srcLayer->isTransparent()) {
+          ASSERT(srcLayer->isImage());
+          tx(new cmd::BackgroundFromLayer(static_cast<LayerImage*>(srcLayer)));
+        }
+        // Tilemap -> Background
+        else if (srcLayer->isTilemap()) {
+          auto newLayer = new LayerImage(sprite);
+          newLayer->configureAsBackground();
+          newLayer->setName(Strings::commands_NewFile_BackgroundLayer());
+          newLayer->setContinuous(srcLayer->isContinuous());
+          tx(new cmd::AddLayer(srcLayer->parent(), newLayer, srcLayer));
+
+          CelList srcCels;
+          srcLayer->getCels(srcCels);
+          for (Cel* srcCel : srcCels)
+            create_cel_copy(tx, srcCel, sprite, newLayer, srcCel->frame());
+
+          tx(new cmd::RemoveLayer(srcLayer));
+        }
+        break;
+
+      case ConvertLayerParam::Layer:
+        // Background -> Layer
+        if (srcLayer->isBackground()) {
+          tx(new cmd::LayerFromBackground(srcLayer));
+        }
+        // Background -> Tilemap
+        else if (srcLayer->isTilemap()) {
+          auto newLayer = new LayerImage(sprite);
+          newLayer->setName(srcLayer->name());
+          newLayer->setContinuous(srcLayer->isContinuous());
+          tx(new cmd::AddLayer(srcLayer->parent(), newLayer, srcLayer));
+
+          copyCels(tx, srcLayer, newLayer);
+
+          tx(new cmd::RemoveLayer(srcLayer));
+        }
+        break;
+
+      case ConvertLayerParam::Tilemap:
+        // Background or Transparent Layer -> Tilemap
+        if (srcLayer->isImage() &&
+            (srcLayer->isBackground() ||
+             srcLayer->isTransparent())) {
+          auto tileset = new Tileset(sprite, site.grid(), 1);
+
+          auto addTileset = new cmd::AddTileset(sprite, tileset);
+          tx(addTileset);
+          tileset_index tsi = addTileset->tilesetIndex();
+
+          auto newLayer = new LayerTilemap(sprite, tsi);
+          newLayer->setName(srcLayer->name());
+          newLayer->setContinuous(srcLayer->isContinuous());
+          tx(new cmd::AddLayer(srcLayer->parent(), newLayer, srcLayer));
+
+          copyCels(tx, srcLayer, newLayer);
+
+          tx(new cmd::RemoveLayer(srcLayer));
+        }
+        break;
+    }
+
+    tx.commit();
+  }
+
+#ifdef ENABLE_UI
+  if (ctx->isUIAvailable())
+    update_screen_for_document(document);
+#endif
+}
+
+void ConvertLayerCommand::copyCels(Tx& tx,
+                                   Layer* srcLayer,
+                                   Layer* newLayer)
+{
+  std::map<doc::ObjectId, doc::Cel*> linkedCels;
+
+  CelList srcCels;
+  srcLayer->getCels(srcCels);
+  for (Cel* srcCel : srcCels) {
+    frame_t frame = srcCel->frame();
+
+    // Keep linked cels in the new layer
+    Cel* linkedSrcCel = srcCel->link();
+    if (linkedSrcCel) {
+      auto it = linkedCels.find(linkedSrcCel->id());
+      if (it != linkedCels.end()) {
+        tx(new cmd::CopyCel(
+             newLayer, linkedSrcCel->frame(),
+             newLayer, frame, true));
+        continue;
+      }
+    }
+
+    Cel* newCel = create_cel_copy(tx, srcCel, srcLayer->sprite(), newLayer, frame);
+    tx(new cmd::AddCel(newLayer, newCel));
+
+    linkedCels[srcCel->id()] = newCel;
+  }
+}
+
+std::string ConvertLayerCommand::onGetFriendlyName() const
+{
+  switch (params().to()) {
+    case ConvertLayerParam::Background: return Strings::commands_ConvertLayer_Background(); break;
+    case ConvertLayerParam::Layer:      return Strings::commands_ConvertLayer_Layer(); break;
+    case ConvertLayerParam::Tilemap:    return Strings::commands_ConvertLayer_Tilemap(); break;
+    default: return getBaseFriendlyName();
+  }
+}
+
+Command* CommandFactory::createConvertLayerCommand()
+{
+  return new ConvertLayerCommand;
+}
+
+} // namespace app
diff --git a/src/app/ui/app_menuitem.cpp b/src/app/ui/app_menuitem.cpp
index 52f8964e6..875e49c38 100644
--- a/src/app/ui/app_menuitem.cpp
+++ b/src/app/ui/app_menuitem.cpp
@@ -1,5 +1,5 @@
 // Aseprite
-// Copyright (C) 2019-2020  Igara Studio S.A.
+// Copyright (C) 2019-2021  Igara Studio S.A.
 // Copyright (C) 2001-2017  David Capello
 //
 // This program is distributed under the terms of
@@ -87,8 +87,12 @@ bool AppMenuItem::onProcessMessage(Message* msg)
   switch (msg->type()) {
 
     case kCloseMessage:
-      // disable the menu (the keyboard shortcuts are processed by "manager_msg_proc")
-      setEnabled(false);
+      // Don't disable items with submenus
+      if (!hasSubmenu()) {
+        // Disable the menu item (the keyboard shortcuts are processed
+        // by "manager_msg_proc")
+        setEnabled(false);
+      }
       break;
   }