Blob Blame Raw
#include "autoinputcellnumberpopup.h"

// Tnz includes
#include "tapp.h"
#include "menubarcommandids.h"
#include "cellselection.h"
#include "columnselection.h"

#if defined(x64)
#include "penciltestpopup.h"
#else
#include "penciltestpopup_qt.h"
#endif

// TnzQt includes
#include "toonzqt/intfield.h"
#include "toonzqt/tselectionhandle.h"
#include "toonzqt/selection.h"
#include "toonzqt/dvdialog.h"

// TnzLib includes
#include "toonz/txsheethandle.h"
#include "toonz/txshcell.h"
#include "toonz/txshsimplelevel.h"
#include "toonz/txshchildlevel.h"
#include "toonz/preferences.h"

#include "tundo.h"
#include "historytypes.h"

#include <QMainWindow>
#include <QGridLayout>
#include <QLabel>
#include <QPushButton>

#include <iostream>

namespace {
class AutoInputCellNumberUndo final : public TUndo {
  int m_increment, m_interval, m_step, m_repeat;
  int m_from, m_to;
  int m_r0, m_r1;
  bool m_isOverwrite;
  std::vector<int> m_columnIndices;
  std::vector<TXshLevelP> m_levels;

  int m_rowsCount;
  std::unique_ptr<TXshCell[]> m_beforeCells;

public:
  AutoInputCellNumberUndo(int increment, int interval, int step, int repeat,
                          int from, int to, int r0, int r1, bool isOverwrite,
                          std::vector<int> columnIndices,
                          std::vector<TXshLevelP> levels);

  ~AutoInputCellNumberUndo() {}

  void undo() const override;
  void redo() const override;
  int getSize() const override { return sizeof(*this); }

  QString getHistoryString() override {
    return QObject::tr("Auto Input Cell Numbers : %1")
        .arg((m_isOverwrite) ? QObject::tr("Overwrite")
                             : QObject::tr("Insert"));
  }
  int getHistoryType() override { return HistoryType::Xsheet; }

  int rowsCount() { return m_rowsCount; }
};
};  // namespace

//-----------------------------------------------------------------------------
// executing this on column selection, set r1 = -1.
// Here are the expected behaviors
// 1. Cell Selection + Overwrite
//    Cells will be input from the top of the selected range.
//    New arrangement CANNOT run over the selected range.
//    If the new arrangement is shorter than the selected range,
//    excess cells will not be cleared but keep their contents.
//    (It is the same behavior as Celsys' QuickChecker)
// 2. Cell Selection + Insert
//    New arrangement will be inserted before the selected range.
//    If the selected range has multiple columns, then the inserted
//    cells will be expanded to the longest arrangement with empty cells.
// 3. Column Selection + Overwrite
//    Cells will be input from the top of the columns.
//    New arrangement CAN run over the existing column range.
//    If the new arrangement is shorter than the selected range,
//    excess cells will not be cleared but keep their contents.
// 4. Column Selection + Insert
//    New arrangement will be inserted at the top of the columns.
//    If multiple columns are selected, then the inserted cells
//    will be expanded to the longest arrangement with empty cells.
//

AutoInputCellNumberUndo::AutoInputCellNumberUndo(int increment, int interval,
                                                 int step, int repeat, int from,
                                                 int to, int r0, int r1,
                                                 bool isOverwrite,
                                                 std::vector<int> columnIndices,
                                                 std::vector<TXshLevelP> levels)
    : m_increment(increment)
    , m_interval(interval)
    , m_step(step)
    , m_repeat(repeat)
    , m_from(from)
    , m_to(to)
    , m_r0(r0)
    , m_r1(r1)
    , m_isOverwrite(isOverwrite)
    , m_columnIndices(columnIndices)
    , m_levels(levels) {
  if (Preferences::instance()->isShowFrameNumberWithLettersEnabled())
    m_increment *= 10;

  int maxInputFrameAmount = 0;
  int frameRangeMin       = std::min(m_from, m_to);
  int frameRangeMax       = std::max(m_from, m_to);

  if (m_increment == 0) {
    // obtain the maximum frame amount to be input
    for (int lv = 0; lv < m_levels.size(); lv++) {
      TXshLevelP level = m_levels.at(lv);
      std::vector<TFrameId> fids;
      level->getFids(fids);
      int fCount = 0;
      for (auto itr = fids.begin(); itr != fids.end(); ++itr) {
        if ((*itr).getNumber() >= frameRangeMin &&
            (*itr).getNumber() <= frameRangeMax) {
          fCount++;
        } else if ((*itr).getNumber() > frameRangeMax)
          break;
      }
      if (maxInputFrameAmount < fCount) maxInputFrameAmount = fCount;
    }
  } else
    maxInputFrameAmount = (int)std::ceil(
        (float)(frameRangeMax - frameRangeMin + 1) / (float)m_increment);

  maxInputFrameAmount *= m_repeat;

  // compute maximum row amount to be arranged
  if (maxInputFrameAmount == 0) {
    m_rowsCount = 0;
    return;
  } else
    m_rowsCount =
        maxInputFrameAmount * m_step + (maxInputFrameAmount - 1) * m_interval;

  // on overwrite case, keep existing cells for undo
  if (m_isOverwrite) {
    int rowUpTo =
        (m_r1 == -1) ? m_rowsCount - 1 : std::min(m_r1, m_r0 + m_rowsCount - 1);
    m_beforeCells.reset(
        new TXshCell[m_columnIndices.size() * (rowUpTo - m_r0 + 1)]);
    int k = 0;
    for (int c = 0; c < m_columnIndices.size(); ++c) {
      for (int r = m_r0; r <= rowUpTo; ++r)
        m_beforeCells[k++] =
            TApp::instance()->getCurrentXsheet()->getXsheet()->getCell(
                r, m_columnIndices.at(c));
    }
  }
}

void AutoInputCellNumberUndo::undo() const {
  TXsheet *xsh = TApp::instance()->getCurrentXsheet()->getXsheet();
  // on overwrite case, retrieve cells
  if (m_isOverwrite) {
    int rowUpTo =
        (m_r1 == -1) ? m_rowsCount - 1 : std::min(m_r1, m_r0 + m_rowsCount - 1);
    int k = 0;
    for (int c = 0; c < m_columnIndices.size(); ++c) {
      for (int r = m_r0; r <= rowUpTo; ++r) {
        if (m_beforeCells[k].isEmpty())
          xsh->clearCells(r, m_columnIndices.at(c));
        else
          xsh->setCell(r, m_columnIndices.at(c), m_beforeCells[k]);
        k++;
      }
    }
  } else {  // on insert case, remove inserted cells
    for (int c = 0; c < m_columnIndices.size(); ++c)
      xsh->removeCells(m_r0, m_columnIndices.at(c), m_rowsCount);
  }
  TApp::instance()->getCurrentXsheet()->notifyXsheetChanged();
}

void AutoInputCellNumberUndo::redo() const {
  TApp::instance()->getCurrentXsheet()->getXsheet()->autoInputCellNumbers(
      m_increment, m_interval, m_step, m_repeat, m_from, m_to, m_r0, m_r1,
      m_isOverwrite, m_columnIndices, m_levels, m_rowsCount);
  TApp::instance()->getCurrentXsheet()->notifyXsheetChanged();
}

//=============================================================================

AutoInputCellNumberPopup::AutoInputCellNumberPopup()
    : Dialog(TApp::instance()->getMainWindow(), false, true,
             "AutoInputCellNumberPopup") {
  setWindowTitle(tr("Auto Input Cell Number"));

  m_from      = new FrameNumberLineEdit(this, TFrameId(1), false);
  m_increment = new DVGui::IntLineEdit(this, 0, 0);
  m_to        = new FrameNumberLineEdit(this, TFrameId(1), false);
  m_interval  = new DVGui::IntLineEdit(this, 0, 0);
  m_step      = new DVGui::IntLineEdit(this, 1, 1);
  m_repeat    = new DVGui::IntLineEdit(this, 1, 1);

  m_overwriteBtn         = new QPushButton(tr("Overwrite"), this);
  m_insertBtn            = new QPushButton(tr("Insert"), this);
  QPushButton *cancelBtn = new QPushButton(tr("Cancel"), this);

  m_increment->setToolTip(
      tr("Setting this value 0 will automatically \npick up all frames in the "
         "selected level."));

  // layout

  QGridLayout *mainLay = new QGridLayout();
  mainLay->setHorizontalSpacing(5);
  mainLay->setVerticalSpacing(10);
  {
    mainLay->addWidget(new QLabel(tr("From frame"), this), 0, 0,
                       Qt::AlignRight | Qt::AlignVCenter);
    mainLay->addWidget(m_from, 0, 1);
    mainLay->addWidget(new QLabel(tr(" ", "from frame"), this), 0, 2, 1, 2);

    mainLay->addWidget(new QLabel(tr("with"), this), 1, 0, 1, 2,
                       Qt::AlignRight | Qt::AlignVCenter);
    mainLay->addWidget(m_increment, 1, 2);
    mainLay->addWidget(new QLabel(tr("frames increment"), this), 1, 3);

    mainLay->addWidget(new QLabel(tr("To frame"), this), 2, 0,
                       Qt::AlignRight | Qt::AlignVCenter);
    mainLay->addWidget(m_to, 2, 1);
    mainLay->addWidget(new QLabel(tr(" ", "to frame"), this), 2, 2, 1, 2);

    mainLay->addWidget(new QLabel(tr("inserting"), this), 3, 0, 1, 2,
                       Qt::AlignRight | Qt::AlignVCenter);
    mainLay->addWidget(m_interval, 3, 2);
    mainLay->addWidget(new QLabel(tr("empty cell intervals"), this), 3, 3);

    mainLay->addWidget(new QLabel(tr("with"), this), 4, 0,
                       Qt::AlignRight | Qt::AlignVCenter);
    mainLay->addWidget(m_step, 4, 1);
    mainLay->addWidget(new QLabel(tr("cell steps"), this), 4, 2, 1, 2);

    mainLay->addWidget(new QLabel(tr("Repeat"), this), 5, 0,
                       Qt::AlignRight | Qt::AlignVCenter);
    mainLay->addWidget(m_repeat, 5, 1);
    mainLay->addWidget(new QLabel(tr("times"), this), 5, 2, 1, 2);
  }
  m_topLayout->addLayout(mainLay);

  m_buttonFrame = new QFrame(this);
  m_buttonFrame->setObjectName("dialogButtonFrame");
  m_buttonFrame->setFrameStyle(QFrame::StyledPanel);
  m_buttonFrame->setFixedHeight(45);

  m_buttonLayout = new QHBoxLayout;
  m_buttonLayout->setMargin(0);
  m_buttonLayout->setSpacing(20);
  m_buttonLayout->setAlignment(Qt::AlignHCenter);
  {
    m_buttonLayout->addSpacing(20);
    m_buttonLayout->addWidget(m_overwriteBtn, 0);
    m_buttonLayout->addWidget(m_insertBtn, 0);
    m_buttonLayout->addWidget(cancelBtn, 0);
    m_buttonLayout->addSpacing(20);
  }
  m_buttonFrame->setLayout(m_buttonLayout);
  layout()->addWidget(m_buttonFrame);

  // signal-slot connections
  bool ret = true;
  ret      = ret && connect(m_overwriteBtn, SIGNAL(clicked()), this,
                       SLOT(onOverwritePressed()));
  ret      = ret &&
        connect(m_insertBtn, SIGNAL(clicked()), this, SLOT(onInsertPressed()));
  ret = ret && connect(cancelBtn, SIGNAL(clicked()), this, SLOT(close()));
  assert(ret);
}

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

void AutoInputCellNumberPopup::onOverwritePressed() { doExecute(true); }

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

void AutoInputCellNumberPopup::onInsertPressed() { doExecute(false); }

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

void AutoInputCellNumberPopup::doExecute(bool overwrite) {
  std::vector<int> columnIndices;
  std::vector<TXshLevelP> levels;
  int r0, r1;
  bool found = getTarget(columnIndices, levels, r0, r1);
  // just in case the execution is done to empty column
  if (!found) {
    DVGui::MsgBox(DVGui::WARNING,
                  tr("No available cells or columns are selected."));
    return;
  }
  AutoInputCellNumberUndo *undo = new AutoInputCellNumberUndo(
      m_increment->getValue(), m_interval->getValue(), m_step->getValue(),
      m_repeat->getValue(), m_from->getValue().getNumber(),
      m_to->getValue().getNumber(), r0, r1, overwrite, columnIndices, levels);
  // if no cells will be arranged, then return
  if (undo->rowsCount() == 0) {
    DVGui::MsgBox(DVGui::WARNING,
                  tr("Selected level has no frames between From and To."));
    delete undo;
    return;
  }
  undo->redo();
  TUndoManager::manager()->add(undo);
  // on cell selection & insert case, select the inserted cell range
  if (!overwrite && r1 != -1) {
    TSelection *selection =
        TApp::instance()->getCurrentSelection()->getSelection();
    if (!selection) return;
    TCellSelection *cellSelection = dynamic_cast<TCellSelection *>(selection);
    if (!cellSelection) return;
    int c0, c1;
    cellSelection->getSelectedCells(r0, c0, r1, c1);
    cellSelection->selectCells(r0, c0, r0 + undo->rowsCount() - 1, c1);
    TApp::instance()->getCurrentSelection()->notifySelectionChanged();
  }
  // If execution is properly completed, then close this popup
  close();
}

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

void AutoInputCellNumberPopup::onSelectionChanged() {
  // enable buttons only if they will work
  m_overwriteBtn->setEnabled(false);
  m_insertBtn->setEnabled(false);

  std::vector<int> columnIndices;
  std::vector<TXshLevelP> levels;
  int r0, r1;
  bool found = getTarget(columnIndices, levels, r0, r1, true);

  // if no reference level is found, return
  if (!found) return;

  int from, to;
  std::vector<TFrameId> fids;
  levels.front()->getFids(fids);
  from = fids.front().getNumber();
  to   = fids.back().getNumber();

  m_from->setValue(from);
  m_to->setValue(to);

  // enable buttons
  m_overwriteBtn->setEnabled(true);
  m_insertBtn->setEnabled(true);
}

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

void AutoInputCellNumberPopup::showEvent(QShowEvent *) {
  bool ret = connect(TApp::instance()->getCurrentSelection(),
                     SIGNAL(selectionChanged(TSelection *)), this,
                     SLOT(onSelectionChanged()));
  assert(ret);
  onSelectionChanged();
}

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

void AutoInputCellNumberPopup::hideEvent(QHideEvent *) {
  bool ret = disconnect(TApp::instance()->getCurrentSelection(),
                        SIGNAL(selectionChanged(TSelection *)), this,
                        SLOT(onSelectionChanged()));
  assert(ret);
}

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

bool AutoInputCellNumberPopup::getTarget(std::vector<int> &columnIndices,
                                         std::vector<TXshLevelP> &levels,
                                         int &r0, int &r1, bool forCheck) {
  TSelection *selection =
      TApp::instance()->getCurrentSelection()->getSelection();
  if (!selection) return false;

  // something must be selected
  if (selection->isEmpty()) return false;

  // selection must be cells or columns
  TCellSelection *cellSelection = dynamic_cast<TCellSelection *>(selection);
  TColumnSelection *columnSelection =
      dynamic_cast<TColumnSelection *>(selection);
  if (!cellSelection && !columnSelection) return false;

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

  if (cellSelection) {
    // if cells are selected, selected range must contain at least one available
    // level
    int c0, c1;
    cellSelection->getSelectedCells(r0, c0, r1, c1);
    for (int col = c0; col <= c1; col++) {
      // column should not be empty or locked
      if (xsh->isColumnEmpty(col)) continue;
      TXshColumn *column = xsh->getColumn(col);
      if (column->isLocked()) continue;
      // column type should be "level"
      if (column->getColumnType() != TXshColumn::eLevelType) continue;
      // try to find the topmost available level in the column
      for (int row = r0; row <= r1; row++) {
        TXshCell cell = xsh->getCell(row, col);
        if (cell.isEmpty()) continue;
        TXshSimpleLevel *simpleLevel = cell.getSimpleLevel();
        TXshChildLevel *childLevel   = cell.getChildLevel();
        if (!simpleLevel && !childLevel) continue;
        if (simpleLevel) {
          TFrameId fid = simpleLevel->getFirstFid();
          // the level should not be single-framed
          if (fid.getNumber() == TFrameId::EMPTY_FRAME ||
              fid.getNumber() == TFrameId::NO_FRAME)
            continue;
        }
        levels.push_back(cell.m_level);
        columnIndices.push_back(col);
        break;
      }
      // if some level found, break the loop for check
      if (forCheck && !levels.empty()) break;
    }
  }

  else {  // columnSelection case
    r0                    = 0;
    r1                    = -1;
    std::set<int> indices = columnSelection->getIndices();
    // try to find the leftmost available column
    for (auto id = indices.begin(); id != indices.end(); ++id) {
      TXshColumn *column = xsh->getColumn(*id);
      if (!column) continue;
      if (column->isEmpty() || column->isLocked()) continue;
      // column type should be "level"
      if (column->getColumnType() != TXshColumn::eLevelType) continue;
      // try to find the topmost available level in the column
      TXshCellColumn *xshColumn = column->getCellColumn();
      if (!xshColumn) continue;
      int tmpR0, tmpR1;
      column->getRange(tmpR0, tmpR1);
      for (int row = tmpR0; row <= tmpR1; row++) {
        TXshCell cell = xshColumn->getCell(row);
        if (cell.isEmpty()) continue;
        TXshSimpleLevel *simpleLevel = cell.getSimpleLevel();
        TXshChildLevel *childLevel   = cell.getChildLevel();
        if (!simpleLevel && !childLevel) continue;
        if (simpleLevel) {
          TFrameId fid = simpleLevel->getFirstFid();
          // the level should not be single-framed
          if (fid.getNumber() == TFrameId::EMPTY_FRAME ||
              fid.getNumber() == TFrameId::NO_FRAME)
            continue;
        }
        levels.push_back(cell.m_level);
        columnIndices.push_back(*id);
        break;
      }
      // if some level found, break the loop for check
      if (forCheck && !levels.empty()) break;
    }
  }

  return !levels.empty();
}

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

OpenPopupCommandHandler<AutoInputCellNumberPopup> openAutoInputCellNumberPopup(
    MI_AutoInputCellNumber);