Blob Blame Raw


#include "cleanuppopup.h"

// Toonz includes
#include "tapp.h"
#include "imageviewer.h"
#include "cleanupsettingsmodel.h"
#include "cellselection.h"
#include "columnselection.h"
#include "mainwindow.h"

// ToonzQt includes
#include "toonzqt/gutil.h"
#include "toonzqt/dvdialog.h"
#include "toonzqt/lineedit.h"
#include "toonzqt/menubarcommand.h"
#include "toonzqt/icongenerator.h"

// ToonzLib includes
#include "toonz/toonzscene.h"
#include "toonz/txshcell.h"
#include "toonz/txshsimplelevel.h"
#include "toonz/txshleveltypes.h"
#include "toonz/levelproperties.h"
#include "toonz/imagemanager.h"
#include "toonz/levelupdater.h"
#include "toonz/tcleanupper.h"
#include "toonz/preferences.h"
#include "toonz/tscenehandle.h"
#include "toonz/txsheethandle.h"
#include "toonz/txshlevelhandle.h"
#include "toonz/palettecontroller.h"
#include "toonz/tpalettehandle.h"
#include "toonz/toonzfolders.h"

// TnzCore includes
#include "tsystem.h"
#include "tlevel_io.h"
#include "timageinfo.h"
#include "tstream.h"

// Qt includes
#include <QLabel>
#include <QPushButton>
#include <QRadioButton>
#include <QButtonGroup>
#include <QCoreApplication>
#include <QMainWindow>
#include <QGroupBox>

// STL includes
#include <set>
#include <map>
#include <numeric>
#include <functional>

//*****************************************************************************
//    Local namespace stuff
//*****************************************************************************

namespace {

enum Resolution {
  NO_RESOLUTION,  //!< No required resolution.
  CANCEL,         //!< Validation was canceled.
  OVERWRITE,   //!< Does not delete old cleanupped levels, but overwrites found
               //! frames.
  WRITE_NEW,   //!< Like above, but does not overwrite. Just adds not cleanupped
               //! frames.
  REPLACE,     //!< Destroy the old level and build one anew.
  ADD_SUFFIX,  //!< Add a suffix to the output path.
  NOPAINT_ONLY  //!< overwrite the result only in "nopaint" folder
};

//-----------------------------------------------------------------------------

static const std::wstring unpaintedStr = L"-unpainted";

//-----------------------------------------------------------------------------

inline QString suffix(int num) { return QString("_") + QString::number(num); }

//-----------------------------------------------------------------------------

inline TFilePath withSuffix(const TFilePath &fp, int num) {
  return fp.withName(fp.getWideName() + suffix(num).toStdWString());
}

//-----------------------------------------------------------------------------

inline bool exists(const TFilePath &fp) {
  return TSystem::doesExistFileOrLevel(
      TApp::instance()->getCurrentScene()->getScene()->decodeFilePath(fp));
}

//-----------------------------------------------------------------------------

inline bool exists(const TFilePath &fp, int num) {
  return exists(withSuffix(fp, num));
}

//-----------------------------------------------------------------------------

void loadCleanupParams(CleanupParameters *params, TXshSimpleLevel *sl) {
  params->assign(CleanupSettingsModel::instance()->getCurrentParameters());
  CleanupSettingsModel::loadSettings(params,
                                     CleanupSettingsModel::getClnPath(sl));
}

//-----------------------------------------------------------------------------

double getBestFactor(QSize viewSize, QSize imageSize) {
  if (abs(viewSize.width() - imageSize.width()) >
      abs(viewSize.height() - imageSize.height())) {
    if (viewSize.width() > imageSize.width())
      return double(imageSize.width()) / double(viewSize.width());
    else
      return double(viewSize.width()) / double(imageSize.width());
  } else {
    if (viewSize.height() > imageSize.height())
      return double(imageSize.height()) / double(viewSize.height());
    else
      return double(viewSize.height()) / double(imageSize.height());
  }

  return 0;
}

//-----------------------------------------------------------------------------
/*! cleanup後のファイルlevelPathに対してUnpaintedファイルを作る。
        Cleanup後のUnpaintedの保存先を1階層下げる(nopaintフォルダ内に入れ、
        "A_np.tlv"のように"_np"を付ける。"_unpainted"は長いので)
        Paletteをキープするかどうかのフラグを追加
*/
void saveUnpaintedLevel(const TFilePath &levelPath, TXshSimpleLevel *sl,
                        std::vector<TFrameId> fids, bool keepOriginalPalette) {
  try {
    /*---nopaintフォルダの作成---*/
    TFilePath nopaintDir = levelPath.getParentDir() + "nopaint";
    if (!TFileStatus(nopaintDir).doesExist()) {
      try {
        TSystem::mkDir(nopaintDir);
      } catch (...) {
        return;
      }
    }

    TFilePath unpaintedLevelPath =
        levelPath.getParentDir() + "nopaint\\" +
        TFilePath(levelPath.getName() + "_np." + levelPath.getType());

    if (!TSystem::doesExistFileOrLevel(unpaintedLevelPath)) {
      // No unpainted level exists. So, just copy the output file.
      TSystem::copyFile(unpaintedLevelPath, levelPath);
      if (keepOriginalPalette) return;

      TFilePath levelPalettePath(levelPath.withType("tpl"));
      TFilePath unpaintedLevelPalettePath =
          levelPalettePath.getParentDir() + "nopaint\\" +
          TFilePath(levelPalettePath.getName() + "_np." +
                    levelPalettePath.getType());

      TSystem::copyFile(unpaintedLevelPalettePath, levelPalettePath);

      return;
    }

    TLevelWriterP lw(unpaintedLevelPath);

    if (keepOriginalPalette) lw->setOverwritePaletteFlag(false);

    int i, fidsCount = fids.size();
    for (i = 0; i < fidsCount; ++i) {
      const TFrameId &fid = fids[i];

      TToonzImageP ti = sl->getFrame(fid, false);
      if (!ti) continue;

      lw->getFrameWriter(fid)->save(ti);
    }
  } catch (...) {
  }
}

//------------------------------------------------------------------------------
/*! Cleanup後のデフォルトPaletteを追加する。
TODO:
Cleanup後にデフォルトPaletteの内容を追加する仕様、Preferencesでオプション化
2016/1/16 shun_iwasawa
*/
void addCleanupDefaultPalette(TXshSimpleLevelP sl) {
  /*--- CleanupデフォルトパレットはStudioPaletteフォルダ内に入れる ---*/
  TFilePath palettePath =
      ToonzFolder::getStudioPaletteFolder() + "cleanup_default.tpl";
  TFileStatus pfs(palettePath);

  if (!pfs.doesExist() || !pfs.isReadable()) {
    DVGui::warning(
        QString("CleanupDefaultPalette file: %1 is not found!")
            .arg(QString::fromStdWString(palettePath.getWideString())));
    return;
  }

  TIStream is(palettePath);
  if (!is) {
    DVGui::warning(
        QString("CleanupDefaultPalette file: failed to get TIStream"));
    return;
  }

  std::string tagName;
  if (!is.matchTag(tagName) || tagName != "palette") {
    DVGui::warning(
        QString("CleanupDefaultPalette file: This is not palette file"));
    return;
  }

  std::string gname;
  is.getTagParam("name", gname);
  TPalette *defaultPalette = new TPalette();
  defaultPalette->loadData(is);

  sl->getPalette()->setIsCleanupPalette(false);

  TPalette::Page *dstPage = sl->getPalette()->getPage(0);
  TPalette::Page *srcPage = defaultPalette->getPage(0);

  for (int srcIndexInPage = 0; srcIndexInPage < srcPage->getStyleCount();
       srcIndexInPage++) {
    int id = srcPage->getStyleId(srcIndexInPage);

    bool isUsedInCleanupPalette;
    isUsedInCleanupPalette = false;

    for (int dstIndexInPage = 0; dstIndexInPage < dstPage->getStyleCount();
         dstIndexInPage++) {
      if (dstPage->getStyleId(dstIndexInPage) == id) {
        isUsedInCleanupPalette = true;
        break;
      }
    }

    if (isUsedInCleanupPalette)
      continue;

    else {
      int addedId = sl->getPalette()->addStyle(
          srcPage->getStyle(srcIndexInPage)->clone());
      dstPage->addStyle(addedId);
      /*---
       * StudioPalette由来のDefaultPaletteの場合、GrobalName(リンク)を消去する
       * ---*/
      sl->getPalette()->getStyle(addedId)->setGlobalName(L"");
      sl->getPalette()->getStyle(addedId)->setOriginalName(L"");
    }
  }
  delete defaultPalette;
}

}  // namespace

//*****************************************************************************
//    CleanupLevel  definition
//*****************************************************************************

struct CleanupPopup::CleanupLevel {
  TXshSimpleLevel *m_sl;           //!< Level to be cleanupped.
  TFilePath m_outputPath;          //!< Output path for the cleanupped level.
  std::vector<TFrameId> m_frames;  //!< Frames to cleanup.
  Resolution m_resolution;         //!< Resolution for verified file conflicts.

public:
  CleanupLevel(TXshSimpleLevel *sl, const CleanupParameters &params)
      : m_sl(sl)
      , m_outputPath(CleanupSettingsModel::getOutputPath(m_sl, &params))
      , m_resolution(NO_RESOLUTION) {}

  bool empty() const { return m_frames.empty(); }
};

//*****************************************************************************
//    CleanupPopup implementation
//*****************************************************************************

CleanupPopup::CleanupPopup()
    : QDialog(TApp::instance()->getMainWindow())
    , m_params(new CleanupParameters)
    , m_updater(new LevelUpdater)
    , m_originalLevelPath()
    , m_originalPalette(0)
    , m_firstLevelFrame(true) {
  setWindowTitle(tr("Cleanup"));
  // Progress Bar
  m_progressLabel = new QLabel(tr("Cleanup in progress"));
  m_progressBar   = new QProgressBar;
  // Text
  m_cleanupQuestionLabel = new QLabel(tr("Do you want to cleanup this frame?"));

  m_imageViewer = new ImageViewer(0, 0, false);

  // Buttons
  m_cleanupButton           = new QPushButton(tr("Cleanup"));
  m_skipButton              = new QPushButton(tr("Skip"));
  m_cleanupAllButton        = new QPushButton(tr("Cleanup All"));
  QPushButton *cancelButton = new QPushButton(tr("Cancel"));
  m_imgViewBox              = new QGroupBox(tr("View"), this);

  m_imgViewBox->setCheckable(true);
  m_imgViewBox->setChecked(false);
  m_imageViewer->setVisible(false);
  m_imageViewer->resize(406, 306);

  //---layout
  QVBoxLayout *mainLayout = new QVBoxLayout();
  mainLayout->setMargin(5);
  mainLayout->setSpacing(5);
  {
    mainLayout->addWidget(m_progressLabel, 0);
    mainLayout->addWidget(m_progressBar, 0);

    mainLayout->addWidget(m_cleanupQuestionLabel);

    QVBoxLayout *imgBoxLay = new QVBoxLayout();
    imgBoxLay->setMargin(5);
    { imgBoxLay->addWidget(m_imageViewer); }
    m_imgViewBox->setLayout(imgBoxLay);
    mainLayout->addWidget(m_imgViewBox, 1);

    QHBoxLayout *buttonLay = new QHBoxLayout();
    buttonLay->setMargin(0);
    buttonLay->setSpacing(5);
    {
      buttonLay->addWidget(m_cleanupButton);
      buttonLay->addWidget(m_skipButton);
      buttonLay->addWidget(m_cleanupAllButton);

      buttonLay->addWidget(cancelButton);
    }
    mainLayout->addLayout(buttonLay);
    mainLayout->addStretch();
  }
  setLayout(mainLayout);

  //--- signal-slot connections

  bool ret = true;
  ret      = ret && connect(m_progressBar, SIGNAL(valueChanged(int)), this,
                       SLOT(onValueChanged(int)));

  // NOTE: On MAC it seems that QAbstractButton's pressed() signal is reemitted
  // at
  // every mouseMoveEvent when the button is pressed...
  // This is why clicked() substitutes pressed() below.

  ret = ret && connect(m_cleanupButton, SIGNAL(clicked()), this,
                       SLOT(onCleanupFrame()));
  ret = ret &&
        connect(m_skipButton, SIGNAL(clicked()), this, SLOT(onSkipFrame()));
  ret = ret && connect(m_cleanupAllButton, SIGNAL(clicked()), this,
                       SLOT(onCleanupAllFrame()));
  ret = ret &&
        connect(cancelButton, SIGNAL(clicked()), this, SLOT(onCancelCleanup()));
  ret = ret && connect(m_imgViewBox, SIGNAL(toggled(bool)), this,
                       SLOT(onImgViewBoxToggled(bool)));

  assert(ret);

  reset();  // Initialize remaining variables

  resize(450, 400);
}

//-----------------------------------------------------------------------------

CleanupPopup::~CleanupPopup() {}

//-----------------------------------------------------------------------------

void CleanupPopup::closeEvent(QCloseEvent *ce) { reset(); }

//-----------------------------------------------------------------------------

void CleanupPopup::reset() {
  closeLevel();

  m_idx = m_completion = std::pair<int, int>(-1, -1);
  m_cleanupLevels.clear();

  m_imageViewer->setImage(TImageP());

  TCleanupper::instance()->setParameters(
      CleanupSettingsModel::instance()->getCurrentParameters());

  m_cleanupQuestionLabel->show();

  m_levelAlreadyExists.clear();

  /*---タイトルバーを元の表示に戻す---*/
  MainWindow *mainWin =
      qobject_cast<MainWindow *>(TApp::instance()->getMainWindow());
  if (mainWin) mainWin->changeWindowTitle();
}

//-----------------------------------------------------------------------------

void CleanupPopup::buildCleanupList() {
  struct locals {
    static inline bool supportsCleanup(TXshSimpleLevel *sl) {
      return (
          sl->getType() & FULLCOLOR_TYPE ||
          (sl->getType() == TZP_XSHLEVEL && !sl->getScannedPath().isEmpty()));
    }
  };  // locals

  typedef std::set<TFrameId> FramesList;

  /*--- これからCleanupするLevel/FrameIdのリスト ---*/
  std::map<TXshSimpleLevel *, FramesList>
      cleanupList;  // List of frames to be cleanupped
  std::vector<TXshSimpleLevel *>
      levelsList;  // List of levels in the cleanup list,
                   // ordered as found in the xsheet.
  m_cleanupLevels.clear();

  // Retrieve current selection
  TCellSelection *selection =
      dynamic_cast<TCellSelection *>(TSelection::getCurrent());
  TColumnSelection *columnSel =
      dynamic_cast<TColumnSelection *>(TSelection::getCurrent());
  /*--- セル選択でも、カラム選択でも無い場合はCleanup自体を無効にする ---*/
  if (!selection && !columnSel) return;

  TXsheet *xsh = TApp::instance()->getCurrentXsheet()->getXsheet();

  /*--- セル選択の場合 ---*/
  if (selection) {
    if (selection->isEmpty()) return;
    // Store frames in the specified selection
    int r0, c0, r1, c1;
    selection->getSelectedCells(r0, c0, r1, c1);

    int r, c;
    for (c = c0; c <= c1; ++c) {
      for (r = r0; r <= r1; ++r) {
        /*---選択範囲内にLevelが無い場合はcontinue---*/
        const TXshCell &cell = xsh->getCell(r, c);
        if (cell.isEmpty()) continue;
        TXshSimpleLevel *sl = cell.getSimpleLevel();
        if (!(sl && locals::supportsCleanup(sl))) continue;
        /*---もし新しいLevelなら、Levelのリストに登録---*/
        std::map<TXshSimpleLevel *, FramesList>::iterator it =
            cleanupList.find(sl);
        if (it == cleanupList.end()) {
          it = cleanupList.insert(std::make_pair(sl, FramesList())).first;
          levelsList.push_back(sl);
        }
        /*---TFrameIdを登録---*/
        it->second.insert(cell.getFrameId());
      }
    }
  }
  /*--- カラム選択の場合 ---*/
  else {
    int frameCount = xsh->getFrameCount();
    if (columnSel->isEmpty() || frameCount <= 0) return;
    /*--- 選択された各カラムについて ---*/
    std::set<int>::const_iterator it = columnSel->getIndices().begin();
    for (; it != columnSel->getIndices().end(); ++it) {
      int c = (*it);
      for (int r = 0; r < frameCount; r++) {
        /*--- 選択範囲内にLevelが無い場合はcontinue ---*/
        const TXshCell &cell = xsh->getCell(r, c);
        if (cell.isEmpty()) continue;
        TXshSimpleLevel *sl = cell.getSimpleLevel();
        if (!sl && locals::supportsCleanup(sl)) continue;
        /*---もし新しいLevelなら、Levelのリストに登録---*/
        std::map<TXshSimpleLevel *, FramesList>::iterator it =
            cleanupList.find(sl);
        if (it == cleanupList.end()) {
          it = cleanupList.insert(std::make_pair(sl, FramesList())).first;
          levelsList.push_back(sl);
        }
        /*---TFrameIdを登録---*/
        it->second.insert(cell.getFrameId());
      }
    }
  }

  // Finally, copy the retrieved data to the sorted output vector
  std::vector<TXshSimpleLevel *>::iterator lt, lEnd = levelsList.end();
  for (lt = levelsList.begin(); lt != lEnd; ++lt) {
    loadCleanupParams(
        m_params.get(),
        *lt);  // Load cleanup parameters associated with current level.
    // This is necessary since the output path is specified among them.
    m_cleanupLevels.push_back(CleanupLevel(*lt, *m_params.get()));
    CleanupLevel &cl = m_cleanupLevels.back();

    FramesList &framesList = cleanupList[cl.m_sl];
    cl.m_frames.assign(framesList.begin(), framesList.end());
  }
}

//-----------------------------------------------------------------------------

bool CleanupPopup::analyzeCleanupList() {
  ToonzScene *scene = TApp::instance()->getCurrentScene()->getScene();

  bool shownOverwriteDialog = false, shownWritingOnSourceFile = false;

  /*--- 消されるLevel名の確認ダイアログを出すため ---*/
  QList<TFilePath> filePathsToBeDeleted;
  /*	Cleanup前に既存のLevelを消す場合、セルのStatusをScannedに変更するために、
          TXshSimpleLevelポインタを格納しておく
  */
  QList<TXshSimpleLevel *> levelsToBeDeleted;

  // Traverse the cleanup list
  for (auto &clt : m_cleanupLevels) {
    TXshSimpleLevel *sl = clt.m_sl;

    /*--- Cleanup対象LevelのCleanupSettingを取得 ---*/
    loadCleanupParams(m_params.get(),
                      sl);  // Needed to retrieve output level resolution

    // Check level existence
    /*--- Cleanup後に得られるであろうTLVのパス ---*/
    TFilePath outputPath = scene->decodeFilePath(clt.m_outputPath);
    {
      /*-- 出力先にTLVファイルが無ければ問題なし(このLevelはCleanupする) --*/
      if (!TSystem::doesExistFileOrLevel(outputPath)) {
        m_levelAlreadyExists[sl] = false;
        continue;  // Newly cleaned up level. No problem.
      } else
        m_levelAlreadyExists[sl] = true;

      // Check whether output file == input file
      /*--- 入力となるScanned(TIFなど)のパスを得る ---*/
      const TFilePath &inputPath =
          scene->decodeFilePath(CleanupSettingsModel::getInputPath(sl));

      if (!shownWritingOnSourceFile && inputPath == outputPath) {
        shownWritingOnSourceFile = true;

        int ret = DVGui::MsgBox(tr("Selected drawings will overwrite the "
                                   "original files after the cleanup process.\n"
                                   "Do you want to continue?"),
                                tr("Ok"), tr("Cancel"));

        if (ret != 1) return false;
      }

      /*---一度も上書き確認のダイアログを出していなかったら、ここで出す---*/
      // Reset the overwrite dialog
      if (!shownOverwriteDialog) {
        shownOverwriteDialog = true;

        if (!m_overwriteDialog)
          m_overwriteDialog.reset(new OverwriteDialog);
        else
          m_overwriteDialog->reset();
      }

      // Prompt user for file conflict resolution
      clt.m_resolution = Resolution(m_overwriteDialog->execute(&clt.m_outputPath));
      switch (clt.m_resolution) {
      case CANCEL:
        return false;

      case NO_RESOLUTION:
        assert(false);
        break;
      case REPLACE:
        /*--- 既存のTLVを消すオプションの場合、消されるファイルのリストを作る
         * ---*/
        break;
      case ADD_SUFFIX:
        continue;
      case NOPAINT_ONLY:
        /*--- NOPAINT_ONLY の場合は、nopaintのみを変更。
                ただし、nopaintのLevelは消さず、処理したフレームを Overwrite
        する
        ---*/
        outputPath =
            outputPath.getParentDir() + "nopaint\\" +
            TFilePath(outputPath.getName() + "_np." + outputPath.getType());
        /*--- nopaintの有無を確かめる。無ければ次のLevelへ
         * (このLevelはCleanupする) ---*/
        if (!TSystem::doesExistFileOrLevel(outputPath)) {
          m_levelAlreadyExists[sl] = false;
          continue;
        }
      default:
        break;
      }

      TLevelP level(0);  // Current level info. Yeah the init is a shame... :(
      /*--- 元のLevelと新しいCleanup結果が混合する場合。REPLACE以外 ---*/
      if (clt.m_resolution == OVERWRITE || clt.m_resolution == WRITE_NEW ||
          clt.m_resolution == NOPAINT_ONLY) {
        // Check output resolution consistency
        // Retrieve file resolution
        /*---現在在るTLVのサイズと、CleanupSettingsのサイズが一致しているかチェック---*/
        TDimension oldRes(0, 0);
        try {
          TLevelReaderP lr(outputPath);
          level = lr->loadInfo();

          if (const TImageInfo *imageInfo = lr->getImageInfo())
            oldRes = TDimension(imageInfo->m_lx, imageInfo->m_ly);
          else
            throw TException();
        } catch (...) {
          // There was a problem reading the existing level data.
          // Thus, the conservative approach is not feasible.

          // Inform the user and abort cleanup
          DVGui::warning(
              tr("There were errors opening the existing level "
                 "\"%1\".\n\nPlease choose to delete the existing level and "
                 "create a new one\nwhen running the cleanup process.")
                  .arg(QString::fromStdWString(outputPath.getLevelNameW())));

          return false;
        }

        // Retrieve output resolution
        TDimension outRes(0, 0);
        TPointD outDpi;
        m_params->getOutputImageInfo(outRes, outDpi.x, outDpi.y);

        if (oldRes != outRes) {
          DVGui::warning(
              tr("The resulting resolution of level \"%1\"\ndoes not match "
                 "with that of previously cleaned up level drawings.\n\nPlease "
                 "set the right camera resolution and closest field, or choose "
                 "to delete\nthe existing level and create a new one when "
                 "running the cleanup process.")
                  .arg(QString::fromStdWString(outputPath.getLevelNameW())));

          return false;
        }
      }
      /*--- REPLACEの場合、消されるファイルパスのリストを作る ---*/
      else if (clt.m_resolution == REPLACE) {
        filePathsToBeDeleted.push_back(outputPath);

        levelsToBeDeleted.push_back(sl);

        /*--- パレットファイルも、あれば消す ---*/
        TFilePath palettePath =
            (outputPath.getParentDir() + outputPath.getName()).withType("tpl");
        if (TSystem::doesExistFileOrLevel(palettePath))
          filePathsToBeDeleted.push_back(palettePath);
        /*--- つぎに、nopaintのTLV。これは、REPLACE、NOPAINT_ONLY 両方で消す
         * ---*/
        TFilePath unpaintedLevelPath =
            outputPath.getParentDir() + "nopaint\\" +
            TFilePath(outputPath.getName() + "_np." + outputPath.getType());
        if (TSystem::doesExistFileOrLevel(unpaintedLevelPath)) {
          filePathsToBeDeleted.push_back(unpaintedLevelPath);
          filePathsToBeDeleted.push_back(
              (unpaintedLevelPath.getParentDir() + unpaintedLevelPath.getName())
                  .withType("tpl"));
        }
      }

      // Finally, apply resolution to individual frames.
      /*--- WRITE_NEW は、「未Cleanupのフレームだけ処理する」オプション ---*/
      if (clt.m_resolution == WRITE_NEW) {
        const TLevel::Table *table = level->getTable();

        clt.m_frames.erase(
            std::remove_if(clt.m_frames.begin(), clt.m_frames.end(),
                           [table](TLevel::Table::key_type const &key) {
                             return table->count(key);
                           }),
            clt.m_frames.end());
      }
    }
  }

  /*--- ファイル消去の確認ダイアログを表示 ---*/
  if (!filePathsToBeDeleted.isEmpty()) {
    QString question = QObject::tr(
        "Delete and Re-cleanup : The following files will be deleted.\n\n");
    for (int i = 0; i < filePathsToBeDeleted.size(); i++) {
      question +=
          "   " +
          QString::fromStdWString(filePathsToBeDeleted[i].getWideString()) +
          "\n";
    }
    question += QObject::tr("\nAre you sure ?");

    int ret = DVGui::MsgBox(question, QObject::tr("Delete"),
                            QObject::tr("Cancel"), 0);
    if (ret == 0 || ret == 2) {
      return false;
    } else if (ret == 1) {
      /*--- 先にCleanup処理で出力先となるファイルを消す ---*/
      try {
        for (int i = 0; i < filePathsToBeDeleted.size(); i++) {
          TSystem::removeFileOrLevel_throw(filePathsToBeDeleted[i]);
        }
      } catch (...) {
        return false;
      }

      // Reset level status
      for (int i = 0; i < levelsToBeDeleted.size(); i++) {
        TXshSimpleLevel *lev = levelsToBeDeleted.at(i);
        /*--- TLVだった場合、Scanned(TIFレベル)に戻す ---*/
        TFilePath scannedPath = lev->getScannedPath();
        if (scannedPath != TFilePath()) {
          lev->setScannedPath(TFilePath());
          lev->setPath(scannedPath, true);
          lev->clearFrames();
          lev->setType(OVL_XSHLEVEL);  // OVL_XSHLEVEL
          lev->setPalette(0);
          if (lev == TApp::instance()->getCurrentLevel()->getLevel())
            TApp::instance()
                ->getPaletteController()
                ->getCurrentLevelPalette()
                ->setPalette(0);

          lev->load();
          int i, frameCount = lev->getFrameCount();
          for (i = 0; i < frameCount; i++) {
            TFrameId id = lev->index2fid(i);
            IconGenerator::instance()->invalidate(lev, id);
          }
          IconGenerator::instance()->invalidateSceneIcon();
        }
      }
    }
  }

  // Before returning, erase levels whose frames list is empty
  /*--- Cleanup対象フレームが無くなったLevelを対象から外す ---*/
  m_cleanupLevels.erase(
      std::remove_if(m_cleanupLevels.begin(), m_cleanupLevels.end(),
                     std::mem_fn(&CleanupLevel::empty)),
      m_cleanupLevels.end());

  return true;
}

//-----------------------------------------------------------------------------

bool CleanupPopup::isValidPosition(const std::pair<int, int> &pos) const {
  if (pos.first < 0 || int(m_cleanupLevels.size()) <= pos.first) return false;

  const CleanupLevel &cl = m_cleanupLevels[pos.first];

  if (pos.second < 0 || int(cl.m_frames.size()) <= pos.second) return false;

  return true;
}

//-----------------------------------------------------------------------------

QString CleanupPopup::currentString() const {
  if (!isValidPosition(m_idx)) return QString();

  const CleanupLevel &cl = m_cleanupLevels[m_idx.first];

  TXshSimpleLevel *sl = cl.m_sl;
  const TFrameId &fid = cl.m_frames[m_idx.second];

  TFilePath scannedPath(sl->getScannedPath());
  if (scannedPath.isEmpty()) scannedPath = sl->getPath();

  TFilePath levelName(scannedPath.getLevelNameW());
  QString imageName = toQString(levelName.withFrame(fid));

  return tr("Cleanup in progress: ") + imageName + " " +
         QString::number(m_completion.first) + "/" +
         QString::number(m_completion.second);
}

//-----------------------------------------------------------------------------
/*--- これからCleanupするフレームを取得 ---*/
TImageP CleanupPopup::currentImage() const {
  if (!isValidPosition(m_idx)) return TImageP();

  const CleanupLevel &cl = m_cleanupLevels[m_idx.first];
  return cl.m_sl->getFrameToCleanup(cl.m_frames[m_idx.second]);
}

//-----------------------------------------------------------------------------

void CleanupPopup::execute() {
  struct locals {
    static inline int addFrames(int a, const CleanupLevel &cl) {
      return a + int(cl.m_frames.size());
    }
  };  // locals

  // Re-initialize the list of frames to be cleanupped
  /*--- Cleanup対象のリストを作る ---*/
  buildCleanupList();

  // In case some cleaned up level already exists, let the user decide what to
  // do
  if (!analyzeCleanupList()) {
    reset();
    return;
  }

  // Initialize completion variable
  m_completion = std::pair<int, int>(
      0, std::accumulate(m_cleanupLevels.begin(), m_cleanupLevels.end(), 0,
                         locals::addFrames));

  // If there are no (more) frames to cleanup, warn and quit
  int framesCount = m_completion.second;
  if (!framesCount) {
    DVGui::info(
        tr("It is not possible to cleanup: the cleanup list is empty."));

    reset();
    return;
  }

  // Initialize the cleanup process and show the popup

  m_idx = std::pair<int, int>(0, 0);

  // Reset progress bar
  m_progressLabel->setText(currentString());

  m_progressBar->setRange(0, framesCount);
  m_progressBar->setValue(0);

  // show the progress to the main window's title bar
  updateTitleString();

  TImageP image = currentImage();
  if (image) {
    m_imageViewer->setImage(image);

    // Set the zoom factor depending on image and viewer sizes, so that the
    // image fits
    // the preview area.

    QSize viewSize  = m_imageViewer->size();
    QSize imageSize = QSize(image->getBBox().getLx(), image->getBBox().getLy());
    double factor   = getBestFactor(viewSize, imageSize);
    TPointD delta(0, 0);
    TAffine viewAff =
        TTranslation(delta) * TScale(factor) * TTranslation(-delta);
    m_imageViewer->setViewAff(viewAff);
  }

  show();
}

//-----------------------------------------------------------------------------

QString CleanupPopup::setupLevel() {
  // Level's pre-cleanup stuff initialization.
  // Invoked right before the cleanupFrame() of the first level frame.

  assert(isValidPosition(m_idx));

  TApp *app         = TApp::instance();
  ToonzScene *scene = app->getCurrentScene()->getScene();

  /*--- これからCleanupするLevel ---*/
  CleanupLevel &cl    = m_cleanupLevels[m_idx.first];
  TXshSimpleLevel *sl = cl.m_sl;

  /*---  保存先のTLVが既に存在する、かつ、REPLACE でも
          NOPAINT_ONLY でもない場合、Paletteを変更せず維持する ---*/
  if (cl.m_resolution != REPLACE && cl.m_resolution != NOPAINT_ONLY &&
      m_levelAlreadyExists[sl] == true)
    m_keepOriginalPalette = true;
  else
    m_keepOriginalPalette = false;

  // Update cleanup parameters, loading a cln if necessary
  TCleanupper *cleanupper = TCleanupper::instance();

  /*--- CleanupSettingsを読み込み、TCleanupperに渡す ---*/
  loadCleanupParams(m_params.get(), sl);
  cleanupper->setParameters(m_params.get());

  // Touch the output parent directory
  TFilePath &outputPath = cl.m_outputPath;

  /*--- 保存先PathをFull Pathにする ---*/
  TFilePath decodedPath(scene->decodeFilePath(outputPath));
  if (!TSystem::touchParentDir(decodedPath))
    return tr("Couldn't create directory \"%1\"").arg(decodedPath.getQString());

  /*---
   * 上書きオプションが選択されているとき、既存のLevel,Palette,Nopaintファイルを消す
   * ---*/
  // If the user decided to remove any existing level, do so now.
  if (cl.m_resolution == REPLACE) {
    const QString &err = resetLevel();

    if (!err.isEmpty()) return err;
  }
  /*--- Nopaintのみ上書きのオプションが選択されているとき(再Clenaupの場合)
     ---*/
  else if (cl.m_resolution == NOPAINT_ONLY) {
    m_originalLevelPath = outputPath;
    /*--- 必要なら、nopaintフォルダを作成 ---*/
    TFilePath nopaintDir = decodedPath.getParentDir() + "nopaint";
    if (!TFileStatus(nopaintDir).doesExist()) {
      try {
        TSystem::mkDir(nopaintDir);
      } catch (...) {
        return NULL;
      }
    }
    /*--- 保存先のパスをnopaintの方に変更 ---*/
    outputPath =
        outputPath.getParentDir() + "nopaint\\" +
        TFilePath(outputPath.getName() + "_np." + outputPath.getType());
    decodedPath = scene->decodeFilePath(outputPath);
  }

  // Frames are cleaned-up at full-sampling. Thus, clear subsampling on the
  // level.
  if (sl->getProperties()->getSubsampling() != 1) {
    sl->getProperties()->setSubsampling(1);
    sl->invalidateFrames();
  }

  bool lineProcessing   = (m_params->m_lineProcessingMode != lpNone),
       notLineProcessed = (sl->getType() != TZP_XSHLEVEL);

  if (lineProcessing) {
    /*--- Keep original palette which will be reverted after cleanup ---*/
    if (m_keepOriginalPalette) {
      if ((sl->getType() == TZP_XSHLEVEL || sl->getType() == TZI_XSHLEVEL) &&
          sl->getPalette() != NULL)
        m_originalPalette = sl->getPalette()->clone();
      else /*--- In case the level has been already cleanupped,
           and is cleanupped again from raster level ---*/
      {
        /*--- Load and keep the palette from destination TLV ---*/
        TFilePath targetPalettePath = outputPath.getParentDir() +
                                      TFilePath(outputPath.getName() + ".tpl");
        TFileStatus pfs(targetPalettePath);
        if (pfs.doesExist() && pfs.isReadable()) {
          TIStream is(targetPalettePath);
          std::string tagName;
          if (!is.matchTag(tagName) || tagName != "palette") {
            DVGui::warning(QString(
                "CleanupDefaultPalette file: This is not palette file"));
            return NULL;
          }
          m_originalPalette = new TPalette();
          m_originalPalette->loadData(is);
        } else
          m_originalPalette = 0;
      }
    }

    if (notLineProcessed) {
      /*-- Type, Pathを切り替えてTLVにする --*/
      // The level type changes to TLV
      sl->makeTlv(outputPath);

      // Remap all current images under the IM control to Scanned status.
      int f, fCount = sl->getFrameCount();

      for (f = 0; f != fCount; ++f) {
        const TFrameId &fid = sl->getFrameId(f);

        /*--- 「スキャン済み」のステータスにし、画像、アイコンのIDを切り替える
         * ---*/
        sl->setFrameStatus(fid, TXshSimpleLevel::Scanned);
        ImageManager::instance()->rebind(
            sl->getImageId(fid, 0),
            sl->getImageId(fid, TXshSimpleLevel::Scanned));

        const std::string &oldIconId = sl->getIconId(fid, 0);
        const std::string &newIconId =
            sl->getIconId(fid, TXshSimpleLevel::Scanned);

        IconGenerator::instance()->remap(newIconId, oldIconId);
      }
    }
    /*--- 対象が既にTLVファイルで、出力先パスが違う場合、切り替える ---*/
    else if (outputPath != sl->getPath()) {
      // Just wants to be written to a different destination path. Update it.
      sl->setPath(outputPath, false);
    }

    /*-- Cleanup用のパレットを作る --*/
    // Update the level palette
    TPaletteP palette =
        TCleanupper::instance()->createToonzPaletteFromCleanupPalette();

    sl->setPalette(palette.getPointer());

    /*--- カレントPaletteを切り替える ---*/
    if (sl == app->getCurrentLevel()->getLevel())
      app->getPaletteController()->getCurrentLevelPalette()->setPalette(
          palette.getPointer());

    // Notify the xsheet that the level has changed visual type informations
    // (either the level type,
    // cleanup status, etc)
    app->getCurrentXsheet()->notifyXsheetChanged();
  } else if (!m_params->getPath(scene)
                  .isEmpty())  // Should never be empty, AFAIK...
  {
    // No line processing

    if (notLineProcessed) {
      // Just change paths
      if (sl->getScannedPath().isEmpty()) sl->setScannedPath(sl->getPath());

      sl->setPath(outputPath, false);  // Reload frames from the result, too
    } else {
      // Return to scan level type
      sl->clearFrames();
      sl->setType(OVL_XSHLEVEL);
      sl->setPath(outputPath);
    }
  }

  // Finally, open the LevelUpdater on the level.
  assert(!m_updater->opened());
  try {
    m_updater->open(sl);
  } catch (...) {
    return tr("Couldn't open \"%1\" for write").arg(outputPath.getQString());
  }

  m_updater->getLevelWriter()->setOverwritePaletteFlag(!m_keepOriginalPalette);

  return QString();
}

//-----------------------------------------------------------------------------
/*	setupLevel()から、 m_overwriteAction == REPLACE のとき呼ばれる。
        選択LevelのCleanup後Levelを消す
*/
QString CleanupPopup::resetLevel() {
  struct locals {
    static bool removeFileOrLevel(const TFilePath &fp) {
      return (!TSystem::doesExistFileOrLevel(fp) ||
              TSystem::removeFileOrLevel(fp));
    }

    static QString decorate(const TFilePath &fp) {
      return tr("Couldn't remove file \"%1\"").arg(fp.getQString());
    }
  };

  // Try to remove the existing level
  const CleanupLevel &cl = m_cleanupLevels[m_idx.first];

  TXshSimpleLevel *sl = cl.m_sl;
  assert(sl);

  ToonzScene *scene = TApp::instance()->getCurrentScene()->getScene();

  /*--- Cleanup後のTLVファイルパスを得る。すなわちこれから消すファイル ---*/
  // Ensure outputPath != inputPath
  const TFilePath &outputPath = scene->decodeFilePath(cl.m_outputPath),
                  &inputPath  = scene->decodeFilePath(
                      CleanupSettingsModel::getInputPath(sl));

  if (inputPath == outputPath)
    return QString();  // Cannot remove source file - however, it can be
                       // overwritten

  // Remove existing output files
  if (!locals::removeFileOrLevel(outputPath))
    return locals::decorate(outputPath);

  if (m_params->m_lineProcessingMode != lpNone) {
    TFilePath fp;

    // Line processing on - remove palette too
    if (!locals::removeFileOrLevel(fp = outputPath.withType("tpl")))
      return locals::decorate(fp);

    // Also remove unpainted output path if any
    const TFilePath &unpaintedPath(
        outputPath.getParentDir() + "nopaint\\" +
        TFilePath(outputPath.getName() + "_np." + outputPath.getType()));

    if (!locals::removeFileOrLevel(unpaintedPath))
      return locals::decorate(unpaintedPath);

    if (!locals::removeFileOrLevel(fp = unpaintedPath.withType("tpl")))
      return locals::decorate(fp);
  }

  // Reset level status
  sl->setPath(sl->getPath(), false);  // false rebuilds level data
  // NOTE: sl is the INPUT level - so this instruction
  return QString();  //       should take place AFTER output availability
                     //       has been ensured.
}

//-----------------------------------------------------------------------------
/*---
 * 現在処理を行っているLevelの最後のフレームの処理が終わってから、フレームを進めるときに呼ばれる
 * ---*/
void CleanupPopup::closeLevel() {
  if (m_cleanuppedLevelFrames.empty()) {
    if (m_updater->opened()) m_updater->close();
    return;
  }

  // Save the unpainted level if necessary
  const CleanupLevel &cl = m_cleanupLevels[m_idx.first];

  TXshSimpleLevel *sl = cl.m_sl;
  assert(sl);

  /*--- Nopaintのみ上書きの場合、Cleanup前に戻す ---*/
  if (cl.m_resolution == NOPAINT_ONLY && !m_originalLevelPath.isEmpty()) {
    sl->setPath(m_originalLevelPath);
    sl->invalidateFrames();
    std::vector<TFrameId> fIds;
    sl->getFids(fIds);
    invalidateIcons(sl, fIds);
    m_originalLevelPath = TFilePath();
  }
  /*--- Paletteを更新しない場合は、ここで取っておいたPaletteデータを元に戻す
   * ---*/
  if (m_keepOriginalPalette && m_originalPalette) {
    sl->setPalette(m_originalPalette);
    if (sl == TApp::instance()->getCurrentLevel()->getLevel())
      TApp::instance()
          ->getPaletteController()
          ->getCurrentLevelPalette()
          ->setPalette(m_originalPalette);
    sl->invalidateFrames();
    std::vector<TFrameId> fIds;
    sl->getFids(fIds);
    invalidateIcons(sl, fIds);
    TApp::instance()->getCurrentPalette()->notifyPaletteSwitched();
  }

  // Close the level updater. Silence any exception (ok, something went bad, old
  // story now).
  try {
    m_updater->close();
  } catch (...) {
  }

  if (sl->getType() == TZP_XSHLEVEL &&
      Preferences::instance()->isSaveUnpaintedInCleanupEnable() &&
      cl.m_resolution !=
          NOPAINT_ONLY) /*--- 再Cleanupの場合は既にNoPaintに上書きしている ---*/
  {
    const TFilePath &outputPath = cl.m_outputPath;

    ToonzScene *scene = TApp::instance()->getCurrentScene()->getScene();
    TFilePath decodedPath(scene->decodeFilePath(outputPath));

    if (outputPath.getLevelNameW().find(L"-np.") == std::wstring::npos &&
        TFileStatus(decodedPath).doesExist()) {
      saveUnpaintedLevel(decodedPath, sl, m_cleanuppedLevelFrames,
                         (m_keepOriginalPalette && m_originalPalette));
    }
  }

  m_cleanuppedLevelFrames.clear();
  m_firstLevelFrame = true;
}

//-----------------------------------------------------------------------------
/*-- 各フレームの処理 --*/
void CleanupPopup::cleanupFrame() {
  assert(isValidPosition(m_idx));

  TRasterImageP original(currentImage());
  if (!original) return;

  CleanupLevel &cl    = m_cleanupLevels[m_idx.first];
  TXshSimpleLevel *sl = cl.m_sl;
  const TFrameId &fid = cl.m_frames[m_idx.second];

  assert(sl);

  // Perform image cleanup
  {
    TCleanupper *cl                 = TCleanupper::instance();
    const CleanupParameters *params = cl->getParameters();

    if (params->m_lineProcessingMode == lpNone) {
      // No line processing

      TRasterImageP ri(original);
      if (params->m_autocenterType != CleanupTypes::AUTOCENTER_NONE) {
        bool autocentered;
        ri = cl->autocenterOnly(original, false, autocentered);
        if (!autocentered)
          DVGui::warning(
              QObject::tr("The autocentering failed on the current drawing."));
      }

      sl->setFrame(fid, ri);

      // Update the associated file. In case the operation throws, oh well the
      // image gets skipped.
      try {
        m_updater->update(fid, ri);
      } catch (...) {
      }

      IconGenerator::instance()->invalidate(sl, fid);
    } else {
      // Perform main processing

      // Obtain the source dpi. Changed it to be done once at the first frame of
      // each level in order to avoid the following problem:
      // If the original raster level has no dpi (such as TGA images), obtaining
      // dpi in every frame causes dpi mismatch between the first frame and the
      // following frames, since the value
      // TXshSimpleLevel::m_properties->getDpi() will be changed to the
      // dpi of cleanup camera (= TLV's dpi) after finishing the first frame.
      if (m_firstLevelFrame) {
        TPointD dpi;
        original->getDpi(dpi.x, dpi.y);
        if (dpi.x == 0 && dpi.y == 0) dpi = sl->getProperties()->getDpi();
        cl->setSourceDpi(dpi);
      }

      CleanupPreprocessedImage *cpi;
      {
        TRasterImageP resampledRaster;
        cpi = cl->process(original, m_firstLevelFrame, resampledRaster);
      }

      if (!cpi) return;

      // Perform post-processing
      TToonzImageP ti(cl->finalize(cpi));

      /*--- Cleanup Default Paletteを作成、適用 ---*/
      if (m_firstLevelFrame) {
        addCleanupDefaultPalette(sl);
        sl->getPalette()->setPaletteName(sl->getName());
      }

      ti->setPalette(sl->getPalette());  // Assigned to sl in setupLevel()
      assert(sl->getPalette());

      // Update the level data about the cleanupped frame
      sl->setFrameStatus(fid,
                         sl->getFrameStatus(fid) | TXshSimpleLevel::Cleanupped);
      sl->setFrame(fid, TImageP());  // Invalidate the old image data

      // Output the cleanupped image to disk
      try {
        m_updater->update(fid, ti);
      }  // The file image data will be reloaded upon request
      catch (...) {
      }

      // Invalidate icons
      IconGenerator::instance()->invalidate(sl, fid);

      int autocenterType = params->m_autocenterType;
      if (autocenterType == CleanupTypes::AUTOCENTER_FDG &&
          !cpi->m_autocentered)
        DVGui::warning(
            QObject::tr("The autocentering failed on the current drawing."));

      delete cpi;

      if (m_firstLevelFrame) {
        // Update result-dependent level data
        TPointD dpi(0, 0);
        ti->getDpi(dpi.x, dpi.y);
        if (dpi.x != 0 && dpi.y != 0) sl->getProperties()->setDpi(dpi);

        sl->getProperties()->setImageRes(ti->getSize());
        sl->getProperties()->setBpp(32);
      }
    }
  }

  // this enables to view the level during cleanup by another user. this
  // behavior may abort Toonz.
  /*
try { m_updater->flush(); }                                           // Release
the opened level from writing
catch(...) {}                                                       // It is
required to have it open for read
                                                                  // when
rebuilding icons... (still dangerous though)
  */
  m_firstLevelFrame = false;
  m_cleanuppedLevelFrames.push_back(fid);

  TApp *app = TApp::instance();
  app->getCurrentLevel()->notifyLevelChange();
  app->getCurrentXsheet()->notifyXsheetChanged();
}

//-----------------------------------------------------------------------------

void CleanupPopup::advanceFrame() {
  assert(isValidPosition(m_idx));

  std::pair<int, int> newIdx = std::make_pair(m_idx.first, m_idx.second + 1);

  // In case the level was completely processed, close down the old level
  if (!isValidPosition(newIdx)) {
    if (!m_cleanuppedLevelFrames.empty()) closeLevel();

    newIdx = std::make_pair(m_idx.first + 1, 0);
  }

  // Advance in the cleanup list
  m_idx = newIdx;

  if (m_imgViewBox->isChecked()) {
    TImageP image = currentImage();
    if (image) m_imageViewer->setImage(image);
  }

  // show the progress in the mainwindow's title bar
  updateTitleString();

  // Update the progress bar
  m_progressBar->setValue(++m_completion.first);
}

//-----------------------------------------------------------------------------

void CleanupPopup::onValueChanged(int value) {
  if (value == m_progressBar->maximum()) {
    close();
    return;
  }

  m_progressLabel->setText(currentString());
}

//-----------------------------------------------------------------------------

void CleanupPopup::onCleanupFrame() {
  /*--- Busy時にボタンをUnableする ---*/
  m_cleanupAllButton->setEnabled(false);
  m_cleanupButton->setEnabled(false);
  m_skipButton->setEnabled(false);

  /*--- 新しいLevelに取り掛かり始めたとき ---*/
  if (m_cleanuppedLevelFrames.empty()) {
    const QString &err = setupLevel();

    if (!err.isEmpty()) {
      DVGui::error(err);
      return;
    }
  }

  cleanupFrame();
  advanceFrame();

  /*--- ボタンを元に戻す---*/
  m_cleanupAllButton->setEnabled(true);
  m_cleanupButton->setEnabled(true);
  m_skipButton->setEnabled(true);
}

//-----------------------------------------------------------------------------

void CleanupPopup::onSkipFrame() { advanceFrame(); }

//-----------------------------------------------------------------------------

void CleanupPopup::onCleanupAllFrame() {
  m_cleanupQuestionLabel->hide();

  /*--- Busy時にボタンをUnableする ---*/
  m_cleanupAllButton->setEnabled(false);
  m_cleanupButton->setEnabled(false);
  m_skipButton->setEnabled(false);

  while (isValidPosition(m_idx)) {
    if (m_cleanuppedLevelFrames.empty()) {
      const QString &err = setupLevel();

      if (!err.isEmpty()) {
        DVGui::error(err);
        return;
      }
    }

    cleanupFrame();
    advanceFrame();

    QCoreApplication::processEvents();  // Allow cancels to be received
  }
  /*--- ボタンを元に戻す---*/
  m_cleanupAllButton->setEnabled(true);
  m_cleanupButton->setEnabled(true);
  m_skipButton->setEnabled(true);

  close();
}

//-----------------------------------------------------------------------------

void CleanupPopup::onCancelCleanup() { close(); }

//*****************************************************************************
//    CleanupPopup::OverwriteDialog  implementation
//*****************************************************************************

CleanupPopup::OverwriteDialog::OverwriteDialog()
    : DVGui::ValidatedChoiceDialog(TApp::instance()->getMainWindow()) {
  setWindowTitle(tr("Warning!"));

  bool ret = connect(m_buttonGroup, SIGNAL(buttonClicked(int)),
                     SLOT(onButtonClicked(int)));
  assert(ret);

  // Option 1: OVERWRITE
  QRadioButton *radioButton = new QRadioButton;
  radioButton->setText(
      tr("Cleanup all selected drawings overwriting those previously cleaned "
         "up.*"));
  radioButton->setFixedHeight(20);
  radioButton->setChecked(true);  // initial option: OVERWRITE

  m_buttonGroup->addButton(radioButton, OVERWRITE);
  addWidget(radioButton);

  // Option 2: WRITE_NEW
  radioButton = new QRadioButton;
  radioButton->setText(
      tr("Cleanup only non-cleaned up drawings and keep those previously "
         "cleaned up.*"));
  radioButton->setFixedHeight(20);

  m_buttonGroup->addButton(radioButton, WRITE_NEW);
  addWidget(radioButton);

  // Option 3: REPLACE
  radioButton = new QRadioButton;
  radioButton->setText(
      tr("Delete existing level and create a new level with selected drawings "
         "only."));
  radioButton->setFixedHeight(20);

  m_buttonGroup->addButton(radioButton, REPLACE);
  addWidget(radioButton);

  // Option 4: ADD_SUFFIX
  QHBoxLayout *suffixLayout = new QHBoxLayout;
  {
    radioButton = new QRadioButton;
    radioButton->setText(tr("Rename the new level adding the suffix "));
    radioButton->setFixedHeight(20);

    m_buttonGroup->addButton(radioButton, ADD_SUFFIX);
    suffixLayout->addWidget(radioButton);

    m_suffix = new DVGui::LineEdit;
    m_suffix->setEnabled(false);

    suffixLayout->addWidget(m_suffix);
  }
  addLayout(suffixLayout);  // Couldnt' place it right after allocation,
                            // DVGui::Dialog::addLayout() crashed...
  // Option 5: NOPAINT_ONLY
  radioButton = new QRadioButton(this);
  radioButton->setText(
      tr("This is Re-Cleanup. Overwrite only to the no-paint files."));
  radioButton->setFixedHeight(20);
  m_buttonGroup->addButton(radioButton, NOPAINT_ONLY);
  addWidget(radioButton);

  QLabel *note = new QLabel(tr("* Palette will not be changed."), this);
  note->setStyleSheet("font-size: 10px; font: italic;");
  addWidget(note);

  endVLayout();

  layout()->setSizeConstraint(QLayout::SetFixedSize);
}

//-----------------------------------------------------------------------------

void CleanupPopup::OverwriteDialog::reset() {
  ValidatedChoiceDialog::reset();
  m_suffixText.clear();
}

//-----------------------------------------------------------------------------

QString CleanupPopup::OverwriteDialog::acceptResolution(void *obj,
                                                        int resolution,
                                                        bool applyToAll) {
  struct locals {
    static inline QString existsStr(const TFilePath &fp) {
      return tr("File \"%1\" already exists.\nWhat do you want to do?")
          .arg(fp.getQString());
    }
  };  // locals

  assert(obj);

  TFilePath &fp = *static_cast<TFilePath *>(obj);

  QString error;

  switch (resolution) {
  case NO_RESOLUTION:
    // fp was already found to be invalid
    assert(::exists(fp));
    error = locals::existsStr(fp);

    // Restore previous apply-to-all options if necessary
    if (!error.isEmpty() && appliedToAll()) {
      assert(!m_suffixText.isEmpty());
      m_suffix->setText(m_suffixText);
    }
    break;

  case ADD_SUFFIX:
    // Save resolution options if necessary
    if (applyToAll) m_suffixText = m_suffix->text();

    // Test produced file path
    const TFilePath &fp_suf =
        fp.withName(fp.getWideName() + m_suffix->text().toStdWString());

    if (::exists(fp_suf))
      error = locals::existsStr(fp_suf);
    else
      fp = fp_suf;

    break;
  }

  return error;
}

//-----------------------------------------------------------------------------

void CleanupPopup::OverwriteDialog::initializeUserInteraction(const void *obj) {
  const TFilePath &fp = *static_cast<const TFilePath *>(obj);

  // Generate a suitable initial suffix
  int num = 1;
  for (; ::exists(fp, num); ++num)
    ;

  m_suffix->setText(::suffix(num));
}

//-----------------------------------------------------------------------------

void CleanupPopup::OverwriteDialog::onButtonClicked(int buttonId) {
  m_suffix->setEnabled(buttonId == ADD_SUFFIX);
}

//-----------------------------------------------------------------------------

void CleanupPopup::onImgViewBoxToggled(bool on) {
  m_imageViewer->setVisible(on);
}

//-----------------------------------------------------------------------------
/*!	Show the progress in the mainwindow's title bar
 */
void CleanupPopup::updateTitleString() {
  if (!TApp::instance()->getMainWindow()) return;
  MainWindow *mainWin =
      qobject_cast<MainWindow *>(TApp::instance()->getMainWindow());
  if (!mainWin) return;

  QString str = QString::number(m_completion.first) + "/" +
                QString::number(m_completion.second) +
                tr(" : Cleanup in progress");

  mainWin->changeWindowTitle(str);
}

//*****************************************************************************
//    CleanupCommand  definition
//*****************************************************************************

class CleanupCommand final : public MenuItemHandler {
public:
  CleanupCommand() : MenuItemHandler("MI_Cleanup") {}

  void execute() override {
    static CleanupPopup *popup = new CleanupPopup;
    popup->execute();
  }

} CleanupCommand;