Blob Blame Raw
#include "penciltestpopup_qt.h"

// Tnz6 includes
#include "tapp.h"
#include "menubarcommandids.h"
#include "formatsettingspopups.h"
#include "filebrowsermodel.h"
#include "cellselection.h"
#include "toonzqt/tselectionhandle.h"
#include "cameracapturelevelcontrol_qt.h"
#include "iocommand.h"

// TnzQt includes
#include "toonzqt/menubarcommand.h"
#include "toonzqt/filefield.h"
#include "toonzqt/intfield.h"
#include "toonzqt/gutil.h"

// Tnzlib includes
#include "toonz/tproject.h"
#include "toonz/tscenehandle.h"
#include "toonz/toonzscene.h"
#include "toutputproperties.h"
#include "toonz/sceneproperties.h"
#include "toonz/levelset.h"
#include "toonz/txshleveltypes.h"
#include "toonz/toonzfolders.h"
#include "toonz/tframehandle.h"
#include "toonz/tcolumnhandle.h"
#include "toonz/txsheethandle.h"
#include "toonz/txshsimplelevel.h"
#include "toonz/levelproperties.h"
#include "toonz/tcamera.h"
#include "toonz/preferences.h"
#include "toonz/filepathproperties.h"

// TnzCore includes
#include "tsystem.h"
#include "tpixelutils.h"
#include "tenv.h"
#include "tlevel_io.h"

#include <algorithm>

// Qt includes
#include <QMainWindow>
#include <QCameraInfo>
#include <QCamera>
#include <QCameraImageCapture>
#include <QCameraViewfinderSettings>
#ifdef MACOSX
#include <QCameraViewfinder>
#endif

#include <QComboBox>
#include <QPushButton>
#include <QLineEdit>
#include <QGroupBox>
#include <QRadioButton>
#include <QSlider>
#include <QCheckBox>
#include <QLabel>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QGridLayout>
#include <QToolButton>
#include <QDateTime>
#include <QMultimedia>
#include <QPainter>
#include <QKeyEvent>
#include <QCommonStyle>
#include <QTimer>
#include <QIntValidator>
#include <QRegExpValidator>

#include <QVideoSurfaceFormat>
#include <QThreadPool>

#ifdef _WIN32
#include <dshow.h>
#endif

using namespace DVGui;

// Connected camera
TEnv::StringVar CamCapCameraName("CamCapCameraName", "");
// Camera resolution
TEnv::StringVar CamCapCameraResolution("CamCapCameraResolution", "");
// Whether to open save-in popup on launch
TEnv::IntVar CamCapOpenSaveInPopupOnLaunch("CamCapOpenSaveInPopupOnLaunch", 0);
// SaveInFolderPopup settings
TEnv::StringVar CamCapSaveInParentFolder("CamCapSaveInParentFolder", "");
TEnv::IntVar CamCapSaveInPopupSubFolder("CamCapSaveInPopupSubFolder", 0);
TEnv::StringVar CamCapSaveInPopupProject("CamCapSaveInPopupProject", "");
TEnv::StringVar CamCapSaveInPopupEpisode("CamCapSaveInPopupEpisode", "1");
TEnv::StringVar CamCapSaveInPopupSequence("CamCapSaveInPopupSequence", "1");
TEnv::StringVar CamCapSaveInPopupScene("CamCapSaveInPopupScene", "1");
TEnv::IntVar CamCapSaveInPopupAutoSubName("CamCapSaveInPopupAutoSubName", 1);
TEnv::IntVar CamCapSaveInPopupCreateSceneInFolder(
    "CamCapSaveInPopupCreateSceneInFolder", 0);

namespace {

void convertImageToRaster(TRaster32P dstRas, const QImage& srcImg) {
  dstRas->lock();
  int lx = dstRas->getLx();
  int ly = dstRas->getLy();
  assert(lx == srcImg.width() && ly == srcImg.height());
  for (int j = 0; j < ly; j++) {
    TPixel32* dstPix = dstRas->pixels(j);
    for (int i = 0; i < lx; i++, dstPix++) {
      QRgb srcPix = srcImg.pixel(lx - 1 - i, j);
      dstPix->r   = qRed(srcPix);
      dstPix->g   = qGreen(srcPix);
      dstPix->b   = qBlue(srcPix);
      dstPix->m   = TPixel32::maxChannelValue;
    }
  }
  dstRas->unlock();
}

void bgReduction(QImage& srcImg, QImage& bgImg, int reduction) {
  float reductionRatio = (float)reduction / 100.0f;
  // first, make the reduction table
  std::vector<int> reductionAmount(256);
  for (int i = 0; i < reductionAmount.size(); i++) {
    reductionAmount[i] = (int)(std::floor((float)(255 - i) * reductionRatio));
  }
  // then, compute for all pixels
  int lx = srcImg.width();
  int ly = srcImg.height();
  for (int j = 0; j < ly; j++) {
    // TPixel32 * pix = ras->pixels(j);
    QRgb* pix   = (QRgb*)srcImg.scanLine(j);
    QRgb* bgPix = (QRgb*)bgImg.scanLine(j);
    for (int i = 0; i < lx; i++, pix++, bgPix++) {
      *pix = qRgb(std::min(255, qRed(*pix) + reductionAmount[qRed(*bgPix)]),
                  std::min(255, qGreen(*pix) + reductionAmount[qGreen(*bgPix)]),
                  std::min(255, qBlue(*pix) + reductionAmount[qBlue(*bgPix)]));
    }
  }
}

void my_compute_lut(int black, int white, float gamma, std::vector<int>& lut) {
  const int maxChannelValue         = lut.size() - 1;
  const float half_maxChannelValueF = 0.5f * maxChannelValue;
  const float maxChannelValueF      = maxChannelValue;

  float value;

  int lutSize = lut.size();
  for (int i = 0; i < lutSize; i++) {
    if (i <= black)
      value = 0.0f;
    else if (i >= white)
      value = 1.0f;
    else {
      value = (float)(i - black) / (float)(white - black);
      value = std::pow(value, 1.0f / gamma);
    }

    lut[i] = (int)std::floor(value * maxChannelValueF);
  }
}

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

inline void doPixGray(QRgb* pix, const std::vector<int>& lut) {
  int gray = lut[qGray(*pix)];
  *pix     = qRgb(gray, gray, gray);
}

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

inline void doPixBinary(QRgb* pix, int threshold) {
  int gray = qGray(*pix);
  if (gray >= threshold)
    gray = 255;
  else
    gray = 0;
  *pix = qRgb(gray, gray, gray);
}

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

inline void doPix(QRgb* pix, const std::vector<int>& lut) {
  // The captured image MUST be full opaque!
  *pix = qRgb(lut[qRed(*pix)], lut[qGreen(*pix)], lut[qBlue(*pix)]);
}

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

void onChange(QImage& img, int black, int white, float gamma, bool doGray) {
  std::vector<int> lut(TPixel32::maxChannelValue + 1);
  my_compute_lut(black, white, gamma, lut);

  int ly = img.height();
  // compute in multi thread
  int threadCount =
      std::max(1, QThreadPool::globalInstance()->maxThreadCount() / 2);
  int tmpStart = 0;
  for (int t = 0; t < threadCount; t++) {
    int tmpEnd = (int)std::round((float)(ly * (t + 1)) / (float)threadCount);

    QRunnable* task;
    if (doGray)
      task = new ApplyGrayLutTask(tmpStart, tmpEnd, img, lut);
    else
      task = new ApplyLutTask(tmpStart, tmpEnd, img, lut);

    QThreadPool::globalInstance()->start(task);

    tmpStart = tmpEnd;
  }

  QThreadPool::globalInstance()->waitForDone();
}

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

void onChangeBW(QImage& img, int threshold) {
  int lx = img.width(), y, ly = img.height();
  for (y = 0; y < ly; ++y) {
    QRgb *pix = (QRgb*)img.scanLine(y), *endPix = (QRgb*)(pix + lx);
    while (pix < endPix) {
      doPixBinary(pix, threshold);
      ++pix;
    }
  }
}

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

TPointD getCurrentCameraDpi() {
  TCamera* camera =
      TApp::instance()->getCurrentScene()->getScene()->getCurrentCamera();
  TDimensionD size = camera->getSize();
  TDimension res   = camera->getRes();
  return TPointD(res.lx / size.lx, res.ly / size.ly);
}

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

QChar numToLetter(int letterNum) {
  switch (letterNum) {
  case 0:
    return QChar();
    break;
  case 1:
    return 'A';
    break;
  case 2:
    return 'B';
    break;
  case 3:
    return 'C';
    break;
  case 4:
    return 'D';
    break;
  case 5:
    return 'E';
    break;
  case 6:
    return 'F';
    break;
  case 7:
    return 'G';
    break;
  case 8:
    return 'H';
    break;
  case 9:
    return 'I';
    break;
  default:
    return QChar();
    break;
  }
}

int letterToNum(QChar appendix) {
  if (appendix == QChar('A') || appendix == QChar('a'))
    return 1;
  else if (appendix == QChar('B') || appendix == QChar('b'))
    return 2;
  else if (appendix == QChar('C') || appendix == QChar('c'))
    return 3;
  else if (appendix == QChar('D') || appendix == QChar('d'))
    return 4;
  else if (appendix == QChar('E') || appendix == QChar('e'))
    return 5;
  else if (appendix == QChar('F') || appendix == QChar('f'))
    return 6;
  else if (appendix == QChar('G') || appendix == QChar('g'))
    return 7;
  else if (appendix == QChar('H') || appendix == QChar('h'))
    return 8;
  else if (appendix == QChar('I') || appendix == QChar('i'))
    return 9;
  else
    return 0;
}

#ifdef _WIN32
void openCaptureFilterSettings(const QWidget* parent,
                               const QString& cameraName) {
  HRESULT hr;

  ICreateDevEnum* createDevEnum = NULL;
  IEnumMoniker* enumMoniker     = NULL;
  IMoniker* moniker             = NULL;

  IBaseFilter* deviceFilter;

  ISpecifyPropertyPages* specifyPropertyPages;
  CAUUID cauuid;
  // set parent's window handle in order to make the dialog modal
  HWND ghwndApp = (HWND)(parent->winId());

  // initialize COM
  CoInitialize(NULL);

  // get device list
  CoCreateInstance(CLSID_SystemDeviceEnum, NULL, CLSCTX_INPROC_SERVER,
                   IID_ICreateDevEnum, (PVOID*)&createDevEnum);

  // create EnumMoniker
  createDevEnum->CreateClassEnumerator(CLSID_VideoInputDeviceCategory,
                                       &enumMoniker, 0);
  if (enumMoniker == NULL) {
    // if no connected devices found
    return;
  }

  // reset EnumMoniker
  enumMoniker->Reset();

  // find target camera
  ULONG fetched      = 0;
  bool isCameraFound = false;
  while (hr = enumMoniker->Next(1, &moniker, &fetched), hr == S_OK) {
    // get friendly name (= device name) of the camera
    IPropertyBag* pPropertyBag;
    moniker->BindToStorage(0, 0, IID_IPropertyBag, (void**)&pPropertyBag);
    VARIANT var;
    var.vt = VT_BSTR;
    VariantInit(&var);

    pPropertyBag->Read(L"FriendlyName", &var, 0);

    QString deviceName = QString::fromWCharArray(var.bstrVal);

    VariantClear(&var);

    if (deviceName == cameraName) {
      // bind monkier to the filter
      moniker->BindToObject(0, 0, IID_IBaseFilter, (void**)&deviceFilter);

      // release moniker etc.
      moniker->Release();
      enumMoniker->Release();
      createDevEnum->Release();

      isCameraFound = true;
      break;
    }
  }

  // if no matching camera found
  if (!isCameraFound) return;

  // open capture filter popup
  hr = deviceFilter->QueryInterface(IID_ISpecifyPropertyPages,
                                    (void**)&specifyPropertyPages);
  if (hr == S_OK) {
    hr = specifyPropertyPages->GetPages(&cauuid);

    hr = OleCreatePropertyFrame(ghwndApp, 30, 30, NULL, 1,
                                (IUnknown**)&deviceFilter, cauuid.cElems,
                                (GUID*)cauuid.pElems, 0, 0, NULL);

    CoTaskMemFree(cauuid.pElems);
    specifyPropertyPages->Release();
  }
}
#endif

QString convertToFrameWithLetter(int value, int length = -1) {
  QString str;
  str.setNum((int)(value / 10));
  while (str.length() < length) str.push_front("0");
  QChar letter = numToLetter(value % 10);
  if (!letter.isNull()) str.append(letter);
  return str;
}

QString fidsToString(const std::vector<TFrameId>& fids,
                     bool letterOptionEnabled) {
  if (fids.empty()) return PencilTestPopup::tr("No", "frame id");
  QString retStr("");
  if (letterOptionEnabled) {
    bool beginBlock = true;
    for (int f = 0; f < fids.size() - 1; f++) {
      int num      = fids[f].getNumber();
      int next_num = fids[f + 1].getNumber();

      if (num % 10 == 0 && num + 10 == next_num) {
        if (beginBlock) {
          retStr += convertToFrameWithLetter(num) + " - ";
          beginBlock = false;
        }
      } else {
        retStr += convertToFrameWithLetter(num) + ", ";
        beginBlock = true;
      }
    }
    retStr += convertToFrameWithLetter(fids.back().getNumber());
  } else {
    bool beginBlock = true;
    for (int f = 0; f < fids.size() - 1; f++) {
      int num      = fids[f].getNumber();
      int next_num = fids[f + 1].getNumber();
      if (num + 1 == next_num) {
        if (beginBlock) {
          retStr += QString::number(num) + " - ";
          beginBlock = false;
        }
      } else {
        retStr += QString::number(num) + ", ";
        beginBlock = true;
      }
    }
    retStr += QString::number(fids.back().getNumber());
  }
  return retStr;
}

bool findCell(TXsheet* xsh, int col, const TXshCell& targetCell,
              int& bottomRowWithTheSameLevel) {
  bottomRowWithTheSameLevel = -1;
  TXshColumnP column        = const_cast<TXsheet*>(xsh)->getColumn(col);
  if (!column) return false;

  TXshCellColumn* cellColumn = column->getCellColumn();
  if (!cellColumn) return false;

  int r0, r1;
  if (!cellColumn->getRange(r0, r1)) return false;

  for (int r = r0; r <= r1; r++) {
    TXshCell cell = cellColumn->getCell(r);
    if (cell == targetCell) return true;
    if (cell.m_level == targetCell.m_level) bottomRowWithTheSameLevel = r;
  }

  return false;
}

bool getRasterLevelSize(TXshLevel* level, TDimension& dim) {
  std::vector<TFrameId> fids;
  level->getFids(fids);
  if (fids.empty()) return false;
  TXshSimpleLevel* simpleLevel = level->getSimpleLevel();
  if (!simpleLevel) return false;
  TRasterImageP rimg = (TRasterImageP)simpleLevel->getFrame(fids[0], false);
  if (!rimg || rimg->isEmpty()) return false;

  dim = rimg->getRaster()->getSize();
  return true;
}

}  // namespace

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

void ApplyLutTask::run() {
  int lx = m_img.width();
  for (int y = m_fromY; y < m_toY; ++y) {
    QRgb *pix = (QRgb*)m_img.scanLine(y), *endPix = (QRgb*)(pix + lx);
    while (pix < endPix) {
      doPix(pix, m_lut);
      ++pix;
    }
  }
}

void ApplyGrayLutTask::run() {
  int lx = m_img.width();
  for (int y = m_fromY; y < m_toY; ++y) {
    QRgb *pix = (QRgb*)m_img.scanLine(y), *endPix = (QRgb*)(pix + lx);
    while (pix < endPix) {
      doPixGray(pix, m_lut);
      ++pix;
    }
  }
}

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

MyVideoSurface::MyVideoSurface(QWidget* widget, QObject* parent)
    : QAbstractVideoSurface(parent)
    , m_widget(widget)
    , m_imageFormat(QImage::Format_Invalid) {}

QList<QVideoFrame::PixelFormat> MyVideoSurface::supportedPixelFormats(
    QAbstractVideoBuffer::HandleType handleType) const {
  if (handleType == QAbstractVideoBuffer::NoHandle) {
    return QList<QVideoFrame::PixelFormat>()
           << QVideoFrame::Format_RGB32 << QVideoFrame::Format_ARGB32
           << QVideoFrame::Format_ARGB32_Premultiplied
           << QVideoFrame::Format_RGB565 << QVideoFrame::Format_RGB555;
  } else {
    return QList<QVideoFrame::PixelFormat>();
  }
}

bool MyVideoSurface::isFormatSupported(const QVideoSurfaceFormat& format,
                                       QVideoSurfaceFormat* similar) const {
  Q_UNUSED(similar);

  const QImage::Format imageFormat =
      QVideoFrame::imageFormatFromPixelFormat(format.pixelFormat());
  const QSize size = format.frameSize();

  return imageFormat != QImage::Format_Invalid && !size.isEmpty() &&
         format.handleType() == QAbstractVideoBuffer::NoHandle;
}

bool MyVideoSurface::start(const QVideoSurfaceFormat& format) {
  const QImage::Format imageFormat =
      QVideoFrame::imageFormatFromPixelFormat(format.pixelFormat());
  const QSize size = format.frameSize();

  if (imageFormat != QImage::Format_Invalid && !size.isEmpty()) {
    m_imageFormat = imageFormat;
    m_imageSize   = size;
    m_sourceRect  = format.viewport();

    QAbstractVideoSurface::start(format);

    m_widget->updateGeometry();
    updateVideoRect();

    return true;
  } else {
    return false;
  }
}

void MyVideoSurface::updateVideoRect() {
  QSize size = surfaceFormat().sizeHint();
  size.scale(m_widget->size(), Qt::KeepAspectRatio);

  m_targetRect = QRect(QPoint(0, 0), size);
  m_targetRect.moveCenter(m_widget->rect().center());

  double scale =
      (double)m_targetRect.width() / (double)surfaceFormat().sizeHint().width();
  m_S2V_Transform =
      QTransform::fromTranslate(m_targetRect.left(), m_targetRect.top())
          .scale(scale, scale);
}

bool MyVideoSurface::present(const QVideoFrame& frame) {
  if (surfaceFormat().pixelFormat() != frame.pixelFormat() ||
      surfaceFormat().frameSize() != frame.size()) {
    setError(IncorrectFormatError);
    stop();
    return false;
  } else {
    m_currentFrame = frame;

    if (m_currentFrame.map(QAbstractVideoBuffer::ReadOnly)) {
      QImage image = QImage(m_currentFrame.bits(), m_currentFrame.width(),
                            m_currentFrame.height(),
                            m_currentFrame.bytesPerLine(), m_imageFormat);
      m_currentFrame.unmap();
      if (!image.isNull()) emit frameCaptured(image);
    }

    return true;
  }
}

void MyVideoSurface::stop() {
  m_currentFrame = QVideoFrame();
  m_targetRect   = QRect();

  QAbstractVideoSurface::stop();

  m_widget->update();
}

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

MyVideoWidget::MyVideoWidget(QWidget* parent)
    : QWidget(parent)
    , m_previousImage(QImage())
    , m_surface(0)
    , m_showOnionSkin(false)
    , m_onionOpacity(128)
    , m_upsideDown(false)
    , m_countDownTime(0)
    , m_subCameraRect(QRect()) {
  setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);

  m_surface = new MyVideoSurface(this);
  setMouseTracking(true);
}

MyVideoWidget::~MyVideoWidget() { delete m_surface; }

QSize MyVideoWidget::sizeHint() const {
  return m_surface->surfaceFormat().sizeHint();
}

void MyVideoWidget::paintEvent(QPaintEvent* event) {
  QPainter p(this);

  p.fillRect(rect(), Qt::black);

  if (m_surface->isActive()) {
    const QRect videoRect         = m_surface->videoRect();
    const QTransform oldTransform = p.transform();

    if (m_upsideDown) {
      p.translate(videoRect.center());
      p.rotate(180);
      p.translate(-videoRect.center());
    }
    if (m_surface->surfaceFormat().scanLineDirection() ==
        QVideoSurfaceFormat::BottomToTop) {
      p.scale(1, -1);
      p.translate(0, -height());
    }

    p.drawImage(videoRect, m_image, m_surface->sourceRect());

    p.setTransform(oldTransform);

    if (m_showOnionSkin && m_onionOpacity > 0.0f && !m_previousImage.isNull() &&
        m_previousImage.size() == m_image.size()) {
      p.setOpacity((qreal)m_onionOpacity / 255.0);
      p.drawImage(videoRect, m_previousImage, m_surface->sourceRect());
      p.setOpacity(1.0);
    }

    // draw subcamera
    if (m_subCameraRect.isValid()) drawSubCamera(p);

    // draw countdown text
    if (m_countDownTime > 0) {
      QString str =
          QTime::fromMSecsSinceStartOfDay(m_countDownTime).toString("s.zzz");
      p.setPen(Qt::yellow);
      QFont font = p.font();
      font.setPixelSize(50);
      p.setFont(font);
      p.drawText(rect(), Qt::AlignRight | Qt::AlignBottom, str);
    }
  } else {
    p.setPen(Qt::white);
    QFont font = p.font();
    font.setPixelSize(30);
    p.setFont(font);
    p.drawText(rect(), Qt::AlignCenter, tr("Camera is not available"));
  }
}

void MyVideoWidget::resizeEvent(QResizeEvent* event) {
  QWidget::resizeEvent(event);

  m_surface->updateVideoRect();
}

void MyVideoWidget::setSubCameraSize(QSize size) {
  QSize frameSize = m_surface->surfaceFormat().frameSize();
  assert(frameSize == size.expandedTo(frameSize));

  m_subCameraRect.setSize(size);
  // make sure the sub camera is inside of the frame
  if (!QRect(QPoint(0, 0), frameSize).contains(m_subCameraRect))
    m_subCameraRect.moveCenter(QRect(QPoint(0, 0), frameSize).center());

  update();
}

void MyVideoWidget::drawSubCamera(QPainter& p) {
  auto drawSubFrameLine = [&](SUBHANDLE handle, QPoint from, QPoint to) {
    p.setPen(QPen(handle == m_activeSubHandle ? Qt::green : Qt::magenta, 2));
    p.drawLine(from, to);
  };

  auto drawHandle = [&](SUBHANDLE handle, QPoint pos) {
    p.setPen(handle == m_activeSubHandle ? Qt::green : Qt::magenta);
    QRect handleRect(0, 0, 11, 11);
    handleRect.moveCenter(pos);
    p.drawRect(handleRect);
  };

  QRect vidSubRect = m_surface->transform().mapRect(m_subCameraRect);
  p.setBrush(Qt::NoBrush);
  drawSubFrameLine(HandleLeft, vidSubRect.topLeft(), vidSubRect.bottomLeft());
  drawSubFrameLine(HandleTop, vidSubRect.topLeft(), vidSubRect.topRight());
  drawSubFrameLine(HandleRight, vidSubRect.topRight(),
                   vidSubRect.bottomRight());
  drawSubFrameLine(HandleBottom, vidSubRect.bottomLeft(),
                   vidSubRect.bottomRight());

  // draw handles
  drawHandle(HandleTopLeft, vidSubRect.topLeft());
  drawHandle(HandleTopRight, vidSubRect.topRight());
  drawHandle(HandleBottomLeft, vidSubRect.bottomLeft());
  drawHandle(HandleBottomRight, vidSubRect.bottomRight());

  // draw cross mark at subcamera center when the cursor is in the frame
  if (m_activeSubHandle != HandleNone) {
    p.setPen(QPen(Qt::magenta, 1, Qt::DashLine));
    QPoint crossP(vidSubRect.width() / 40, vidSubRect.height() / 40);
    p.drawLine(vidSubRect.center() - crossP, vidSubRect.center() + crossP);
    crossP.setX(-crossP.x());
    p.drawLine(vidSubRect.center() - crossP, vidSubRect.center() + crossP);
  }
}

void MyVideoWidget::mouseMoveEvent(QMouseEvent* event) {
  int d = 10;

  auto isNearBy = [&](QPoint handlePos) -> bool {
    return (handlePos - event->pos()).manhattanLength() <= d * 2;
  };

  auto isNearEdge = [&](int handlePos, int mousePos) -> bool {
    return std::abs(handlePos - mousePos) <= d;
  };

  // if the sub camera is not active, do nothing and return
  if (!m_surface->isActive() || m_subCameraRect.isNull()) return;

  // with no mouse button, update the active handles
  if (event->buttons() == Qt::NoButton) {
    QRect vidSubRect    = m_surface->transform().mapRect(m_subCameraRect);
    SUBHANDLE preHandle = m_activeSubHandle;
    if (!vidSubRect.adjusted(-d, -d, d, d).contains(event->pos()))
      m_activeSubHandle = HandleNone;
    else if (vidSubRect.adjusted(d, d, -d, -d).contains(event->pos()))
      m_activeSubHandle = HandleFrame;
    else if (isNearBy(vidSubRect.topLeft()))
      m_activeSubHandle = HandleTopLeft;
    else if (isNearBy(vidSubRect.topRight()))
      m_activeSubHandle = HandleTopRight;
    else if (isNearBy(vidSubRect.bottomLeft()))
      m_activeSubHandle = HandleBottomLeft;
    else if (isNearBy(vidSubRect.bottomRight()))
      m_activeSubHandle = HandleBottomRight;
    else if (isNearEdge(vidSubRect.left(), event->pos().x()))
      m_activeSubHandle = HandleLeft;
    else if (isNearEdge(vidSubRect.top(), event->pos().y()))
      m_activeSubHandle = HandleTop;
    else if (isNearEdge(vidSubRect.right(), event->pos().x()))
      m_activeSubHandle = HandleRight;
    else if (isNearEdge(vidSubRect.bottom(), event->pos().y()))
      m_activeSubHandle = HandleBottom;
    else
      m_activeSubHandle = HandleNone;
    if (preHandle != m_activeSubHandle) {
      Qt::CursorShape cursor;
      if (m_activeSubHandle == HandleNone)
        cursor = Qt::ArrowCursor;
      else if (m_activeSubHandle == HandleFrame)
        cursor = Qt::SizeAllCursor;
      else if (m_activeSubHandle == HandleTopLeft ||
               m_activeSubHandle == HandleBottomRight)
        cursor = Qt::SizeFDiagCursor;
      else if (m_activeSubHandle == HandleTopRight ||
               m_activeSubHandle == HandleBottomLeft)
        cursor = Qt::SizeBDiagCursor;
      else if (m_activeSubHandle == HandleLeft ||
               m_activeSubHandle == HandleRight)
        cursor = Qt::SplitHCursor;
      else  // if (m_activeSubHandle == HandleTop || m_activeSubHandle ==
            // HandleBottom)
        cursor = Qt::SplitVCursor;

      setCursor(cursor);
      update();
    }
  }
  // if left button is pressed and some handle is active, transform the
  // subcamera
  else if (event->buttons() & Qt::LeftButton &&
           m_activeSubHandle != HandleNone && m_preSubCameraRect.isValid()) {
    auto clampVal = [&](int& val, int min, int max) {
      if (val < min)
        val = min;
      else if (val > max)
        val = max;
    };
    auto clampPoint = [&](QPoint& pos, int xmin, int xmax, int ymin, int ymax) {
      clampVal(pos.rx(), xmin, xmax);
      clampVal(pos.ry(), ymin, ymax);
    };

    int minimumSize = 100;

    QPoint offset =
        m_surface->transform().inverted().map(event->pos()) - m_dragStartPos;
    if (m_activeSubHandle >= HandleTopLeft &&
        m_activeSubHandle <= HandleBottomRight) {
      QSize offsetSize = m_preSubCameraRect.size();
      if (m_activeSubHandle == HandleBottomLeft ||
          m_activeSubHandle == HandleTopRight)
        offset.rx() *= -1;
      offsetSize.scale(offset.x(), offset.y(), Qt::KeepAspectRatioByExpanding);
      offset = QPoint(offsetSize.width(), offsetSize.height());
      if (m_activeSubHandle == HandleBottomLeft ||
          m_activeSubHandle == HandleTopRight)
        offset.rx() *= -1;
    }
    QSize camSize = m_surface->surfaceFormat().sizeHint();

    if (m_activeSubHandle == HandleFrame) {
      clampPoint(offset, -m_preSubCameraRect.left(),
                 camSize.width() - m_preSubCameraRect.right(),
                 -m_preSubCameraRect.top(),
                 camSize.height() - m_preSubCameraRect.bottom());
      m_subCameraRect = m_preSubCameraRect.translated(offset);
    } else {
      if (m_activeSubHandle == HandleTopLeft ||
          m_activeSubHandle == HandleBottomLeft ||
          m_activeSubHandle == HandleLeft) {
        clampVal(offset.rx(), -m_preSubCameraRect.left(),
                 m_preSubCameraRect.width() - minimumSize);
        m_subCameraRect.setLeft(m_preSubCameraRect.left() + offset.x());
      } else if (m_activeSubHandle == HandleTopRight ||
                 m_activeSubHandle == HandleBottomRight ||
                 m_activeSubHandle == HandleRight) {
        clampVal(offset.rx(), -m_preSubCameraRect.width() + minimumSize,
                 camSize.width() - m_preSubCameraRect.right());
        m_subCameraRect.setRight(m_preSubCameraRect.right() + offset.x());
      }

      if (m_activeSubHandle == HandleTopLeft ||
          m_activeSubHandle == HandleTopRight ||
          m_activeSubHandle == HandleTop) {
        clampVal(offset.ry(), -m_preSubCameraRect.top(),
                 m_preSubCameraRect.height() - minimumSize);
        m_subCameraRect.setTop(m_preSubCameraRect.top() + offset.y());
      } else if (m_activeSubHandle == HandleBottomRight ||
                 m_activeSubHandle == HandleBottomLeft ||
                 m_activeSubHandle == HandleBottom) {
        clampVal(offset.ry(), -m_preSubCameraRect.height() + minimumSize,
                 camSize.height() - m_preSubCameraRect.bottom());
        m_subCameraRect.setBottom(m_preSubCameraRect.bottom() + offset.y());
      }
      // if the sub camera size is changed, notify the parent for updating the
      // fields
      emit subCameraResized(true);
    }
    update();
  }
}

void MyVideoWidget::mousePressEvent(QMouseEvent* event) {
  // if the sub camera is not active, do nothing and return
  // use left button only and some handle must be active
  if (!m_surface->isActive() || m_subCameraRect.isNull() ||
      event->button() != Qt::LeftButton || m_activeSubHandle == HandleNone)
    return;

  // record the original sub camera size
  m_preSubCameraRect = m_subCameraRect;
  m_dragStartPos     = m_surface->transform().inverted().map(event->pos());

  // temporary stop the camera
  emit stopCamera();
}

void MyVideoWidget::mouseReleaseEvent(QMouseEvent* event) {
  // if the sub camera is not active, do nothing and return
  // use left button only and some handle must be active
  if (!m_surface->isActive() || m_subCameraRect.isNull() ||
      event->button() != Qt::LeftButton || m_activeSubHandle == HandleNone)
    return;

  m_preSubCameraRect = QRect();
  if (m_activeSubHandle != HandleFrame) emit subCameraResized(false);

  // restart the camera
  emit startCamera();
}

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

FrameNumberLineEdit::FrameNumberLineEdit(QWidget* parent, TFrameId fId,
                                         bool acceptLetter)
    : LineEdit(parent) {
  if (acceptLetter) {
    QString regExpStr   = QString("^%1$").arg(TFilePath::fidRegExpStr());
    m_regexpValidator   = new QRegExpValidator(QRegExp(regExpStr), this);
    TProjectManager* pm = TProjectManager::instance();
    pm->addListener(this);
  } else
    m_regexpValidator = new QRegExpValidator(QRegExp("^\\d{1,4}$"), this);

  m_regexpValidator_alt =
      new QRegExpValidator(QRegExp("^\\d{1,3}[A-Ia-i]?$"), this);

  updateValidator();
  updateSize();

  setValue(fId);
}

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

void FrameNumberLineEdit::updateValidator() {
  if (Preferences::instance()->isShowFrameNumberWithLettersEnabled())
    setValidator(m_regexpValidator_alt);
  else
    setValidator(m_regexpValidator);
}

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

void FrameNumberLineEdit::updateSize() {
  FilePathProperties* fpProp =
      TProjectManager::instance()->getCurrentProject()->getFilePathProperties();
  bool useStandard = fpProp->useStandard();
  int letterCount  = fpProp->letterCountForSuffix();
  if (useStandard)
    setFixedWidth(60);
  else {
    // 4 digits + letters reserve 12 px each
    int lc = (letterCount == 0) ? 9 : letterCount + 4;
    setFixedWidth(12 * lc);
  }
  updateGeometry();
}

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

void FrameNumberLineEdit::setValue(TFrameId fId) {
  QString str;
  if (Preferences::instance()->isShowFrameNumberWithLettersEnabled()) {
    if (!fId.getLetter().isEmpty()) {
      // need some warning?
    }
    str = convertToFrameWithLetter(fId.getNumber(), 3);
  } else {
    str = QString::fromStdString(fId.expand());
  }
  setText(str);
  setCursorPosition(0);
}

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

TFrameId FrameNumberLineEdit::getValue() {
  if (Preferences::instance()->isShowFrameNumberWithLettersEnabled()) {
    QString str = text();
    int f;
    // if no letters added
    if (str.at(str.size() - 1).isDigit())
      f = str.toInt() * 10;
    else {
      f = str.left(str.size() - 1).toInt() * 10 +
          letterToNum(str.at(str.size() - 1));
    }
    return TFrameId(f);
  } else {
    QString regExpStr = QString("^%1$").arg(TFilePath::fidRegExpStr());
    QRegExp rx(regExpStr);
    int pos = rx.indexIn(text());
    if (pos < 0) return TFrameId();
    if (rx.cap(2).isEmpty())
      return TFrameId(rx.cap(1).toInt());
    else
      return TFrameId(rx.cap(1).toInt(), rx.cap(2));
  }
}

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

void FrameNumberLineEdit::onProjectSwitched() {
  QRegExpValidator* oldValidator = m_regexpValidator;
  QString regExpStr = QString("^%1$").arg(TFilePath::fidRegExpStr());
  m_regexpValidator = new QRegExpValidator(QRegExp(regExpStr), this);
  updateValidator();
  if (oldValidator) delete oldValidator;
}

void FrameNumberLineEdit::onProjectChanged() { onProjectSwitched(); }

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

void FrameNumberLineEdit::focusInEvent(QFocusEvent* e) {
  m_textOnFocusIn = text();
}

void FrameNumberLineEdit::focusOutEvent(QFocusEvent* e) {
  // if the field is empty, then revert the last input
  if (text().isEmpty()) setText(m_textOnFocusIn);

  LineEdit::focusOutEvent(e);
}

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

LevelNameLineEdit::LevelNameLineEdit(QWidget* parent)
    : QLineEdit(parent), m_textOnFocusIn("") {
  // Exclude all character which cannot fit in a filepath (Win).
  // Dots are also prohibited since they are internally managed by Toonz.
  QRegExp rx("[^\\\\/:?*.\"<>|]+");
  setValidator(new QRegExpValidator(rx, this));
  setObjectName("LargeSizedText");

  connect(this, SIGNAL(editingFinished()), this, SLOT(onEditingFinished()));
}

void LevelNameLineEdit::focusInEvent(QFocusEvent* e) {
  m_textOnFocusIn = text();
}

void LevelNameLineEdit::onEditingFinished() {
  // if the content is not changed, do nothing.
  if (text() == m_textOnFocusIn) return;

  emit levelNameEdited();
}

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

std::wstring FlexibleNameCreator::getPrevious() {
  if (m_s.empty() || (m_s[0] == 0 && m_s.size() == 1)) {
    m_s.push_back('Z' - 'A');
    m_s.push_back('Z' - 'A');
    return L"ZZ";
  }
  int i = 0;
  int n = m_s.size();
  while (i < n) {
    m_s[i]--;
    if (m_s[i] >= 0) break;
    m_s[i] = 'Z' - 'A';
    i++;
  }
  if (i >= n) {
    n--;
    m_s.pop_back();
  }
  std::wstring s;
  for (i = n - 1; i >= 0; i--) s.append(1, (wchar_t)(L'A' + m_s[i]));
  return s;
}

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

bool FlexibleNameCreator::setCurrent(std::wstring name) {
  if (name.empty() || name.size() > 2) return false;
  std::vector<int> newNameBuf;
  for (std::wstring::iterator it = name.begin(); it != name.end(); ++it) {
    int s = (int)((*it) - L'A');
    if (s < 0 || s > 'Z' - 'A') return false;
    newNameBuf.push_back(s);
  }
  m_s.clear();
  for (int i = newNameBuf.size() - 1; i >= 0; i--) m_s.push_back(newNameBuf[i]);
  return true;
}

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

PencilTestSaveInFolderPopup::PencilTestSaveInFolderPopup(QWidget* parent)
    : Dialog(parent, true, false, "PencilTestSaveInFolder") {
  setWindowTitle(tr("Create the Destination Subfolder to Save"));

  m_parentFolderField = new FileField(this);

  QPushButton* setAsDefaultBtn = new QPushButton(tr("Set As Default"), this);
  setAsDefaultBtn->setToolTip(
      tr("Set the current \"Save In\" path as the default."));

  m_subFolderCB = new QCheckBox(tr("Create Subfolder"), this);

  QFrame* subFolderFrame = new QFrame(this);

  QGroupBox* infoGroupBox    = new QGroupBox(tr("Information"), this);
  QGroupBox* subNameGroupBox = new QGroupBox(tr("Subfolder Name"), this);

  m_projectField  = new QLineEdit(this);
  m_episodeField  = new QLineEdit(this);
  m_sequenceField = new QLineEdit(this);
  m_sceneField    = new QLineEdit(this);

  m_autoSubNameCB      = new QCheckBox(tr("Auto Format:"), this);
  m_subNameFormatCombo = new QComboBox(this);
  m_subFolderNameField = new QLineEdit(this);

  QCheckBox* showPopupOnLaunchCB =
      new QCheckBox(tr("Show This on Launch of the Camera Capture"), this);
  m_createSceneInFolderCB = new QCheckBox(tr("Save Scene in Subfolder"), this);

  QPushButton* okBtn     = new QPushButton(tr("OK"), this);
  QPushButton* cancelBtn = new QPushButton(tr("Cancel"), this);

  //---- properties

  m_subFolderCB->setChecked(CamCapSaveInPopupSubFolder != 0);
  subFolderFrame->setEnabled(CamCapSaveInPopupSubFolder != 0);

  // project name
  QString prjName = QString::fromStdString(CamCapSaveInPopupProject.getValue());
  if (prjName.isEmpty()) {
    prjName = TProjectManager::instance()
                  ->getCurrentProject()
                  ->getName()
                  .getQString();
  }
  m_projectField->setText(prjName);

  m_episodeField->setText(
      QString::fromStdString(CamCapSaveInPopupEpisode.getValue()));
  m_sequenceField->setText(
      QString::fromStdString(CamCapSaveInPopupSequence.getValue()));
  m_sceneField->setText(
      QString::fromStdString(CamCapSaveInPopupScene.getValue()));

  m_autoSubNameCB->setChecked(CamCapSaveInPopupAutoSubName != 0);
  m_subNameFormatCombo->setEnabled(CamCapSaveInPopupAutoSubName != 0);
  QStringList items;
  items << tr("C- + Sequence + Scene") << tr("Sequence + Scene")
        << tr("Episode + Sequence + Scene")
        << tr("Project + Episode + Sequence + Scene");
  m_subNameFormatCombo->addItems(items);
  m_subNameFormatCombo->setCurrentIndex(CamCapSaveInPopupAutoSubName - 1);

  showPopupOnLaunchCB->setChecked(CamCapOpenSaveInPopupOnLaunch != 0);
  m_createSceneInFolderCB->setChecked(CamCapSaveInPopupCreateSceneInFolder !=
                                      0);
  m_createSceneInFolderCB->setToolTip(
      tr("Save the current scene in the subfolder.\nSet the output folder path "
         "to the subfolder as well."));

  addButtonBarWidget(okBtn, cancelBtn);

  //---- layout
  m_topLayout->setMargin(10);
  m_topLayout->setSpacing(10);
  {
    QGridLayout* saveInLay = new QGridLayout();
    saveInLay->setMargin(0);
    saveInLay->setHorizontalSpacing(3);
    saveInLay->setVerticalSpacing(0);
    {
      saveInLay->addWidget(new QLabel(tr("Save In:"), this), 0, 0,
                           Qt::AlignRight | Qt::AlignVCenter);
      saveInLay->addWidget(m_parentFolderField, 0, 1);
      saveInLay->addWidget(setAsDefaultBtn, 1, 1);
    }
    saveInLay->setColumnStretch(0, 0);
    saveInLay->setColumnStretch(1, 1);
    m_topLayout->addLayout(saveInLay);

    m_topLayout->addWidget(m_subFolderCB, 0, Qt::AlignLeft);

    QVBoxLayout* subFolderLay = new QVBoxLayout();
    subFolderLay->setMargin(0);
    subFolderLay->setSpacing(10);
    {
      QGridLayout* infoLay = new QGridLayout();
      infoLay->setMargin(10);
      infoLay->setHorizontalSpacing(3);
      infoLay->setVerticalSpacing(10);
      {
        infoLay->addWidget(new QLabel(tr("Project:"), this), 0, 0);
        infoLay->addWidget(m_projectField, 0, 1);

        infoLay->addWidget(new QLabel(tr("Episode:"), this), 1, 0);
        infoLay->addWidget(m_episodeField, 1, 1);

        infoLay->addWidget(new QLabel(tr("Sequence:"), this), 2, 0);
        infoLay->addWidget(m_sequenceField, 2, 1);

        infoLay->addWidget(new QLabel(tr("Scene:"), this), 3, 0);
        infoLay->addWidget(m_sceneField, 3, 1);
      }
      infoLay->setColumnStretch(0, 0);
      infoLay->setColumnStretch(1, 1);
      infoGroupBox->setLayout(infoLay);
      subFolderLay->addWidget(infoGroupBox, 0);

      QGridLayout* subNameLay = new QGridLayout();
      subNameLay->setMargin(10);
      subNameLay->setHorizontalSpacing(3);
      subNameLay->setVerticalSpacing(10);
      {
        subNameLay->addWidget(m_autoSubNameCB, 0, 0);
        subNameLay->addWidget(m_subNameFormatCombo, 0, 1);

        subNameLay->addWidget(new QLabel(tr("Subfolder Name:"), this), 1, 0);
        subNameLay->addWidget(m_subFolderNameField, 1, 1);
      }
      subNameLay->setColumnStretch(0, 0);
      subNameLay->setColumnStretch(1, 1);
      subNameGroupBox->setLayout(subNameLay);
      subFolderLay->addWidget(subNameGroupBox, 0);

      subFolderLay->addWidget(m_createSceneInFolderCB, 0, Qt::AlignLeft);
    }
    subFolderFrame->setLayout(subFolderLay);
    m_topLayout->addWidget(subFolderFrame);

    m_topLayout->addWidget(showPopupOnLaunchCB, 0, Qt::AlignLeft);

    m_topLayout->addStretch(1);
  }

  resize(300, 440);

  //---- signal-slot connection
  bool ret = true;

  ret = ret && connect(m_subFolderCB, SIGNAL(clicked(bool)), subFolderFrame,
                       SLOT(setEnabled(bool)));
  ret = ret && connect(m_projectField, SIGNAL(textEdited(const QString&)), this,
                       SLOT(updateSubFolderName()));
  ret = ret && connect(m_episodeField, SIGNAL(textEdited(const QString&)), this,
                       SLOT(updateSubFolderName()));
  ret = ret && connect(m_sequenceField, SIGNAL(textEdited(const QString&)),
                       this, SLOT(updateSubFolderName()));
  ret = ret && connect(m_sceneField, SIGNAL(textEdited(const QString&)), this,
                       SLOT(updateSubFolderName()));
  ret = ret && connect(m_autoSubNameCB, SIGNAL(clicked(bool)), this,
                       SLOT(onAutoSubNameCBClicked(bool)));
  ret = ret && connect(m_subNameFormatCombo, SIGNAL(currentIndexChanged(int)),
                       this, SLOT(updateSubFolderName()));

  ret = ret && connect(showPopupOnLaunchCB, SIGNAL(clicked(bool)), this,
                       SLOT(onShowPopupOnLaunchCBClicked(bool)));
  ret = ret && connect(m_createSceneInFolderCB, SIGNAL(clicked(bool)), this,
                       SLOT(onCreateSceneInFolderCBClicked(bool)));
  ret = ret && connect(setAsDefaultBtn, SIGNAL(pressed()), this,
                       SLOT(onSetAsDefaultBtnPressed()));

  ret = ret && connect(okBtn, SIGNAL(clicked(bool)), this, SLOT(onOkPressed()));
  ret = ret && connect(cancelBtn, SIGNAL(clicked(bool)), this, SLOT(reject()));
  assert(ret);

  updateSubFolderName();
}

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

QString PencilTestSaveInFolderPopup::getPath() {
  if (!m_subFolderCB->isChecked()) return m_parentFolderField->getPath();

  // re-code filepath
  TFilePath path(m_parentFolderField->getPath() + "\\" +
                 m_subFolderNameField->text());
  ToonzScene* scene = TApp::instance()->getCurrentScene()->getScene();
  if (scene) {
    path = scene->decodeFilePath(path);
    path = scene->codeFilePath(path);
  }
  return path.getQString();
}

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

QString PencilTestSaveInFolderPopup::getParentPath() {
  return m_parentFolderField->getPath();
}

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

void PencilTestSaveInFolderPopup::showEvent(QShowEvent* event) {
  // Show "Save the scene" check box only when the scene is untitled
  bool isUntitled =
      TApp::instance()->getCurrentScene()->getScene()->isUntitled();
  m_createSceneInFolderCB->setVisible(isUntitled);
}

//-----------------------------------------------------------------------------
namespace {
QString formatString(QString inStr, int charNum) {
  if (inStr.isEmpty()) return QString("0").rightJustified(charNum, '0');

  QString numStr, postStr;
  // find the first non-digit character
  int index = inStr.indexOf(QRegExp("[^0-9]"), 0);

  if (index == -1)  // only digits
    numStr = inStr;
  else if (index == 0)  // only post strings
    return inStr;
  else {  // contains both
    numStr  = inStr.left(index);
    postStr = inStr.right(inStr.length() - index);
  }
  return numStr.rightJustified(charNum, '0') + postStr;
}
};  // namespace

void PencilTestSaveInFolderPopup::updateSubFolderName() {
  if (!m_autoSubNameCB->isChecked()) return;

  QString episodeStr  = formatString(m_episodeField->text(), 3);
  QString sequenceStr = formatString(m_sequenceField->text(), 3);
  QString sceneStr    = formatString(m_sceneField->text(), 4);

  QString str;

  switch (m_subNameFormatCombo->currentIndex()) {
  case 0:  // C- + Sequence + Scene
    str = QString("C-%1-%2").arg(sequenceStr).arg(sceneStr);
    break;
  case 1:  // Sequence + Scene
    str = QString("%1-%2").arg(sequenceStr).arg(sceneStr);
    break;
  case 2:  // Episode + Sequence + Scene
    str = QString("%1-%2-%3").arg(episodeStr).arg(sequenceStr).arg(sceneStr);
    break;
  case 3:  // Project + Episode + Sequence + Scene
    str = QString("%1-%2-%3-%4")
              .arg(m_projectField->text())
              .arg(episodeStr)
              .arg(sequenceStr)
              .arg(sceneStr);
    break;
  default:
    return;
  }
  m_subFolderNameField->setText(str);
}

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

void PencilTestSaveInFolderPopup::onAutoSubNameCBClicked(bool on) {
  m_subNameFormatCombo->setEnabled(on);
  updateSubFolderName();
}

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

void PencilTestSaveInFolderPopup::onShowPopupOnLaunchCBClicked(bool on) {
  CamCapOpenSaveInPopupOnLaunch = (on) ? 1 : 0;
}

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

void PencilTestSaveInFolderPopup::onCreateSceneInFolderCBClicked(bool on) {
  CamCapSaveInPopupCreateSceneInFolder = (on) ? 1 : 0;
}

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

void PencilTestSaveInFolderPopup::onSetAsDefaultBtnPressed() {
  CamCapSaveInParentFolder = m_parentFolderField->getPath().toStdString();
}

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

void PencilTestSaveInFolderPopup::onOkPressed() {
  if (!m_subFolderCB->isChecked()) {
    accept();
    return;
  }

  // check the subFolder value
  QString subFolderName = m_subFolderNameField->text();
  if (subFolderName.isEmpty()) {
    DVGui::MsgBox(WARNING, tr("Subfolder name should not be empty."));
    return;
  }

  int index = subFolderName.indexOf(QRegExp("[\\]:;|=,\\[\\*\\.\"/\\\\]"), 0);
  if (index >= 0) {
    DVGui::MsgBox(WARNING, tr("Subfolder name should not contain following "
                              "characters:  * . \" / \\ [ ] : ; | = , "));
    return;
  }

  TFilePath fp(m_parentFolderField->getPath());
  fp += TFilePath(subFolderName);
  TFilePath actualFp =
      TApp::instance()->getCurrentScene()->getScene()->decodeFilePath(fp);

  if (QFileInfo::exists(actualFp.getQString())) {
    DVGui::MsgBox(WARNING,
                  tr("Folder %1 already exists.").arg(actualFp.getQString()));
    return;
  }

  // save the current properties to env data
  CamCapSaveInPopupSubFolder   = (m_subFolderCB->isChecked()) ? 1 : 0;
  CamCapSaveInPopupProject     = m_projectField->text().toStdString();
  CamCapSaveInPopupEpisode     = m_episodeField->text().toStdString();
  CamCapSaveInPopupSequence    = m_sequenceField->text().toStdString();
  CamCapSaveInPopupScene       = m_sceneField->text().toStdString();
  CamCapSaveInPopupAutoSubName = (!m_autoSubNameCB->isChecked())
                                     ? 0
                                     : m_subNameFormatCombo->currentIndex() + 1;

  // create folder
  try {
    TSystem::mkDir(actualFp);
  } catch (...) {
    MsgBox(CRITICAL, tr("It is not possible to create the %1 folder.")
                         .arg(toQString(actualFp)));
    return;
  }

  createSceneInFolder();
  accept();
}

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

void PencilTestSaveInFolderPopup::createSceneInFolder() {
  // make sure that the check box is displayed (= the scene is untitled) and is
  // checked.
  if (m_createSceneInFolderCB->isHidden() ||
      !m_createSceneInFolderCB->isChecked())
    return;
  // just in case
  if (!m_subFolderCB->isChecked()) return;

  // set the output folder
  ToonzScene* scene = TApp::instance()->getCurrentScene()->getScene();
  if (!scene) return;

  TFilePath fp(getPath().toStdWString());

  // for the scene folder mode, output destination must be already set to
  // $scenefolder or its subfolder. See TSceneProperties::onInitialize()
  if (Preferences::instance()->getPathAliasPriority() !=
      Preferences::SceneFolderAlias) {
    TOutputProperties* prop = scene->getProperties()->getOutputProperties();
    prop->setPath(prop->getPath().withParentDir(fp));
  }

  // save the scene
  TFilePath sceneFp =
      scene->decodeFilePath(fp) +
      TFilePath(m_subFolderNameField->text().toStdWString()).withType("tnz");
  IoCmd::saveScene(sceneFp, 0);
}

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

void PencilTestSaveInFolderPopup::updateParentFolder() {
  // If the parent folder is saved in the scene, use it
  ToonzScene* scene = TApp::instance()->getCurrentScene()->getScene();
  QString parentFolder =
      scene->getProperties()->cameraCaptureSaveInPath().getQString();
  if (parentFolder.isEmpty()) {
    // else then, if the user-env stores the parent folder value, use it
    parentFolder = QString::fromStdString(CamCapSaveInParentFolder);
    // else, use "+extras" project folder
    if (parentFolder.isEmpty())
      parentFolder =
          QString("+%1").arg(QString::fromStdString(TProject::Extras));
  }

  m_parentFolderField->setPath(parentFolder);
}

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

PencilTestPopup::PencilTestPopup()
    // set the parent 0 in order to enable the popup behind the main window
    : Dialog(0, false, false, "PencilTest")
    , m_currentCamera(NULL)
    , m_captureWhiteBGCue(false)
    , m_captureCue(false) {
  setWindowTitle(tr("Camera Capture"));

  // add maximize button to the dialog
  setWindowFlags(windowFlags() | Qt::WindowMaximizeButtonHint);

  layout()->setSizeConstraint(QLayout::SetNoConstraint);

  m_saveInFolderPopup = new PencilTestSaveInFolderPopup(this);

  m_videoWidget = new MyVideoWidget(this);

  m_cameraListCombo                 = new QComboBox(this);
  QPushButton* refreshCamListButton = new QPushButton(tr("Refresh"), this);
  m_resolutionCombo                 = new QComboBox(this);

  QGroupBox* fileFrame = new QGroupBox(tr("File"), this);
  m_levelNameEdit      = new LevelNameLineEdit(this);
  // set the start frame 10 if the option in preferences
  // "Show ABC Appendix to the Frame Number in Xsheet Cell" is active.
  // (frame 10 is displayed as "1" with this option)
  int startFrame =
      Preferences::instance()->isShowFrameNumberWithLettersEnabled() ? 10 : 1;
  m_frameNumberEdit        = new FrameNumberLineEdit(this, startFrame);
  m_frameInfoLabel         = new QLabel("", this);
  m_fileTypeCombo          = new QComboBox(this);
  m_fileFormatOptionButton = new QPushButton(tr("Options"), this);

  m_saveInFileFld = new FileField(this, m_saveInFolderPopup->getParentPath());

  QToolButton* nextLevelButton = new QToolButton(this);
  m_previousLevelButton        = new QToolButton(this);

  m_saveOnCaptureCB =
      new QCheckBox(tr("Save images as they are captured"), this);

  QGroupBox* imageFrame = new QGroupBox(tr("Image adjust"), this);
  m_colorTypeCombo      = new QComboBox(this);

  m_camCapLevelControl = new CameraCaptureLevelControl(this);
  m_upsideDownCB       = new QCheckBox(tr("Upside down"), this);

  m_bgReductionFld       = new IntField(this);
  m_captureWhiteBGButton = new QPushButton(tr("Capture white BG"), this);

  QGroupBox* displayFrame = new QGroupBox(tr("Display"), this);
  m_onionSkinCB           = new QCheckBox(tr("Show onion skin"), this);
  m_loadImageButton       = new QPushButton(tr("Load Selected Image"), this);
  m_onionOpacityFld       = new IntField(this);

  QGroupBox* timerFrame = new QGroupBox(tr("Interval timer"), this);
  m_timerCB             = new QCheckBox(tr("Use interval timer"), this);
  m_timerIntervalFld    = new IntField(this);
  m_captureTimer        = new QTimer(this);
  m_countdownTimer      = new QTimer(this);

  m_captureButton          = new QPushButton(tr("Capture\n[Return key]"), this);
  QPushButton* closeButton = new QPushButton(tr("Close"), this);

#ifdef _WIN32
  m_captureFilterSettingsBtn = new QPushButton(this);
#else
  m_captureFilterSettingsBtn = 0;
#endif

  QPushButton* subfolderButton = new QPushButton(tr("Subfolder"), this);

  // subcamera
  m_subcameraButton     = new QPushButton(tr("Subcamera"), this);
  m_subWidthFld         = new IntLineEdit(this);
  m_subHeightFld        = new IntLineEdit(this);
  QWidget* subCamWidget = new QWidget(this);

  //----

  m_resolutionCombo->setMaximumWidth(fontMetrics().width("0000 x 0000") + 25);
  m_fileTypeCombo->addItems({"jpg", "png", "tga", "tif"});
  m_fileTypeCombo->setCurrentIndex(0);

  fileFrame->setObjectName("CleanupSettingsFrame");
  m_frameNumberEdit->setObjectName("LargeSizedText");
  m_frameInfoLabel->setAlignment(Qt::AlignRight);
  nextLevelButton->setFixedSize(24, 24);
  nextLevelButton->setArrowType(Qt::RightArrow);
  nextLevelButton->setToolTip(tr("Next Level"));
  m_previousLevelButton->setFixedSize(24, 24);
  m_previousLevelButton->setArrowType(Qt::LeftArrow);
  m_previousLevelButton->setToolTip(tr("Previous Level"));
  m_saveOnCaptureCB->setChecked(true);

  imageFrame->setObjectName("CleanupSettingsFrame");
  m_colorTypeCombo->addItems(
      {tr("Color"), tr("Grayscale"), tr("Black & White")});
  m_colorTypeCombo->setCurrentIndex(0);
  m_upsideDownCB->setChecked(false);

  m_bgReductionFld->setRange(0, 100);
  m_bgReductionFld->setValue(0);
  m_bgReductionFld->setDisabled(true);

  displayFrame->setObjectName("CleanupSettingsFrame");
  m_onionSkinCB->setChecked(false);
  m_onionOpacityFld->setRange(1, 100);
  m_onionOpacityFld->setValue(50);
  m_onionOpacityFld->setDisabled(true);

  timerFrame->setObjectName("CleanupSettingsFrame");
  m_timerCB->setChecked(false);
  m_timerIntervalFld->setRange(0, 60);
  m_timerIntervalFld->setValue(10);
  m_timerIntervalFld->setDisabled(true);
  // Make the interval timer single-shot. When the capture finished, restart
  // timer for next frame.
  // This is because capturing and saving the image needs some time.
  m_captureTimer->setSingleShot(true);

  m_captureButton->setObjectName("LargeSizedText");
  m_captureButton->setFixedHeight(75);
  QCommonStyle style;
  m_captureButton->setIcon(style.standardIcon(QStyle::SP_DialogOkButton));
  m_captureButton->setIconSize(QSize(30, 30));

  if (m_captureFilterSettingsBtn) {
    m_captureFilterSettingsBtn->setObjectName("GearButton");
    m_captureFilterSettingsBtn->setFixedSize(24, 24);
    m_captureFilterSettingsBtn->setIconSize(QSize(16, 16));
    m_captureFilterSettingsBtn->setIcon(createQIcon("gear"));
    m_captureFilterSettingsBtn->setToolTip(
        tr("Video Capture Filter Settings..."));
  }

  subfolderButton->setObjectName("SubfolderButton");
  subfolderButton->setIconSize(QSize(16, 16));
  subfolderButton->setIcon(createQIcon("folder_new"));
  m_saveInFileFld->setMaximumWidth(380);

  m_saveInFolderPopup->hide();

  m_subcameraButton->setObjectName("SubcameraButton");
  m_subcameraButton->setIconSize(QSize(16, 16));
  m_subcameraButton->setIcon(createQIcon("subcamera"));
  m_subcameraButton->setCheckable(true);
  m_subcameraButton->setChecked(false);
  subCamWidget->setHidden(true);

  //---- layout ----
  m_topLayout->setMargin(10);
  m_topLayout->setSpacing(10);
  {
    QHBoxLayout* camLay = new QHBoxLayout();
    camLay->setMargin(0);
    camLay->setSpacing(3);
    {
      camLay->addWidget(new QLabel(tr("Camera:"), this), 0);
      camLay->addWidget(m_cameraListCombo, 1);
      camLay->addWidget(refreshCamListButton, 0);
      camLay->addSpacing(10);
      camLay->addWidget(new QLabel(tr("Resolution:"), this), 0);
      camLay->addWidget(m_resolutionCombo, 1);

      if (m_captureFilterSettingsBtn) {
        camLay->addSpacing(10);
        camLay->addWidget(m_captureFilterSettingsBtn);
      }

      camLay->addSpacing(10);
      camLay->addWidget(m_subcameraButton, 0);
      QHBoxLayout* subCamLay = new QHBoxLayout();
      subCamLay->setMargin(0);
      subCamLay->setSpacing(3);
      {
        subCamLay->addWidget(m_subWidthFld, 0);
        subCamLay->addWidget(new QLabel("x", this), 0);
        subCamLay->addWidget(m_subHeightFld, 0);
        subCamLay->addStretch(0);
      }
      subCamWidget->setLayout(subCamLay);
      camLay->addWidget(subCamWidget, 0);

      camLay->addStretch(0);
      camLay->addSpacing(15);
      camLay->addWidget(new QLabel(tr("Save In:"), this), 0);
      camLay->addWidget(m_saveInFileFld, 1);

      camLay->addSpacing(10);
      camLay->addWidget(subfolderButton, 0);
    }
    m_topLayout->addLayout(camLay, 0);

    QHBoxLayout* bottomLay = new QHBoxLayout();
    bottomLay->setMargin(0);
    bottomLay->setSpacing(10);
    {
      bottomLay->addWidget(m_videoWidget, 1);

      QVBoxLayout* rightLay = new QVBoxLayout();
      rightLay->setMargin(0);
      rightLay->setSpacing(5);
      {
        QVBoxLayout* fileLay = new QVBoxLayout();
        fileLay->setMargin(8);
        fileLay->setSpacing(5);
        {
          QGridLayout* levelLay = new QGridLayout();
          levelLay->setMargin(0);
          levelLay->setHorizontalSpacing(3);
          levelLay->setVerticalSpacing(5);
          {
            levelLay->addWidget(new QLabel(tr("Name:"), this), 0, 0,
                                Qt::AlignRight);
            QHBoxLayout* nameLay = new QHBoxLayout();
            nameLay->setMargin(0);
            nameLay->setSpacing(2);
            {
              nameLay->addWidget(m_previousLevelButton, 0);
              nameLay->addWidget(m_levelNameEdit, 1);
              nameLay->addWidget(nextLevelButton, 0);
            }
            levelLay->addLayout(nameLay, 0, 1);

            levelLay->addWidget(new QLabel(tr("Frame:"), this), 1, 0,
                                Qt::AlignRight);

            QHBoxLayout* frameLay = new QHBoxLayout();
            frameLay->setMargin(0);
            frameLay->setSpacing(2);
            {
              frameLay->addWidget(m_frameNumberEdit, 1);
              frameLay->addWidget(m_frameInfoLabel, 1, Qt::AlignVCenter);
            }
            levelLay->addLayout(frameLay, 1, 1);
          }
          levelLay->setColumnStretch(0, 0);
          levelLay->setColumnStretch(1, 1);
          fileLay->addLayout(levelLay, 0);

          QHBoxLayout* fileTypeLay = new QHBoxLayout();
          fileTypeLay->setMargin(0);
          fileTypeLay->setSpacing(3);
          {
            fileTypeLay->addWidget(new QLabel(tr("File Type:"), this), 0);
            fileTypeLay->addWidget(m_fileTypeCombo, 1);
            fileTypeLay->addSpacing(10);
            fileTypeLay->addWidget(m_fileFormatOptionButton);
          }
          fileLay->addLayout(fileTypeLay, 0);

          fileLay->addWidget(m_saveOnCaptureCB, 0);
        }
        fileFrame->setLayout(fileLay);
        rightLay->addWidget(fileFrame, 0);

        QGridLayout* imageLay = new QGridLayout();
        imageLay->setMargin(8);
        imageLay->setHorizontalSpacing(3);
        imageLay->setVerticalSpacing(5);
        {
          imageLay->addWidget(new QLabel(tr("Color type:"), this), 0, 0,
                              Qt::AlignRight);
          imageLay->addWidget(m_colorTypeCombo, 0, 1);

          imageLay->addWidget(m_camCapLevelControl, 1, 0, 1, 3);

          imageLay->addWidget(m_upsideDownCB, 2, 0, 1, 3, Qt::AlignLeft);

          imageLay->addWidget(new QLabel(tr("BG reduction:"), this), 3, 0,
                              Qt::AlignRight);
          imageLay->addWidget(m_bgReductionFld, 3, 1, 1, 2);

          imageLay->addWidget(m_captureWhiteBGButton, 4, 0, 1, 3);
        }
        imageLay->setColumnStretch(0, 0);
        imageLay->setColumnStretch(1, 0);
        imageLay->setColumnStretch(2, 1);
        imageFrame->setLayout(imageLay);
        rightLay->addWidget(imageFrame, 0);

        QGridLayout* displayLay = new QGridLayout();
        displayLay->setMargin(8);
        displayLay->setHorizontalSpacing(3);
        displayLay->setVerticalSpacing(5);
        {
          displayLay->addWidget(m_onionSkinCB, 0, 0, 1, 2);

          displayLay->addWidget(new QLabel(tr("Opacity(%):"), this), 1, 0,
                                Qt::AlignRight);
          displayLay->addWidget(m_onionOpacityFld, 1, 1);
          displayLay->addWidget(m_loadImageButton, 2, 0, 1, 2);
        }
        displayLay->setColumnStretch(0, 0);
        displayLay->setColumnStretch(1, 1);
        displayFrame->setLayout(displayLay);
        rightLay->addWidget(displayFrame);

        QGridLayout* timerLay = new QGridLayout();
        timerLay->setMargin(8);
        timerLay->setHorizontalSpacing(3);
        timerLay->setVerticalSpacing(5);
        {
          timerLay->addWidget(m_timerCB, 0, 0, 1, 2);

          timerLay->addWidget(new QLabel(tr("Interval(sec):"), this), 1, 0,
                              Qt::AlignRight);
          timerLay->addWidget(m_timerIntervalFld, 1, 1);
        }
        timerLay->setColumnStretch(0, 0);
        timerLay->setColumnStretch(1, 1);
        timerFrame->setLayout(timerLay);
        rightLay->addWidget(timerFrame);

        rightLay->addStretch(1);

        rightLay->addWidget(m_captureButton, 0);
        rightLay->addSpacing(10);
        rightLay->addWidget(closeButton, 0);
        rightLay->addSpacing(5);
      }
      bottomLay->addLayout(rightLay, 0);
    }
    m_topLayout->addLayout(bottomLay, 1);
  }

  //---- signal-slot connections ----
  bool ret = true;
  ret      = ret && connect(refreshCamListButton, SIGNAL(pressed()), this,
                            SLOT(refreshCameraList()));
  ret      = ret && connect(m_cameraListCombo, SIGNAL(activated(int)), this,
                            SLOT(onCameraListComboActivated(int)));
  ret = ret && connect(m_resolutionCombo, SIGNAL(activated(const QString&)),
                       this, SLOT(onResolutionComboActivated(const QString&)));
  ret = ret && connect(m_fileFormatOptionButton, SIGNAL(pressed()), this,
                       SLOT(onFileFormatOptionButtonPressed()));
  ret = ret && connect(m_levelNameEdit, SIGNAL(levelNameEdited()), this,
                       SLOT(onLevelNameEdited()));
  ret = ret &&
        connect(nextLevelButton, SIGNAL(pressed()), this, SLOT(onNextName()));
  ret = ret && connect(m_previousLevelButton, SIGNAL(pressed()), this,
                       SLOT(onPreviousName()));
  ret = ret && connect(m_colorTypeCombo, SIGNAL(currentIndexChanged(int)), this,
                       SLOT(onColorTypeComboChanged(int)));
  ret = ret && connect(m_captureWhiteBGButton, SIGNAL(pressed()), this,
                       SLOT(onCaptureWhiteBGButtonPressed()));
  ret = ret && connect(m_onionSkinCB, SIGNAL(toggled(bool)), this,
                       SLOT(onOnionCBToggled(bool)));
  ret = ret && connect(m_loadImageButton, SIGNAL(pressed()), this,
                       SLOT(onLoadImageButtonPressed()));
  ret = ret && connect(m_onionOpacityFld, SIGNAL(valueEditedByHand()), this,
                       SLOT(onOnionOpacityFldEdited()));
  ret = ret && connect(m_upsideDownCB, SIGNAL(toggled(bool)), m_videoWidget,
                       SLOT(onUpsideDownChecked(bool)));
  ret = ret && connect(m_timerCB, SIGNAL(toggled(bool)), this,
                       SLOT(onTimerCBToggled(bool)));
  ret = ret && connect(m_captureTimer, SIGNAL(timeout()), this,
                       SLOT(onCaptureTimerTimeout()));
  ret = ret &&
        connect(m_countdownTimer, SIGNAL(timeout()), this, SLOT(onCountDown()));

  ret = ret && connect(closeButton, SIGNAL(clicked()), this, SLOT(reject()));
  ret = ret && connect(m_captureButton, SIGNAL(clicked(bool)), this,
                       SLOT(onCaptureButtonClicked(bool)));
  if (m_captureFilterSettingsBtn)
    ret = ret && connect(m_captureFilterSettingsBtn, SIGNAL(pressed()), this,
                         SLOT(onCaptureFilterSettingsBtnPressed()));
  ret = ret && connect(subfolderButton, SIGNAL(clicked(bool)), this,
                       SLOT(openSaveInFolderPopup()));
  ret = ret && connect(m_saveInFileFld, SIGNAL(pathChanged()), this,
                       SLOT(onSaveInPathEdited()));
  ret = ret && connect(m_fileTypeCombo, SIGNAL(activated(int)), this,
                       SLOT(refreshFrameInfo()));
  ret = ret && connect(m_frameNumberEdit, SIGNAL(editingFinished()), this,
                       SLOT(refreshFrameInfo()));

  // sub camera
  ret = ret && connect(m_subcameraButton, SIGNAL(toggled(bool)), this,
                       SLOT(onSubCameraToggled(bool)));
  ret = ret && connect(m_subcameraButton, SIGNAL(toggled(bool)), subCamWidget,
                       SLOT(setVisible(bool)));
  ret = ret && connect(m_subWidthFld, SIGNAL(editingFinished()), this,
                       SLOT(onSubCameraSizeEdited()));
  ret = ret && connect(m_subHeightFld, SIGNAL(editingFinished()), this,
                       SLOT(onSubCameraSizeEdited()));
  ret = ret && connect(m_videoWidget, &MyVideoWidget::startCamera, [&]() {
          if (m_currentCamera) m_currentCamera->start();
        });
  ret = ret && connect(m_videoWidget, &MyVideoWidget::stopCamera, [&]() {
          if (m_currentCamera) m_currentCamera->stop();
        });
  ret = ret && connect(m_videoWidget, SIGNAL(subCameraResized(bool)), this,
                       SLOT(onSubCameraResized(bool)));

  assert(ret);

  refreshCameraList();

  int startupCamIndex = m_cameraListCombo->findText(
      QString::fromStdString(CamCapCameraName.getValue()));
  if (startupCamIndex > 0) {
    m_cameraListCombo->setCurrentIndex(startupCamIndex);
    onCameraListComboActivated(startupCamIndex);
  }

  QString resStr = QString::fromStdString(CamCapCameraResolution.getValue());
  if (m_currentCamera && !resStr.isEmpty()) {
    int startupResolutionIndex = m_resolutionCombo->findText(resStr);
    if (startupResolutionIndex >= 0) {
      m_resolutionCombo->setCurrentIndex(startupResolutionIndex);
      onResolutionComboActivated(resStr);
    }
  }

  setToNextNewLevel();
}

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

PencilTestPopup::~PencilTestPopup() {
  if (m_currentCamera) {
    if (m_currentCamera->state() == QCamera::ActiveState)
      m_currentCamera->stop();
    if (m_currentCamera->state() == QCamera::LoadedState)
      m_currentCamera->unload();
    delete m_currentCamera;
  }
}

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

void PencilTestPopup::refreshCameraList() {
  m_cameraListCombo->clear();

  QList<QCameraInfo> cameras = QCameraInfo::availableCameras();
  if (cameras.empty()) {
    m_cameraListCombo->addItem(tr("No camera found"));
    m_cameraListCombo->setMaximumWidth(250);
    m_cameraListCombo->setDisabled(true);
  }

  int maxTextLength = 0;
  // add non-connected state as default
  m_cameraListCombo->addItem(tr("- Select camera -"));
  for (int c = 0; c < cameras.size(); c++) {
    QString camDesc = cameras.at(c).description();
    m_cameraListCombo->addItem(camDesc);
    maxTextLength = std::max(maxTextLength, fontMetrics().width(camDesc));
  }
  m_cameraListCombo->setMaximumWidth(maxTextLength + 25);
  m_cameraListCombo->setEnabled(true);
  m_cameraListCombo->setCurrentIndex(0);
}

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

void PencilTestPopup::onCameraListComboActivated(int comboIndex) {
  QList<QCameraInfo> cameras = QCameraInfo::availableCameras();
  if (cameras.size() != m_cameraListCombo->count() - 1) return;

  // if selected the non-connected state, then disconnect the current camera
  if (comboIndex == 0) {
    m_videoWidget->videoSurface()->stop();
    if (m_currentCamera) {
      if (m_currentCamera->state() == QCamera::ActiveState)
        m_currentCamera->stop();
      if (m_currentCamera->state() == QCamera::LoadedState)
        m_currentCamera->unload();
    }
    m_deviceName = QString();
    m_videoWidget->setImage(QImage());
    // update env
    CamCapCameraName = "";
    return;
  }

  int index = comboIndex - 1;
  // in case the camera is not changed (just click the combobox)
  if (cameras.at(index).deviceName() == m_deviceName) return;

  QCamera* oldCamera = m_currentCamera;
  if (oldCamera) m_videoWidget->videoSurface()->stop();

  m_currentCamera = new QCamera(cameras.at(index), this);
  m_deviceName    = cameras.at(index).deviceName();

  // loading new camera
  m_currentCamera->load();

  // refresh resolution
  m_resolutionCombo->clear();
  QList<QSize> sizes = m_currentCamera->supportedViewfinderResolutions();

  for (int s = 0; s < sizes.size(); s++) {
    m_resolutionCombo->addItem(
        QString("%1 x %2").arg(sizes.at(s).width()).arg(sizes.at(s).height()));
  }
  if (!sizes.isEmpty()) {
    // select the largest available resolution
    m_resolutionCombo->setCurrentIndex(m_resolutionCombo->count() - 1);
    QCameraViewfinderSettings settings = m_currentCamera->viewfinderSettings();
    settings.setResolution(sizes.last());
    m_currentCamera->setViewfinderSettings(settings);
  }
  m_currentCamera->setViewfinder(m_videoWidget->videoSurface());
  m_videoWidget->videoSurface()->start(
      m_videoWidget->videoSurface()->surfaceFormat());

  // deleting old camera
  if (oldCamera) {
    if (oldCamera->state() == QCamera::ActiveState) oldCamera->stop();
    delete oldCamera;
  }
  // start new camera
  m_currentCamera->start();
  m_videoWidget->setImage(QImage());

  // update env
  CamCapCameraName = m_cameraListCombo->itemText(comboIndex).toStdString();
}

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

void PencilTestPopup::onResolutionComboActivated(const QString& itemText) {
  // resolution is written in the itemText with the format "<width> x <height>"
  // (e.g. "800 x 600")
  QStringList texts = itemText.split(' ');
  // the splited text must be "<width>" "x" and "<height>"
  if (texts.size() != 3) return;

#ifndef MACOSX
  m_currentCamera->stop();
  m_currentCamera->unload();
#endif

  QCameraViewfinderSettings settings = m_currentCamera->viewfinderSettings();
  QSize newResolution(texts[0].toInt(), texts[2].toInt());
  settings.setResolution(newResolution);
  m_currentCamera->setViewfinderSettings(settings);

  // reset white bg
  m_whiteBGImg = QImage();
  m_bgReductionFld->setDisabled(true);
#ifndef MACOSX
  m_currentCamera->start();
#endif
  m_videoWidget->setImage(QImage());

  // update env
  CamCapCameraResolution = itemText.toStdString();

  refreshFrameInfo();

  // reset subcamera info
  m_subcameraButton->setChecked(false);  // this will hide the size fields
  m_subWidthFld->setRange(10, newResolution.width());
  m_subHeightFld->setRange(10, newResolution.height());
  // if there is no existing level or its size is larger than the current camera
  if (!m_allowedCameraSize.isValid() ||
      m_allowedCameraSize.width() > newResolution.width() ||
      m_allowedCameraSize.height() > newResolution.height()) {
    // make the initial subcamera size to be with the same aspect ratio as the
    // current camera
    TCamera* camera =
        TApp::instance()->getCurrentScene()->getScene()->getCurrentCamera();
    TDimension camres = camera->getRes();
    newResolution =
        QSize(camres.lx, camres.ly).scaled(newResolution, Qt::KeepAspectRatio);
    // newResolution.scale(QSize(res.lx, res.ly), Qt::KeepAspectRatio);
    m_subWidthFld->setValue(newResolution.width());
    m_subHeightFld->setValue(newResolution.height());
  } else {
    m_subWidthFld->setValue(m_allowedCameraSize.width());
    m_subHeightFld->setValue(m_allowedCameraSize.height());
  }
}

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

void PencilTestPopup::onFileFormatOptionButtonPressed() {
  // Tentatively use the preview output settings
  ToonzScene* scene = TApp::instance()->getCurrentScene()->getScene();
  if (!scene) return;
  TOutputProperties* prop = scene->getProperties()->getPreviewProperties();
  std::string ext         = m_fileTypeCombo->currentText().toStdString();
  TFrameId oldTmplFId     = scene->getProperties()->formatTemplateFIdForInput();
  openFormatSettingsPopup(this, ext, prop->getFileFormatProperties(ext),
                          &scene->getProperties()->formatTemplateFIdForInput());

  TFrameId newTmplFId = scene->getProperties()->formatTemplateFIdForInput();
  if (oldTmplFId.getZeroPadding() != newTmplFId.getZeroPadding() ||
      oldTmplFId.getStartSeqInd() != newTmplFId.getStartSeqInd())
    refreshFrameInfo();
}

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

void PencilTestPopup::onLevelNameEdited() {
  updateLevelNameAndFrame(m_levelNameEdit->text().toStdWString());
}

//-----------------------------------------------------------------------------
void PencilTestPopup::onNextName() {
  std::unique_ptr<FlexibleNameCreator> nameCreator(new FlexibleNameCreator());
  if (!nameCreator->setCurrent(m_levelNameEdit->text().toStdWString())) {
    setToNextNewLevel();
    return;
  }

  std::wstring levelName = nameCreator->getNext();

  updateLevelNameAndFrame(levelName);
}

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

void PencilTestPopup::onPreviousName() {
  std::unique_ptr<FlexibleNameCreator> nameCreator(new FlexibleNameCreator());

  std::wstring levelName;

  // if the current level name is non-sequential, then try to switch the last
  // sequential level in the scene.
  if (!nameCreator->setCurrent(m_levelNameEdit->text().toStdWString())) {
    TLevelSet* levelSet =
        TApp::instance()->getCurrentScene()->getScene()->getLevelSet();
    nameCreator->setCurrent(L"ZZ");
    for (;;) {
      levelName = nameCreator->getPrevious();
      if (levelSet->getLevel(levelName) != 0) break;
      if (levelName == L"A") {
        setToNextNewLevel();
        return;
      }
    }
  } else
    levelName = nameCreator->getPrevious();

  updateLevelNameAndFrame(levelName);
}

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

void PencilTestPopup::setToNextNewLevel() {
  const std::unique_ptr<NameBuilder> nameBuilder(NameBuilder::getBuilder(L""));

  TLevelSet* levelSet =
      TApp::instance()->getCurrentScene()->getScene()->getLevelSet();
  ToonzScene* scene      = TApp::instance()->getCurrentScene()->getScene();
  std::wstring levelName = L"";

  // Select a different unique level name in case it already exists (either in
  // scene or on disk)
  TFilePath fp;
  TFilePath actualFp;
  for (;;) {
    levelName = nameBuilder->getNext();

    if (levelSet->getLevel(levelName) != 0) continue;

    fp = TFilePath(m_saveInFileFld->getPath()) +
         TFilePath(levelName + L".." +
                   m_fileTypeCombo->currentText().toStdWString());
    actualFp = scene->decodeFilePath(fp);

    if (TSystem::doesExistFileOrLevel(actualFp)) {
      continue;
    }

    break;
  }

  updateLevelNameAndFrame(levelName);
}

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

void PencilTestPopup::updateLevelNameAndFrame(std::wstring levelName) {
  if (levelName != m_levelNameEdit->text().toStdWString())
    m_levelNameEdit->setText(QString::fromStdWString(levelName));
  m_previousLevelButton->setDisabled(levelName == L"A");

  // set the start frame 10 if the option in preferences
  // "Show ABC Appendix to the Frame Number in Xsheet Cell" is active.
  // (frame 10 is displayed as "1" with this option)
  bool withLetter =
      Preferences::instance()->isShowFrameNumberWithLettersEnabled();

  TLevelSet* levelSet =
      TApp::instance()->getCurrentScene()->getScene()->getLevelSet();
  TXshLevel* level_p = levelSet->getLevel(levelName);
  int startFrame;
  if (!level_p) {
    startFrame = withLetter ? 10 : 1;
  } else {
    std::vector<TFrameId> fids;
    level_p->getFids(fids);
    if (fids.empty()) {
      startFrame = withLetter ? 10 : 1;
    } else {
      int lastNum = fids.back().getNumber();
      startFrame  = withLetter ? ((int)(lastNum / 10) + 1) * 10 : lastNum + 1;
    }
  }
  m_frameNumberEdit->setValue(startFrame);

  refreshFrameInfo();
}

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

void PencilTestPopup::onColorTypeComboChanged(int index) {
  m_camCapLevelControl->setMode(index != 2);
}

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

void PencilTestPopup::onFrameCaptured(QImage& image) {
  if (!m_videoWidget || image.isNull()) return;
  // capture the white BG
  if (m_captureWhiteBGCue) {
    m_whiteBGImg        = image.copy();
    m_captureWhiteBGCue = false;
    m_bgReductionFld->setEnabled(true);
  }

  processImage(image);
  m_videoWidget->setImage(image.copy());

  if (m_captureCue) {
#ifndef MACOSX
    m_currentCamera->stop();
#endif
    m_captureCue = false;

    bool scanBtoT =
        m_videoWidget->videoSurface()->surfaceFormat().scanLineDirection() ==
        QVideoSurfaceFormat::BottomToTop;
    bool upsideDown = m_upsideDownCB->isChecked();

    image = image.mirrored(upsideDown, upsideDown != scanBtoT);

    if (importImage(image)) {
      m_videoWidget->setPreviousImage(image.copy());
      if (Preferences::instance()->isShowFrameNumberWithLettersEnabled()) {
        TFrameId fId = m_frameNumberEdit->getValue();
        int f        = fId.getNumber();
        if (f % 10 == 0)  // next number
          m_frameNumberEdit->setValue(TFrameId(((int)(f / 10) + 1) * 10));
        else  // next alphabet
          m_frameNumberEdit->setValue(TFrameId(f + 1));
      } else {
        TFrameId fId = m_frameNumberEdit->getValue();

        if (fId.getLetter() == '\0' || fId.getLetter() == 'Z' ||
            fId.getLetter() == 'z')  // next number
          m_frameNumberEdit->setValue(TFrameId(fId.getNumber() + 1));
        else {  // next alphabet
          QByteArray byteArray = fId.getLetter().toUtf8();
          // return incrementing the last letter
          byteArray.data()[byteArray.size() - 1]++;
          m_frameNumberEdit->setValue(
              TFrameId(fId.getNumber(), QString::fromUtf8(byteArray)));
        }
      }

      /* notify */
      TApp::instance()->getCurrentScene()->notifySceneChanged();
      TApp::instance()->getCurrentScene()->notifyCastChange();
      TApp::instance()->getCurrentXsheet()->notifyXsheetChanged();

      // restart interval timer for capturing next frame (it is single shot)
      if (m_timerCB->isChecked() && m_captureButton->isChecked()) {
        m_captureTimer->start(m_timerIntervalFld->getValue() * 1000);
        // restart the count down as well (for aligning the timing. It is not
        // single shot)
        if (m_timerIntervalFld->getValue() != 0) m_countdownTimer->start(100);
      }
    }
    // if capture was failed, stop interval capturing
    else if (m_timerCB->isChecked()) {
      m_captureButton->setChecked(false);
      onCaptureButtonClicked(false);
    }
#ifndef MACOSX
    m_currentCamera->start();
#endif
  }
}

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

void PencilTestPopup::showEvent(QShowEvent* event) {
  // m_timerId = startTimer(10);

  // if there is another action of which "return" key is assigned as short cut
  // key,
  // then release the shortcut key temporary while the popup opens
  QAction* action = CommandManager::instance()->getActionFromShortcut("Return");
  if (action) action->setShortcut(QKeySequence(""));

  connect(m_videoWidget->videoSurface(), SIGNAL(frameCaptured(QImage&)), this,
          SLOT(onFrameCaptured(QImage&)));

  // reload camera
  if (m_currentCamera) {
    if (m_currentCamera->state() == QCamera::UnloadedState)
      m_currentCamera->load();
    if (m_currentCamera->state() == QCamera::LoadedState)
      m_currentCamera->start();
  }

  TSceneHandle* sceneHandle = TApp::instance()->getCurrentScene();
  connect(sceneHandle, SIGNAL(sceneSwitched()), this, SLOT(onSceneSwitched()));
  connect(sceneHandle, SIGNAL(castChanged()), this, SLOT(refreshFrameInfo()));

  bool tmp_alwaysOverwrite = m_alwaysOverwrite;
  onSceneSwitched();
  m_alwaysOverwrite = tmp_alwaysOverwrite;
}

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

void PencilTestPopup::hideEvent(QHideEvent* event) {
  // set back the "return" short cut key
  QAction* action = CommandManager::instance()->getActionFromShortcut("Return");
  if (action) action->setShortcut(QKeySequence("Return"));

  disconnect(m_videoWidget->videoSurface(), SIGNAL(frameCaptured(QImage&)),
             this, SLOT(onFrameCaptured(QImage&)));

  // stop interval timer if it is active
  if (m_timerCB->isChecked() && m_captureButton->isChecked()) {
    m_captureButton->setChecked(false);
    onCaptureButtonClicked(false);
  }

  // release camera
  if (m_currentCamera) {
    if (m_currentCamera->state() == QCamera::ActiveState)
      m_currentCamera->stop();
    if (m_currentCamera->state() == QCamera::LoadedState)
      m_currentCamera->unload();
  }
  Dialog::hideEvent(event);

  TSceneHandle* sceneHandle = TApp::instance()->getCurrentScene();
  disconnect(sceneHandle, SIGNAL(sceneSwitched()), this,
             SLOT(refreshFrameInfo()));
  disconnect(sceneHandle, SIGNAL(castChanged()), this,
             SLOT(refreshFrameInfo()));
}

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

void PencilTestPopup::keyPressEvent(QKeyEvent* event) {
  // override return (or enter) key as shortcut key for capturing
  int key = event->key();
  if (key == Qt::Key_Return || key == Qt::Key_Enter) {
    // show button-clicking animation followed by calling
    // onCaptureButtonClicked()
    m_captureButton->animateClick();
    event->accept();
  } else
    event->ignore();
}

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

bool PencilTestPopup::event(QEvent* event) {
  if (event->type() == QEvent::ShortcutOverride) {
    QKeyEvent* ke = static_cast<QKeyEvent*>(event);
    int key       = ke->key();
    if (key >= Qt::Key_0 && key <= Qt::Key_9) {
      if (!m_frameNumberEdit->hasFocus()) {
        m_frameNumberEdit->setFocus();
        m_frameNumberEdit->clear();
      }
      event->accept();
      return true;
    }
  }
  return DVGui::Dialog::event(event);
}
//-----------------------------------------------------------------------------

void PencilTestPopup::processImage(QImage& image) {
  /* "upside down" is not executed here. It will be done when capturing the
   * image */
  // white bg reduction
  if (!m_whiteBGImg.isNull() && m_bgReductionFld->getValue() != 0) {
    bgReduction(image, m_whiteBGImg, m_bgReductionFld->getValue());
  }
  // obtain histogram AFTER bg reduction
  m_camCapLevelControl->updateHistogram(image);

  // color and grayscale mode
  if (m_colorTypeCombo->currentIndex() != 2) {
    int black, white;
    float gamma;
    m_camCapLevelControl->getValues(black, white, gamma);
    if (black != 0 || white != 255 || gamma != 1.0)
      onChange(image, black, white, gamma,
               m_colorTypeCombo->currentIndex() != 0);
  } else {
    onChangeBW(image, m_camCapLevelControl->getThreshold());
  }
}

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

void PencilTestPopup::onCaptureWhiteBGButtonPressed() {
  m_captureWhiteBGCue = true;
}

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

void PencilTestPopup::onOnionCBToggled(bool on) {
  m_videoWidget->setShowOnionSkin(on);
  m_onionOpacityFld->setEnabled(on);
}

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

void PencilTestPopup::onLoadImageButtonPressed() {
  TCellSelection* cellSelection = dynamic_cast<TCellSelection*>(
      TApp::instance()->getCurrentSelection()->getSelection());
  if (cellSelection) {
    int c0;
    int r0;
    TCellSelection::Range range = cellSelection->getSelectedCells();
    if (range.isEmpty()) {
      error(tr("No image selected.  Please select an image in the Xsheet."));
      return;
    }
    c0 = range.m_c0;
    r0 = range.m_r0;
    TXshCell cell =
        TApp::instance()->getCurrentXsheet()->getXsheet()->getCell(r0, c0);
    if (cell.getSimpleLevel() == nullptr) {
      error(tr("No image selected.  Please select an image in the Xsheet."));
      return;
    }
    TXshSimpleLevel* level = cell.getSimpleLevel();
    int type               = level->getType();
    if (type != TXshLevelType::OVL_XSHLEVEL) {
      error(tr("The selected image is not in a raster level."));
      return;
    }
    TImageP origImage = cell.getImage(false);
    TRasterImageP tempImage(origImage);
    TRasterImage* image = (TRasterImage*)tempImage->cloneImage();

    int m_lx          = image->getRaster()->getLx();
    int m_ly          = image->getRaster()->getLy();
    QString res       = m_resolutionCombo->currentText();
    QStringList texts = res.split(' ');
    if (m_lx != texts[0].toInt() || m_ly != texts[2].toInt()) {
      error(
          tr("The selected image size does not match the current camera "
             "settings."));
      return;
    }
    int m_bpp      = image->getRaster()->getPixelSize();
    int totalBytes = m_lx * m_ly * m_bpp;
    image->getRaster()->yMirror();

    // lock raster to get data
    image->getRaster()->lock();
    void* buffin = image->getRaster()->getRawData();
    assert(buffin);
    void* buffer = malloc(totalBytes);
    memcpy(buffer, buffin, totalBytes);

    image->getRaster()->unlock();

    QImage qi = QImage((uint8_t*)buffer, m_lx, m_ly, QImage::Format_ARGB32);
    QImage qi2(qi.size(), QImage::Format_ARGB32);
    qi2.fill(QColor(Qt::white).rgb());
    QPainter painter(&qi2);
    painter.drawImage(0, 0, qi);
    m_videoWidget->setPreviousImage(qi2);
    m_onionSkinCB->setChecked(true);
    free(buffer);
  }
}

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

void PencilTestPopup::onOnionOpacityFldEdited() {
  int value = (int)(255.0f * (float)m_onionOpacityFld->getValue() / 100.0f);
  m_videoWidget->setOnionOpacity(value);
}

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

void PencilTestPopup::onTimerCBToggled(bool on) {
  m_timerIntervalFld->setEnabled(on);
  m_captureButton->setCheckable(on);
  if (on)
    m_captureButton->setText(tr("Start Capturing\n[Return key]"));
  else
    m_captureButton->setText(tr("Capture\n[Return key]"));
}

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

void PencilTestPopup::onCaptureButtonClicked(bool on) {
  if (m_timerCB->isChecked()) {
    m_timerCB->setDisabled(on);
    m_timerIntervalFld->setDisabled(on);
    // start interval capturing
    if (on) {
      m_captureButton->setText(tr("Stop Capturing\n[Return key]"));
      m_captureTimer->start(m_timerIntervalFld->getValue() * 1000);
      if (m_timerIntervalFld->getValue() != 0) m_countdownTimer->start(100);
    }
    // stop interval capturing
    else {
      m_captureButton->setText(tr("Start Capturing\n[Return key]"));
      m_captureTimer->stop();
      m_countdownTimer->stop();
      // hide the count down text
      m_videoWidget->showCountDownTime(0);
    }
  }
  // capture immediately
  else
    m_captureCue = true;
}

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

void PencilTestPopup::onCaptureTimerTimeout() { m_captureCue = true; }

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

void PencilTestPopup::onCountDown() {
  m_videoWidget->showCountDownTime(
      m_captureTimer->isActive() ? m_captureTimer->remainingTime() : 0);
}

//-----------------------------------------------------------------------------
/*! referenced from LevelCreatePopup::apply()
 */
bool PencilTestPopup::importImage(QImage image) {
  TApp* app         = TApp::instance();
  ToonzScene* scene = app->getCurrentScene()->getScene();
  TXsheet* xsh      = scene->getXsheet();

  std::wstring levelName = m_levelNameEdit->text().toStdWString();
  if (levelName.empty()) {
    error(tr("No level name specified: please choose a valid level name"));
    return false;
  }

  TFrameId fId = m_frameNumberEdit->getValue();

  /* create parent directory if it does not exist */
  TFilePath parentDir =
      scene->decodeFilePath(TFilePath(m_saveInFileFld->getPath()));
  if (!TFileStatus(parentDir).doesExist()) {
    QString question;
    question = tr("Folder %1 doesn't exist.\nDo you want to create it?")
                   .arg(toQString(parentDir));
    int ret = DVGui::MsgBox(question, QObject::tr("Yes"), QObject::tr("No"));
    if (ret == 0 || ret == 2) return false;
    try {
      TSystem::mkDir(parentDir);
      DvDirModel::instance()->refreshFolder(parentDir.getParentDir());
    } catch (...) {
      error(tr("Unable to create") + toQString(parentDir));
      return false;
    }
  }

  TFrameId tmplFId = scene->getProperties()->formatTemplateFIdForInput();

  TFilePath levelFp = TFilePath(m_saveInFileFld->getPath()) +
                      TFilePath(levelName + L"." +
                                m_fileTypeCombo->currentText().toStdWString())
                          .withFrame(tmplFId);
  TFilePath actualLevelFp = scene->decodeFilePath(levelFp);

  TXshSimpleLevel* sl = 0;

  TXshLevel* level = scene->getLevelSet()->getLevel(levelName);
  enum State { NEWLEVEL = 0, ADDFRAME, OVERWRITE } state;

  // retrieve subcamera image
  if (m_subcameraButton->isChecked() &&
      m_videoWidget->subCameraRect().isValid())
    image = image.copy(m_videoWidget->subCameraRect());

  /* if the level already exists in the scene cast */
  if (level) {
    /* if the existing level is not a raster level, then return */
    if (level->getType() != OVL_XSHLEVEL) {
      error(
          tr("The level name specified is already used: please choose a "
             "different level name."));
      return false;
    }
    /* if the existing level does not match file path and pixel size, then
     * return */
    sl = level->getSimpleLevel();
    if (scene->decodeFilePath(sl->getPath()) != actualLevelFp) {
      error(
          tr("The save in path specified does not match with the existing "
             "level."));
      return false;
    }
    if (sl->getProperties()->getImageRes() !=
        TDimension(image.width(), image.height())) {
      error(tr(
          "The captured image size does not match with the existing level."));
      return false;
    }

    // if the level already has a frame, use the same zero padding regardless of
    // the frame format setting
    sl->formatFId(fId, tmplFId);

    /* if the level already have the same frame, then ask if overwrite it */
    TFilePath frameFp(actualLevelFp.withFrame(fId));
    if (TFileStatus(frameFp).doesExist()) {
      if (!m_alwaysOverwrite) {
        QString question =
            tr("File %1 does exist.\nDo you want to overwrite it?")
                .arg(toQString(frameFp));
        int ret = DVGui::MsgBox(question, QObject::tr("Overwrite"),
                                QObject::tr("Always Overwrite in This Scene"),
                                QObject::tr("Cancel"));
        if (ret == 0 || ret == 3) return false;
        if (ret == 2) m_alwaysOverwrite = true;
      }
      state = OVERWRITE;
    } else
      state = ADDFRAME;
  }
  /* if the level does not exist in the scene cast */
  else {
    /* if the file does exist, load it first */
    if (TSystem::doesExistFileOrLevel(actualLevelFp)) {
      level = scene->loadLevel(actualLevelFp);
      if (!level) {
        error(tr("Failed to load %1.").arg(toQString(actualLevelFp)));
        return false;
      }

      /* if the loaded level does not match in pixel size, then return */
      sl = level->getSimpleLevel();
      if (!sl || sl->getProperties()->getImageRes() !=
                     TDimension(image.width(), image.height())) {
        error(tr(
            "The captured image size does not match with the existing level."));
        return false;
      }
      // if the level already has a frame, use the same zero padding regardless
      // of the frame format setting
      sl->formatFId(fId, tmplFId);

      /* confirm overwrite */
      TFilePath frameFp(actualLevelFp.withFrame(fId));
      if (TFileStatus(frameFp).doesExist()) {
        if (!m_alwaysOverwrite) {
          QString question =
              tr("File %1 does exist.\nDo you want to overwrite it?")
                  .arg(toQString(frameFp));
          int ret = DVGui::MsgBox(question, QObject::tr("Overwrite"),
                                  QObject::tr("Always Overwrite in This Scene"),
                                  QObject::tr("Cancel"));
          if (ret == 0 || ret == 3) return false;
          if (ret == 2) m_alwaysOverwrite = true;
        }
      }
    }
    /* if the file does not exist, then create a new level */
    else {
      TXshLevel* level = scene->createNewLevel(OVL_XSHLEVEL, levelName,
                                               TDimension(), 0, levelFp);
      sl               = level->getSimpleLevel();
      sl->setPath(levelFp, true);
      sl->getProperties()->setDpiPolicy(LevelProperties::DP_CustomDpi);
      TPointD dpi;
      // if the subcamera is not active or the pixel unit is used, apply the
      // current camera dpi
      if (!m_subcameraButton->isChecked() ||
          !m_videoWidget->subCameraRect().isValid() ||
          Preferences::instance()->getPixelsOnly())
        dpi = getCurrentCameraDpi();
      // if the subcamera is active, compute the dpi so that the image will fit
      // to the camera frame
      else {
        TCamera* camera =
            TApp::instance()->getCurrentScene()->getScene()->getCurrentCamera();
        TDimensionD size = camera->getSize();
        double minimumDpi =
            std::min(image.width() / size.lx, image.height() / size.ly);
        dpi = TPointD(minimumDpi, minimumDpi);
      }
      sl->getProperties()->setDpi(dpi.x);
      sl->getProperties()->setImageDpi(dpi);
      sl->getProperties()->setImageRes(
          TDimension(image.width(), image.height()));
      sl->formatFId(fId, tmplFId);
    }

    state = NEWLEVEL;
  }

  TPointD levelDpi = sl->getDpi();
  /* create the raster */
  TRaster32P raster(image.width(), image.height());
  convertImageToRaster(raster, image.mirrored(true, true));

  TRasterImageP ri(raster);
  ri->setDpi(levelDpi.x, levelDpi.y);
  /* setting the frame */
  sl->setFrame(fId, ri);

  /* set dirty flag */
  sl->getProperties()->setDirtyFlag(true);

  if (m_saveOnCaptureCB->isChecked()) sl->save();

  /* placement in xsheet */

  int row = app->getCurrentFrame()->getFrame();
  int col = app->getCurrentColumn()->getColumnIndex();

  // if the level is newly created or imported, then insert a new column
  if (state == NEWLEVEL) {
    if (col < 0 || !xsh->isColumnEmpty(col)) {
      col += 1;
      xsh->insertColumn(col);
    }
    xsh->setCell(row, col, TXshCell(sl, fId));
    app->getCurrentColumn()->setColumnIndex(col);
    return true;
  }

  // state == OVERWRITE, ADDFRAME

  // if the same cell is already in the column, then just replace the content
  // and do not set a new cell
  int foundCol, foundRow = -1;
  // most possibly, it's in the current column
  int rowCheck;
  if (findCell(xsh, col, TXshCell(sl, fId), rowCheck)) return true;
  if (rowCheck >= 0) {
    foundRow = rowCheck;
    foundCol = col;
  }
  // search entire xsheet
  for (int c = 0; c < xsh->getColumnCount(); c++) {
    if (c == col) continue;
    if (findCell(xsh, c, TXshCell(sl, fId), rowCheck)) return true;
    if (rowCheck >= 0) {
      foundRow = rowCheck;
      foundCol = c;
    }
  }
  // if there is a column containing the same level
  if (foundRow >= 0) {
    // put the cell at the bottom
    int tmpRow = foundRow + 1;
    while (1) {
      if (xsh->getCell(tmpRow, foundCol).isEmpty()) {
        xsh->setCell(tmpRow, foundCol, TXshCell(sl, fId));
        app->getCurrentColumn()->setColumnIndex(foundCol);
        break;
      }
      tmpRow++;
    }
  }
  // if the level is registered in the scene, but is not placed in the xsheet,
  // then insert a new column
  else {
    if (!xsh->isColumnEmpty(col)) {
      col += 1;
      xsh->insertColumn(col);
    }
    xsh->setCell(row, col, TXshCell(sl, fId));
    app->getCurrentColumn()->setColumnIndex(col);
  }

  return true;
}

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

void PencilTestPopup::onCaptureFilterSettingsBtnPressed() {
  if (!m_currentCamera || m_deviceName.isNull()) return;

  QList<QCameraInfo> cameras = QCameraInfo::availableCameras();
  for (int c = 0; c < cameras.size(); c++) {
    if (cameras.at(c).deviceName() == m_deviceName) {
#ifdef _WIN32
      openCaptureFilterSettings(this, cameras.at(c).description());
#endif
      return;
    }
  }
}

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

void PencilTestPopup::openSaveInFolderPopup() {
  if (m_saveInFolderPopup->exec()) {
    QString oldPath = m_saveInFileFld->getPath();
    m_saveInFileFld->setPath(m_saveInFolderPopup->getPath());
    if (oldPath == m_saveInFileFld->getPath())
      setToNextNewLevel();
    else {
      onSaveInPathEdited();
    }
  }
}

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

// Refresh information that how many & which frames are saved for the current
// level
void PencilTestPopup::refreshFrameInfo() {
  if (!m_currentCamera || m_deviceName.isNull()) {
    m_frameInfoLabel->setText("");
    return;
  }

  QString tooltipStr, labelStr;
  enum InfoType { NEW = 0, ADD, OVERWRITE, WARNING } infoType(WARNING);

  static QColor infoColors[4] = {Qt::cyan, Qt::green, Qt::yellow, Qt::red};

  ToonzScene* currentScene = TApp::instance()->getCurrentScene()->getScene();
  TLevelSet* levelSet      = currentScene->getLevelSet();

  std::wstring levelName = m_levelNameEdit->text().toStdWString();
  TFrameId fId           = m_frameNumberEdit->getValue();

  TDimension camRes;
  if (m_subcameraButton->isChecked())
    camRes = TDimension(m_subWidthFld->getValue(), m_subHeightFld->getValue());
  else {
    QStringList texts = m_resolutionCombo->currentText().split(' ');
    if (texts.size() != 3) return;
    camRes = TDimension(texts[0].toInt(), texts[2].toInt());
  }

  bool letterOptionEnabled =
      Preferences::instance()->isShowFrameNumberWithLettersEnabled();

  // level with the same name
  TXshLevel* level_sameName = levelSet->getLevel(levelName);

  TOutputProperties* prop =
      currentScene->getProperties()->getPreviewProperties();
  TFrameId tmplFId = prop->formatTemplateFId();

  TFilePath levelFp = TFilePath(m_saveInFileFld->getPath()) +
                      TFilePath(levelName + L"." +
                                m_fileTypeCombo->currentText().toStdWString())
                          .withFrame(tmplFId);

  // level with the same path
  TXshLevel* level_samePath = levelSet->getLevel(*(currentScene), levelFp);

  TFilePath actualLevelFp = currentScene->decodeFilePath(levelFp);

  // level existence
  bool levelExist = TSystem::doesExistFileOrLevel(actualLevelFp);

  // frame existence
  TFilePath frameFp(actualLevelFp.withFrame(fId));
  bool frameExist = false;
  if (levelExist) frameExist = TFileStatus(frameFp).doesExist();

  // reset acceptable camera size
  m_allowedCameraSize = QSize();

  // ### CASE 1 ###
  // If there is no same level registered in the scene cast
  if (!level_sameName && !level_samePath) {
    // If there is a level in the file system
    if (levelExist) {
      TLevelReaderP lr;
      TLevelP level_p;
      try {
        lr = TLevelReaderP(actualLevelFp);
      } catch (...) {
        // TODO: output something
        m_frameInfoLabel->setText(tr("UNDEFINED WARNING"));
        return;
      }
      if (!lr) {
        // TODO: output something
        m_frameInfoLabel->setText(tr("UNDEFINED WARNING"));
        return;
      }
      try {
        level_p = lr->loadInfo();
      } catch (...) {
        // TODO: output something
        m_frameInfoLabel->setText(tr("UNDEFINED WARNING"));
        return;
      }
      if (!level_p) {
        // TODO: output something
        m_frameInfoLabel->setText(tr("UNDEFINED WARNING"));
        return;
      }
      int frameCount      = level_p->getFrameCount();
      TLevel::Iterator it = level_p->begin();
      std::vector<TFrameId> fids;
      for (int i = 0; it != level_p->end(); ++it, ++i) {
        fids.push_back(it->first);
        // in case fId with different format
        if (!frameExist && fId == it->first) frameExist = true;
      }

      tooltipStr +=
          tr("The level is not registered in the scene, but exists in the file "
             "system.");

      // check resolution
      const TImageInfo* ii;
      try {
        ii = lr->getImageInfo(fids[0]);
      } catch (...) {
        // TODO: output something
        m_frameInfoLabel->setText(tr("UNDEFINED WARNING"));
        return;
      }
      TDimension dim(ii->m_lx, ii->m_ly);
      // if the saved images has not the same resolution as the current camera
      // resolution
      if (camRes != dim) {
        tooltipStr += tr("\nWARNING : Image size mismatch. The saved image "
                         "size is %1 x %2.")
                          .arg(dim.lx)
                          .arg(dim.ly);
        labelStr += tr("WARNING");
        infoType = WARNING;
      }
      // if the resolutions are matched
      else {
        if (frameCount == 1)
          tooltipStr += tr("\nFrame %1 exists.")
                            .arg(fidsToString(fids, letterOptionEnabled));
        else
          tooltipStr += tr("\nFrames %1 exist.")
                            .arg(fidsToString(fids, letterOptionEnabled));
        // if the frame exists, then it will be overwritten
        if (frameExist) {
          labelStr += tr("OVERWRITE 1 of");
          infoType = OVERWRITE;
        } else {
          labelStr += tr("ADD to");
          infoType = ADD;
        }
        if (frameCount == 1)
          labelStr += tr(" %1 frame").arg(frameCount);
        else
          labelStr += tr(" %1 frames").arg(frameCount);
      }
      m_allowedCameraSize = QSize(dim.lx, dim.ly);
    }
    // If no level exists in the file system, then it will be a new level
    else {
      tooltipStr += tr("The level will be newly created.");
      labelStr += tr("NEW");
      infoType = NEW;
    }
  }
  // ### CASE 2 ###
  // If there is already the level registered in the scene cast
  else if (level_sameName && level_samePath &&
           level_sameName == level_samePath) {
    tooltipStr += tr("The level is already registered in the scene.");
    if (!levelExist) tooltipStr += tr("\nNOTE : The level is not saved.");

    std::vector<TFrameId> fids;
    level_sameName->getFids(fids);

    // check resolution
    TDimension dim;
    bool ret = getRasterLevelSize(level_sameName, dim);
    if (!ret) {
      tooltipStr +=
          tr("\nWARNING : Failed to get image size of the existing level %1.")
              .arg(QString::fromStdWString(levelName));
      labelStr += tr("WARNING");
      infoType = WARNING;
    }
    // if the saved images has not the same resolution as the current camera
    // resolution
    else if (camRes != dim) {
      tooltipStr += tr("\nWARNING : Image size mismatch. The existing level "
                       "size is %1 x %2.")
                        .arg(dim.lx)
                        .arg(dim.ly);
      labelStr += tr("WARNING");
      infoType = WARNING;
    }
    // if the resolutions are matched
    else {
      int frameCount = fids.size();
      if (fids.size() == 1)
        tooltipStr += tr("\nFrame %1 exists.")
                          .arg(fidsToString(fids, letterOptionEnabled));
      else
        tooltipStr += tr("\nFrames %1 exist.")
                          .arg(fidsToString(fids, letterOptionEnabled));
      // Check if the target frame already exist in the level
      bool hasFrame = false;
      for (int f = 0; f < frameCount; f++) {
        if (fids.at(f) == fId) {
          hasFrame = true;
          break;
        }
      }
      // If there is already the frame then it will be overwritten
      if (hasFrame) {
        labelStr += tr("OVERWRITE 1 of");
        infoType = OVERWRITE;
      }
      // Or, the frame will be added to the level
      else {
        labelStr += tr("ADD to");
        infoType = ADD;
      }
      if (frameCount == 1)
        labelStr += tr(" %1 frame").arg(frameCount);
      else
        labelStr += tr(" %1 frames").arg(frameCount);
    }
    m_allowedCameraSize = QSize(dim.lx, dim.ly);
  }
  // ### CASE 3 ###
  // If there are some conflicts with the existing level.
  else {
    if (level_sameName) {
      TFilePath anotherPath = level_sameName->getPath();
      tooltipStr +=
          tr("WARNING : Level name conflicts. There already is a level %1 in the scene with the path\
                        \n          %2.")
              .arg(QString::fromStdWString(levelName))
              .arg(toQString(anotherPath));
      // check resolution
      TDimension dim;
      bool ret = getRasterLevelSize(level_sameName, dim);
      if (ret && camRes != dim)
        tooltipStr += tr("\nWARNING : Image size mismatch. The size of level "
                         "with the same name is is %1 x %2.")
                          .arg(dim.lx)
                          .arg(dim.ly);
      m_allowedCameraSize = QSize(dim.lx, dim.ly);
    }
    if (level_samePath) {
      std::wstring anotherName = level_samePath->getName();
      if (!tooltipStr.isEmpty()) tooltipStr += QString("\n");
      tooltipStr +=
          tr("WARNING : Level path conflicts. There already is a level with the path %1\
                        \n          in the scene with the name %2.")
              .arg(toQString(levelFp))
              .arg(QString::fromStdWString(anotherName));
      // check resolution
      TDimension dim;
      bool ret = getRasterLevelSize(level_samePath, dim);
      if (ret && camRes != dim)
        tooltipStr += tr("\nWARNING : Image size mismatch. The size of level "
                         "with the same path is %1 x %2.")
                          .arg(dim.lx)
                          .arg(dim.ly);
      m_allowedCameraSize = QSize(dim.lx, dim.ly);
    }
    labelStr += tr("WARNING");
    infoType = WARNING;
  }

  QColor infoColor = infoColors[(int)infoType];
  m_frameInfoLabel->setStyleSheet(QString("QLabel{color: %1;}\
                                          QLabel QWidget{ color: black;}")
                                      .arg(infoColor.name()));
  m_frameInfoLabel->setText(labelStr);
  m_frameInfoLabel->setToolTip(tooltipStr);
}

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

void PencilTestPopup::onSaveInPathEdited() {
  ToonzScene* scene = TApp::instance()->getCurrentScene()->getScene();
  TFilePath saveInPath(m_saveInFileFld->getPath().toStdWString());
  scene->getProperties()->setCameraCaptureSaveInPath(saveInPath);
  refreshFrameInfo();
}

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

void PencilTestPopup::onSceneSwitched() {
  m_saveInFolderPopup->updateParentFolder();
  m_saveInFileFld->setPath(m_saveInFolderPopup->getParentPath());
  refreshFrameInfo();
  m_alwaysOverwrite = false;
}

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

void PencilTestPopup::onSubCameraToggled(bool on) {
  m_videoWidget->setSubCameraSize(
      on ? QSize(m_subWidthFld->getValue(), m_subHeightFld->getValue())
         : QSize());
  refreshFrameInfo();
}

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

void PencilTestPopup::onSubCameraResized(bool isDragging) {
  QSize subSize = m_videoWidget->subCameraRect().size();
  assert(subSize.isValid());
  m_subWidthFld->setValue(subSize.width());
  m_subHeightFld->setValue(subSize.height());
  if (!isDragging) refreshFrameInfo();
}

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

void PencilTestPopup::onSubCameraSizeEdited() {
  m_videoWidget->setSubCameraSize(
      QSize(m_subWidthFld->getValue(), m_subHeightFld->getValue()));
  refreshFrameInfo();
}

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

OpenPopupCommandHandler<PencilTestPopup> openPencilTestPopup(MI_PencilTest);

// specialized in order to call openSaveInFolderPopup()
template <>
void OpenPopupCommandHandler<PencilTestPopup>::execute() {
  if (!m_popup) m_popup = new PencilTestPopup();
  m_popup->show();
  m_popup->raise();
  m_popup->activateWindow();
  if (CamCapOpenSaveInPopupOnLaunch != 0) m_popup->openSaveInFolderPopup();
}