

#include "preferencespopup.h"

// Tnz6 includes
#include "menubarcommandids.h"
#include "versioncontrol.h"
#include "permissionsmanager.h"
#include "versioncontrolxmlwriter.h"
#include "levelsettingspopup.h"
#include "tapp.h"
#include "cleanupsettingsmodel.h"
#include "formatsettingspopups.h"
#include "columncommand.h"

// TnzQt includes
#include "toonzqt/tabbar.h"
#include "toonzqt/menubarcommand.h"
#include "toonzqt/checkbox.h"
#include "toonzqt/gutil.h"
#include "toonzqt/doublefield.h"
#include "toonzqt/dvdialog.h"
#include "toonzqt/filefield.h"
#include "toonzqt/lutcalibrator.h"

// TnzLib includes
#include "toonz/txsheethandle.h"
#include "toonz/tscenehandle.h"
#include "toonz/txshlevelhandle.h"
#include "toonz/txshleveltypes.h"
#include "toonz/toonzscene.h"
#include "toonz/tcamera.h"
#include "toonz/levelproperties.h"
#include "toonz/tonionskinmaskhandle.h"
#include "toonz/stage.h"

// TnzCore includes
#include "tsystem.h"
#include "tfont.h"

// TnzTools includes
#include "tools/toolhandle.h"

#include "kis_tablet_support_win8.h"

// Qt includes
#include <QHBoxLayout>
#include <QComboBox>
#include <QFontComboBox>
#include <QLabel>
#include <QStackedWidget>
#include <QLineEdit>
#include <QFileDialog>
#include <QFile>
#include <QPushButton>
#include <QApplication>
#include <QMainWindow>
#include <QStringList>
#include <QListWidget>
#include <QGroupBox>
#include <QKeySequence>

using namespace DVGui;

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

namespace {
enum DpiPolicy { DP_ImageDpi, DP_CustomDpi };

inline void setupLayout(QGridLayout* lay, int margin = 15) {
  lay->setContentsMargins(margin, margin, margin, margin);
  lay->setHorizontalSpacing(5);
  lay->setVerticalSpacing(10);
  lay->setColumnStretch(2, 1);
}

QGridLayout* insertGroupBox(const QString label, QGridLayout* layout) {
  QGroupBox* box   = new QGroupBox(label);
  QGridLayout* lay = new QGridLayout();
  setupLayout(lay, 5);
  box->setLayout(lay);
  layout->addWidget(box, layout->rowCount(), 0, 1, 3);
  return lay;
}

inline TPixel colorToTPixel(const QColor& color) {
  return TPixel(color.red(), color.green(), color.blue(), color.alpha());
}
}  // namespace

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

SizeField::SizeField(QSize min, QSize max, QSize value, QWidget* parent)
    : QWidget(parent) {
  m_fieldX =
      new DVGui::IntLineEdit(this, value.width(), min.width(), max.width());
  m_fieldY =
      new DVGui::IntLineEdit(this, value.height(), min.height(), max.height());
  QHBoxLayout* lay = new QHBoxLayout();
  lay->setSpacing(5);
  lay->setContentsMargins(0, 0, 0, 0);
  lay->addWidget(m_fieldX, 1);
  lay->addWidget(new QLabel("x", this), 0);
  lay->addWidget(m_fieldY, 1);
  lay->addStretch(1);
  setLayout(lay);

  connect(m_fieldX, &DVGui::IntLineEdit::editingFinished, this,
          &SizeField::editingFinished);
  connect(m_fieldY, &DVGui::IntLineEdit::editingFinished, this,
          &SizeField::editingFinished);
}

QSize SizeField::getValue() const {
  return QSize(m_fieldX->getValue(), m_fieldY->getValue());
}

void SizeField::setValue(const QSize& size) {
  m_fieldX->setValue(size.width());
  m_fieldY->setValue(size.height());
}

//**********************************************************************************
//    PreferencesPopup::FormatProperties  implementation
//**********************************************************************************

PreferencesPopup::FormatProperties::FormatProperties(PreferencesPopup* parent)
    : DVGui::Dialog(parent, false, true) {
  setWindowTitle(tr("Level Settings by File Format"));
  setModal(true);  // The underlying selected format should not
  // be changed by the user

  // Main layout
  beginVLayout();

  QGridLayout* gridLayout = new QGridLayout;
  int row                 = 0;

  // Key values
  QLabel* nameLabel = new QLabel(tr("Name:"));
  nameLabel->setFixedHeight(20);  // Due to DVGui::Dialog's quirky behavior
  gridLayout->addWidget(nameLabel, row, 0, Qt::AlignRight);

  m_name = new DVGui::LineEdit;
  gridLayout->addWidget(m_name, row++, 1);

  QLabel* regExpLabel = new QLabel(tr("Regular Expression:"));
  gridLayout->addWidget(regExpLabel, row, 0, Qt::AlignRight);

  m_regExp = new DVGui::LineEdit;
  gridLayout->addWidget(m_regExp, row++, 1);

  QLabel* priorityLabel = new QLabel(tr("Priority"));
  gridLayout->addWidget(priorityLabel, row, 0, Qt::AlignRight);

  m_priority = new DVGui::IntLineEdit;
  gridLayout->addWidget(m_priority, row++, 1);

  gridLayout->setRowMinimumHeight(row++, 20);

  // LevelProperties
  m_dpiPolicy = new QComboBox;
  gridLayout->addWidget(m_dpiPolicy, row++, 1);

  m_dpiPolicy->addItem(QObject::tr("Image DPI"));
  m_dpiPolicy->addItem(QObject::tr("Custom DPI"));

  QLabel* dpiLabel = new QLabel(LevelSettingsPopup::tr("DPI:"));
  gridLayout->addWidget(dpiLabel, row, 0, Qt::AlignRight);

  m_dpi = new DVGui::DoubleLineEdit;
  m_dpi->setRange(1, (std::numeric_limits<double>::max)());
  gridLayout->addWidget(m_dpi, row++, 1);

  m_premultiply = new DVGui::CheckBox(LevelSettingsPopup::tr("Premultiply"));
  gridLayout->addWidget(m_premultiply, row++, 1);

  m_whiteTransp =
      new DVGui::CheckBox(LevelSettingsPopup::tr("White As Transparent"));
  gridLayout->addWidget(m_whiteTransp, row++, 1);

  m_doAntialias =
      new DVGui::CheckBox(LevelSettingsPopup::tr("Add Antialiasing"));
  gridLayout->addWidget(m_doAntialias, row++, 1);

  QLabel* antialiasLabel =
      new QLabel(LevelSettingsPopup::tr("Antialias Softness:"));
  gridLayout->addWidget(antialiasLabel, row, 0, Qt::AlignRight);

  m_antialias = new DVGui::IntLineEdit(this, 10, 0, 100);
  gridLayout->addWidget(m_antialias, row++, 1);

  QLabel* subsamplingLabel = new QLabel(LevelSettingsPopup::tr("Subsampling:"));
  gridLayout->addWidget(subsamplingLabel, row, 0, Qt::AlignRight);

  m_subsampling = new DVGui::IntLineEdit(this, 1, 1);
  gridLayout->addWidget(m_subsampling, row++, 1);

  QLabel* gammaLabel = new QLabel(LevelSettingsPopup::tr("Color Space Gamma:"));
  gridLayout->addWidget(gammaLabel, row, 0, Qt::AlignRight);

  m_colorSpaceGamma = new DVGui::DoubleLineEdit(this);
  m_colorSpaceGamma->setRange(0.1, 10.);
  gridLayout->addWidget(m_colorSpaceGamma, row++, 1);

  addLayout(gridLayout);

  endVLayout();

  // Establish connections
  // enable gamma field only when the regexp field contains ".exr"
  connect(m_regExp, &DVGui::LineEdit::editingFinished, this,
          &FormatProperties::updateEnabledStatus);
  connect(m_dpiPolicy, QOverload<int>::of(&QComboBox::currentIndexChanged),
          this, &FormatProperties::updateEnabledStatus);
  connect(m_doAntialias, &QAbstractButton::clicked, this,
          &FormatProperties::updateEnabledStatus);
}

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

void PreferencesPopup::FormatProperties::updateEnabledStatus() {
  m_dpi->setEnabled(m_dpiPolicy->currentIndex() == DP_CustomDpi);
  m_antialias->setEnabled(m_doAntialias->isChecked());

  // enable gamma field only when the regexp field contains ".exr"
  m_colorSpaceGamma->setEnabled(m_regExp->text().contains(".exr"));
}

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

void PreferencesPopup::FormatProperties::setLevelFormat(
    const Preferences::LevelFormat& lf) {
  const LevelOptions& lo = lf.m_options;

  m_name->setText(lf.m_name);
  m_regExp->setText(lf.m_pathFormat.pattern());
  m_priority->setValue(lf.m_priority);

  m_dpiPolicy->setCurrentIndex(
      lo.m_dpiPolicy == LevelOptions::DP_ImageDpi ? DP_ImageDpi : DP_CustomDpi);
  m_dpi->setValue(lo.m_dpi);
  m_premultiply->setChecked(lo.m_premultiply);
  m_whiteTransp->setChecked(lo.m_whiteTransp);
  m_doAntialias->setChecked(lo.m_antialias > 0);
  m_antialias->setValue(lo.m_antialias);
  m_subsampling->setValue(lo.m_subsampling);
  m_colorSpaceGamma->setValue(lo.m_colorSpaceGamma);

  updateEnabledStatus();
}

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

Preferences::LevelFormat PreferencesPopup::FormatProperties::levelFormat()
    const {
  Preferences::LevelFormat lf(m_name->text());

  // Assign key values
  lf.m_pathFormat.setPattern(m_regExp->text());
  lf.m_priority = m_priority->getValue();

  // Assign level format values
  lf.m_options.m_dpiPolicy   = (m_dpiPolicy->currentIndex() == DP_ImageDpi)
                                   ? LevelOptions::DP_ImageDpi
                                   : LevelOptions::DP_CustomDpi;
  lf.m_options.m_dpi         = m_dpi->getValue();
  lf.m_options.m_subsampling = m_subsampling->getValue();
  lf.m_options.m_antialias =
      m_doAntialias->isChecked() ? m_antialias->getValue() : 0;
  lf.m_options.m_premultiply = m_premultiply->isChecked();
  lf.m_options.m_whiteTransp = m_whiteTransp->isChecked();

  if (m_colorSpaceGamma->isEnabled())
    lf.m_options.m_colorSpaceGamma = m_colorSpaceGamma->getValue();

  return lf;
}

//**********************************************************************************
//    PreferencesPopup::AdditionalStyleEdit  implementation
//**********************************************************************************

PreferencesPopup::AdditionalStyleEdit::AdditionalStyleEdit(
    PreferencesPopup* parent)
    : DVGui::Dialog(parent, true, false, "AdditionalStyleEdit") {
  setWindowTitle(tr("Additional Style Sheet"));
  setModal(true);

  m_edit                   = new QTextEdit(this);
  QPushButton* okButton    = new QPushButton(tr("OK"), this);
  QPushButton* applyButton = new QPushButton(tr("Apply"), this);
  QPushButton* closeButton = new QPushButton(tr("Close"), this);

  QString placeHolderTxt(
      "/* Type additional style sheet here to customize GUI. \n"
      "   Example: To enlarge the Style Editor buttons */\n\n"
      "#StyleEditor #bottomWidget QPushButton{ \n  padding : 13 21; \n }");
  m_edit->setPlaceholderText(placeHolderTxt);
  m_edit->setAcceptRichText(false);

  m_topLayout->addWidget(m_edit);

  addButtonBarWidget(okButton, applyButton, closeButton);

  connect(okButton, &QPushButton::pressed, this, &AdditionalStyleEdit::onOK);
  connect(applyButton, &QPushButton::pressed, this,
          &AdditionalStyleEdit::onApply);
  connect(closeButton, &QPushButton::pressed, this,
          &AdditionalStyleEdit::close);
}

void PreferencesPopup::AdditionalStyleEdit::showEvent(QShowEvent*) {
  m_edit->setPlainText(Preferences::instance()->getAdditionalStyleSheet());
}

void PreferencesPopup::AdditionalStyleEdit::onOK() {
  onApply();
  close();
}

void PreferencesPopup::AdditionalStyleEdit::onApply() {
  Preferences::instance()->setValue(additionalStyleSheet,
                                    m_edit->toPlainText());
  emit additionalSheetEdited();
}

//**********************************************************************************
//   PreferencesPopup::Display30bitCheckerView  implementation
//**********************************************************************************

PreferencesPopup::Display30bitChecker::GLView::GLView(QWidget* parent,
                                                      bool is30bit)
    : QOpenGLWidget(parent), m_is30bit(is30bit) {
  setFixedSize(500, 100);
  if (m_is30bit) setTextureFormat(TGL_TexFmt10);
}

void PreferencesPopup::Display30bitChecker::GLView::initializeGL() {
  initializeOpenGLFunctions();
  glClear(GL_COLOR_BUFFER_BIT);
}
void PreferencesPopup::Display30bitChecker::GLView::resizeGL(int width,
                                                             int height) {
  glViewport(0, 0, width, height);
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  glOrtho(0, 1, 0, 1, -1, 1);
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
}
void PreferencesPopup::Display30bitChecker::GLView::paintGL() {
  initializeOpenGLFunctions();
  glPushMatrix();
  glBegin(GL_QUADS);
  glColor3d(0.071, 0.153, 0.0);
  glVertex3d(0, 0, 0);
  glVertex3d(0, 1, 0);
  glColor3d(0.141, 0.239, 0.0);
  glVertex3d(1, 1, 0);
  glVertex3d(1, 0, 0);
  glEnd();
  glPopMatrix();
}

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

PreferencesPopup::Display30bitChecker::Display30bitChecker(
    PreferencesPopup* parent)
    : QDialog(parent) {
  setModal(true);
  m_currentDefaultFormat = QSurfaceFormat::defaultFormat();

  setWindowTitle(tr("Check 30bit display availability"));

  QSurfaceFormat sFmt = m_currentDefaultFormat;
  sFmt.setRedBufferSize(10);
  sFmt.setGreenBufferSize(10);
  sFmt.setBlueBufferSize(10);
  sFmt.setAlphaBufferSize(2);
  QSurfaceFormat::setDefaultFormat(sFmt);

  GLView* view8bit      = new GLView(this, false);
  GLView* view10bit     = new GLView(this, true);
  QPushButton* closeBtn = new QPushButton(tr("Close"), this);
  QString infoLabel     = tr(
      "If the lower gradient looks smooth and has no banding compared to the upper gradient,\n\
30bit display is available in the current configuration.");

  QVBoxLayout* lay = new QVBoxLayout();
  lay->setContentsMargins(10, 10, 10, 10);
  lay->setSpacing(10);
  {
    lay->addWidget(view8bit);
    lay->addWidget(view10bit);
    lay->addWidget(new QLabel(infoLabel, this));
    lay->addWidget(closeBtn, 0, Qt::AlignCenter);
  }
  setLayout(lay);
  lay->setSizeConstraint(QLayout::SetFixedSize);

  connect(closeBtn, &QPushButton::clicked, this, &Display30bitChecker::accept);
}

PreferencesPopup::Display30bitChecker::~Display30bitChecker() {
  QSurfaceFormat::setDefaultFormat(m_currentDefaultFormat);
}

//**********************************************************************************
//    PreferencesPopup  implementation
//**********************************************************************************

void PreferencesPopup::rebuildFormatsList() {
  const Preferences& prefs = *Preferences::instance();

  m_levelFormatNames->clear();

  int lf, lfCount = prefs.levelFormatsCount();
  for (lf = 0; lf != lfCount; ++lf)
    m_levelFormatNames->addItem(prefs.levelFormat(lf).m_name);

  m_editLevelFormat->setEnabled(m_levelFormatNames->count() > 0);
}

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

QList<ComboBoxItem> PreferencesPopup::buildFontStyleList() const {
  TFontManager* instance = TFontManager::instance();
  std::vector<std::wstring> typefaces;
  std::vector<std::wstring>::iterator it;
  QString font  = m_pref->getStringValue(interfaceFont);
  QString style = m_pref->getStringValue(interfaceFontStyle);
  try {
    instance->loadFontNames();
    instance->setFamily(font.toStdWString());
    instance->getAllTypefaces(typefaces);
  } catch (TFontCreationError&) {
    it = typefaces.begin();
    typefaces.insert(it, style.toStdWString());
  }
  QList<ComboBoxItem> styleList;
  for (it = typefaces.begin(); it != typefaces.end(); ++it)
    styleList.append(ComboBoxItem(QString::fromStdWString(*it),
                                  QString::fromStdWString(*it)));
  return styleList;
}

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

QList<ComboBoxItem> PreferencesPopup::buildSvnUserList() const {
  PermissionsManager* instance = PermissionsManager::instance();
  QList<ComboBoxItem> userList;
  std::string username;
  username = instance->getSVNUserName(0);
  for (int i = 1; !username.empty(); i++) {
    userList.append(ComboBoxItem(QString::fromStdString(username),
                                 QString::fromStdString(username)));
    username = instance->getSVNUserName(i);
  }
  return userList;
}

QList<ComboBoxItem> PreferencesPopup::buildSvnRepList() const {
  VersionControl* instance = VersionControl::instance();
  QList<ComboBoxItem> repList;
  QList<SVNRepository> repositories = instance->getRepositories();
  for (int i = 0; i < repositories.size(); i++) {
    SVNRepository r = repositories.at(i);
    repList.append(ComboBoxItem(r.m_name, r.m_name));
  }
  return repList;
}

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

void PreferencesPopup::onAutoSaveChanged() {
  bool on = getUI<QGroupBox*>(autosaveEnabled)->isChecked();
  if (!on) return;
  CheckBox* autoSaveSceneCB      = getUI<CheckBox*>(autosaveSceneEnabled);
  CheckBox* autoSaveOtherFilesCB = getUI<CheckBox*>(autosaveOtherFilesEnabled);
  if (!autoSaveSceneCB->isChecked() && !autoSaveOtherFilesCB->isChecked()) {
    autoSaveSceneCB->setChecked(true);
    autoSaveOtherFilesCB->setChecked(true);
  }
}

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

void PreferencesPopup::onAutoSaveOptionsChanged() {
  bool autoSaveScene = getUI<CheckBox*>(autosaveSceneEnabled)->isChecked();
  bool autoSaveOtherFiles =
      getUI<CheckBox*>(autosaveOtherFilesEnabled)->isChecked();
  if (!autoSaveScene && !autoSaveOtherFiles) {
    getUI<QGroupBox*>(autosaveEnabled)->setChecked(false);
  }
}

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

void PreferencesPopup::onWatchFileSystemClicked() {
  // emit signal to update behavior of the File browser
  TApp::instance()->getCurrentScene()->notifyPreferenceChanged(
      "WatchFileSystem");
}

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

void PreferencesPopup::onPathAliasPriorityChanged() {
  TApp::instance()->getCurrentScene()->notifyPreferenceChanged(
      "PathAliasPriority");
}

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

void PreferencesPopup::onStyleSheetTypeChanged() {
  QApplication::setOverrideCursor(Qt::WaitCursor);
  QString currentStyle = m_pref->getCurrentStyleSheet();
  qApp->setStyleSheet(currentStyle);
  QApplication::restoreOverrideCursor();

  // Update icons
  ThemeManager& tm = ThemeManager::getInstance();
  tm.parseCustomPropertiesFromStylesheet(currentStyle);

  // Try request a full UI repaint to update icons
  // TODO: Can be better, may not refresh all widgets like popups...
  QMainWindow* mainwindow = TApp::instance()->getMainWindow();
  if (mainwindow) {
    mainwindow->update();
  }
}

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

void PreferencesPopup::onPixelsOnlyChanged() {
  QComboBox* unitOm           = getUI<QComboBox*>(linearUnits);
  QComboBox* cameraUnitOm     = getUI<QComboBox*>(cameraUnits);
  DoubleLineEdit* defLevelDpi = getUI<DoubleLineEdit*>(DefLevelDpi);
  MeasuredDoubleLineEdit* defLevelWidth =
      getUI<MeasuredDoubleLineEdit*>(DefLevelWidth);
  MeasuredDoubleLineEdit* defLevelHeight =
      getUI<MeasuredDoubleLineEdit*>(DefLevelHeight);

  bool isPixel = m_pref->getBoolValue(pixelsOnly);
  if (isPixel) {
    m_pref->setValue(DefLevelDpi, Stage::standardDpi);
    TCamera* camera =
        TApp::instance()->getCurrentScene()->getScene()->getCurrentCamera();
    TDimension camRes = camera->getRes();
    TDimensionD camSize;
    camSize.lx = camRes.lx / Stage::standardDpi;
    camSize.ly = camRes.ly / Stage::standardDpi;
    camera->setSize(camSize);
    TDimension cleanupRes = CleanupSettingsModel::instance()
                                ->getCurrentParameters()
                                ->m_camera.getRes();
    TDimensionD cleanupSize;
    cleanupSize.lx = cleanupRes.lx / Stage::standardDpi;
    cleanupSize.ly = cleanupRes.ly / Stage::standardDpi;
    CleanupSettingsModel::instance()->getCurrentParameters()->m_camera.setSize(
        cleanupSize);

    m_pref->storeOldUnits();

    if (unitOm->currentText() != tr("pixel"))
      unitOm->setCurrentText(tr("pixel"));
    if (cameraUnitOm->currentText() != tr("pixel"))
      cameraUnitOm->setCurrentText(tr("pixel"));
    unitOm->setDisabled(true);
    cameraUnitOm->setDisabled(true);
    defLevelDpi->setDisabled(true);
    defLevelDpi->setValue(Stage::standardDpi);
    defLevelWidth->setMeasure("camera.lx");
    defLevelHeight->setMeasure("camera.ly");
    defLevelWidth->setValue(m_pref->getDoubleValue(DefLevelWidth));
    defLevelHeight->setValue(m_pref->getDoubleValue(DefLevelHeight));
    defLevelHeight->setDecimals(0);
    defLevelWidth->setDecimals(0);
  } else {
    QString tempUnit = m_pref->getStringValue(oldUnits);
    unitOm->setCurrentIndex(unitOm->findData(tempUnit));
    tempUnit = m_pref->getStringValue(oldCameraUnits);
    cameraUnitOm->setCurrentIndex(cameraUnitOm->findData(tempUnit));
    unitOm->setDisabled(false);
    cameraUnitOm->setDisabled(false);
    bool isRaster = m_pref->getIntValue(DefLevelType) != PLI_XSHLEVEL;
    if (isRaster) {
      defLevelDpi->setDisabled(false);
    }
    defLevelHeight->setMeasure("level.ly");
    defLevelWidth->setMeasure("level.lx");
    defLevelWidth->setValue(m_pref->getDoubleValue(DefLevelWidth));
    defLevelHeight->setValue(m_pref->getDoubleValue(DefLevelHeight));
    defLevelHeight->setDecimals(4);
    defLevelWidth->setDecimals(4);
  }
  TApp::instance()->getCurrentScene()->notifyPreferenceChanged("pixelsOnly");
}

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

void PreferencesPopup::beforeUnitChanged() { m_pref->storeOldUnits(); }

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

void PreferencesPopup::onUnitChanged() {
  CheckBox* pixelsOnlyCB = getUI<CheckBox*>(pixelsOnly);
  if (!pixelsOnlyCB->isChecked() &&
      (m_pref->getStringValue(linearUnits) == "pixel" ||
       m_pref->getStringValue(cameraUnits) == "pixel")) {
    pixelsOnlyCB->setCheckState(Qt::Checked);
  }
}

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

void PreferencesPopup::beforeRoomChoiceChanged() {
  TApp::instance()->writeSettings();
}

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

void PreferencesPopup::onColorCalibrationChanged() {
  LutManager::instance()->update();
  TApp::instance()->getCurrentScene()->notifyPreferenceChanged(
      "ColorCalibration");
}

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

void PreferencesPopup::onDefLevelTypeChanged() {
  bool isRaster = m_pref->getIntValue(DefLevelType) != PLI_XSHLEVEL &&
                  !m_pref->getBoolValue(newLevelSizeToCameraSizeEnabled);
  m_controlIdMap.key(DefLevelWidth)->setEnabled(isRaster);
  m_controlIdMap.key(DefLevelHeight)->setEnabled(isRaster);
  if (!m_pref->getBoolValue(pixelsOnly))
    m_controlIdMap.key(DefLevelDpi)->setEnabled(isRaster);
}

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

void PreferencesPopup::onUseNumpadForSwitchingStylesClicked() {
  bool checked = m_pref->getBoolValue(useNumpadForSwitchingStyles);
  if (checked) {
    // check if there are any commands with numpadkey shortcuts
    CommandManager* cm = CommandManager::instance();
    QList<QAction*> actionsList;
    for (int key = Qt::Key_0; key <= Qt::Key_9; key++) {
      std::string str = QKeySequence(key).toString().toStdString();
      QAction* action = cm->getActionFromShortcut(str);
      if (action) actionsList.append(action);
    }
    QAction* tabAction = cm->getActionFromShortcut("Tab");
    if (tabAction) actionsList.append(tabAction);
    tabAction = cm->getActionFromShortcut("Shift+Tab");
    if (tabAction) actionsList.append(tabAction);
    // if there are actions using numpad shortcuts, notify to release them.
    if (!actionsList.isEmpty()) {
      QString msgStr =
          tr("Numpad keys are assigned to the following commands.\nIs it OK to "
             "release these shortcuts?");
      for (int a = 0; a < actionsList.size(); a++) {
        msgStr += "\n" + actionsList.at(a)->iconText() + "  (" +
                  actionsList.at(a)->shortcut().toString() + ")";
      }
      int ret = DVGui::MsgBox(msgStr, tr("OK"), tr("Cancel"), 1);
      if (ret == 2 || ret == 0) {  // canceled
        getUI<CheckBox*>(useNumpadForSwitchingStyles)->setChecked(false);
        return;
      } else {  // accepted, then release shortcuts
        for (int a = 0; a < actionsList.size(); a++)
          cm->setShortcut(actionsList[a], "");
      }
    }
  }
  // emit signal to update Palette and Viewer
  TApp::instance()->getCurrentScene()->notifyPreferenceChanged(
      "NumpadForSwitchingStyles");
}

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

void PreferencesPopup::onLevelBasedToolsDisplayChanged() {
  TApp::instance()->getCurrentScene()->notifyPreferenceChanged(
      "ToolbarDisplay");
}

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

void PreferencesPopup::onShowKeyframesOnCellAreaChanged() {
  TApp::instance()->getCurrentScene()->notifyPreferenceChanged("XsheetCamera");
}

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

void PreferencesPopup::onShowXSheetToolbarClicked() {
  TApp::instance()->getCurrentScene()->notifyPreferenceChanged("XSheetToolbar");
}

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

void PreferencesPopup::onUnifyColumnVisibilityTogglesChanged() {
  // Check if any column has visibility toggles with different states and the
  // "unify visibility toggles" option is enabled
  if (Preferences::instance()->isUnifyColumnVisibilityTogglesEnabled())
    ColumnCmd::unifyColumnVisibilityToggles();

  TApp::instance()->getCurrentScene()->notifyPreferenceChanged(
      "unifyColumnVisibilityToggles");
}

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

void PreferencesPopup::onShowXsheetBreadcrumbsClicked() {
  TApp::instance()->getCurrentScene()->notifyPreferenceChanged(
      "XsheetBreadcrumbs");
}

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

void PreferencesPopup::onModifyExpressionOnMovingReferencesChanged() {
  TApp::instance()->getCurrentScene()->notifyPreferenceChanged(
      "modifyExpressionOnMovingReferences");
}

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

void PreferencesPopup::onBlankCountChanged() {
  TApp::instance()->getCurrentScene()->notifyPreferenceChanged("BlankCount");
}

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

void PreferencesPopup::onBlankColorChanged() {
  TApp::instance()->getCurrentScene()->notifyPreferenceChanged("BlankColor");
}

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

void PreferencesPopup::onOnionSkinVisibilityChanged() {
  bool onionActive = m_pref->getBoolValue(onionSkinEnabled);
  m_controlIdMap.key(onionPaperThickness)->setEnabled(onionActive);
  m_controlIdMap.key(backOnionColor)->setEnabled(onionActive);
  m_controlIdMap.key(frontOnionColor)->setEnabled(onionActive);
  m_controlIdMap.key(onionInksOnly)->setEnabled(onionActive);

  OnionSkinMask osm =
      TApp::instance()->getCurrentOnionSkin()->getOnionSkinMask();
  osm.enable(onionActive);
  TApp::instance()->getCurrentOnionSkin()->setOnionSkinMask(osm);
  TApp::instance()->getCurrentOnionSkin()->notifyOnionSkinMaskChanged();
}

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

void PreferencesPopup::onOnionColorChanged() {
  TApp::instance()->getCurrentScene()->notifySceneChanged();
  TApp::instance()->getCurrentLevel()->notifyLevelViewChange();
  TApp::instance()->getCurrentOnionSkin()->notifyOnionSkinMaskChanged();
}

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

void invalidateIcons();  // TODO: Find the appropriate header for this
                         // declaration

void PreferencesPopup::onTranspCheckDataChanged() { invalidateIcons(); }

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

void PreferencesPopup::onChessboardChanged() {
  CommonChessboard::instance()->update();
}

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

void PreferencesPopup::onSVNEnabledChanged() {
  if (m_pref->getBoolValue(SVNEnabled)) {
    if (!VersionControl::instance()->testSetup())
      getUI<CheckBox*>(SVNEnabled)->setChecked(false);
  }
}

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

void PreferencesPopup::notifySceneChanged() {
  TApp::instance()->getCurrentScene()->notifySceneChanged();
}

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

void PreferencesPopup::onAutoSaveExternallyChanged() {
  QGroupBox* autoSaveGroup = getUI<QGroupBox*>(autosaveEnabled);
  autoSaveGroup->setChecked(m_pref->getBoolValue(autosaveEnabled));
}

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

void PreferencesPopup::onAutoSavePeriodExternallyChanged() {
  IntLineEdit* minuteFld = getUI<IntLineEdit*>(autosavePeriod);
  minuteFld->setValue(m_pref->getIntValue(autosavePeriod));
}

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

void PreferencesPopup::onProjectRootChanged() {
  int index = 0;
  // if (m_projectRootStuff->isChecked())
  index |= 0x08;
  if (m_projectRootDocuments->isChecked()) index |= 0x04;
  if (m_projectRootDesktop->isChecked()) index |= 0x02;
  if (m_projectRootCustom->isChecked()) index |= 0x01;
  m_pref->setValue(projectRoot, index);
}
//-----------------------------------------------------------------------------

void PreferencesPopup::onEditAdditionalStyleSheet() {
  if (!m_additionalStyleEdit) {
    m_additionalStyleEdit = new AdditionalStyleEdit(this);

    connect(m_additionalStyleEdit, &AdditionalStyleEdit::additionalSheetEdited,
            this, &PreferencesPopup::onAdditionalStyleSheetEdited);
  }
  m_additionalStyleEdit->show();
}

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

void PreferencesPopup::onAdditionalStyleSheetEdited() {
  onStyleSheetTypeChanged();
}

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

void PreferencesPopup::onPixelUnitExternallySelected(bool on) {
  CheckBox* pixelsOnlyCB = getUI<CheckBox*>(pixelsOnly);
  // call slot function onPixelsOnlyChanged() accordingly
  pixelsOnlyCB->setCheckState((on) ? Qt::Checked : Qt::Unchecked);
}

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

void PreferencesPopup::onInterfaceFontChanged(const QString& text) {
  m_pref->setValue(interfaceFont, text);

  // rebuild font styles
  QComboBox* fontStyleCombo         = getUI<QComboBox*>(interfaceFontStyle);
  QString oldTypeface               = fontStyleCombo->currentText();
  QList<ComboBoxItem> newStyleItems = buildFontStyleList();
  fontStyleCombo->clear();
  for (ComboBoxItem& item : newStyleItems)
    fontStyleCombo->addItem(item.first, item.second);
  if (!oldTypeface.isEmpty()) {
    int newIndex = fontStyleCombo->findText(oldTypeface);
    if (newIndex < 0) newIndex = 0;
    fontStyleCombo->setCurrentIndex(newIndex);
  }

  if (text.contains("Comic Sans"))
    DVGui::warning(tr("Life is too short for Comic Sans"));
  if (text.contains("Wingdings"))
    DVGui::warning(tr("Good luck.  You're on your own from here."));
}

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

void PreferencesPopup::onLutPathChanged() {
  FileField* lutPathFileField = getUI<FileField*>(colorCalibrationLutPaths);
  m_pref->setColorCalibrationLutPath(LutManager::instance()->getMonitorName(),
                                     lutPathFileField->getPath());
  onColorCalibrationChanged();
}

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

void PreferencesPopup::onCheck30bitDisplay() {
  Display30bitChecker checker(this);
  checker.exec();
}

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

void PreferencesPopup::onAddLevelFormat() {
  bool ok            = true;
  QString formatName = DVGui::getText(tr("New Level Format"),
                                      tr("Assign the new level format name:"),
                                      tr("New Format"), &ok);

  if (ok) {
    int formatIdx = Preferences::instance()->addLevelFormat(formatName);
    rebuildFormatsList();

    m_levelFormatNames->setCurrentIndex(formatIdx);
  }
}

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

void PreferencesPopup::onRemoveLevelFormat() {
  Preferences::instance()->removeLevelFormat(
      m_levelFormatNames->currentIndex());
  rebuildFormatsList();
}

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

void PreferencesPopup::onEditLevelFormat() {
  if (!m_formatProperties) {
    m_formatProperties = new FormatProperties(this);

    connect(m_formatProperties, &FormatProperties::dialogClosed, this,
            &PreferencesPopup::onLevelFormatEdited);
  }

  const Preferences::LevelFormat& lf =
      Preferences::instance()->levelFormat(m_levelFormatNames->currentIndex());

  m_formatProperties->setLevelFormat(lf);
  m_formatProperties->show();
}

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

void PreferencesPopup::onLevelFormatEdited() {
  assert(m_formatProperties);

  Preferences& prefs = *Preferences::instance();
  int formatIdx      = m_levelFormatNames->currentIndex();

  prefs.removeLevelFormat(formatIdx);
  formatIdx = prefs.addLevelFormat(m_formatProperties->levelFormat());

  rebuildFormatsList();

  m_levelFormatNames->setCurrentIndex(formatIdx);
}

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

void PreferencesPopup::onImportPolicyExternallyChanged(int policy) {
  QComboBox* importPolicyCombo = getUI<QComboBox*>(importPolicy);
  // update preferences data accordingly
  importPolicyCombo->setCurrentIndex(policy);
}

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

void PreferencesPopup::onRenamePolicyExternallyChanged(int policy) {
  QComboBox* renamePolicyCombo = getUI<QComboBox*>(renamePolicy);
  // update preferences data accordingly
  renamePolicyCombo->setCurrentIndex(policy);
}
//-----------------------------------------------------------------------------

void PreferencesPopup::onConvertPolicyExternallyChanged(int policy) {
  QComboBox* convertPolicyCombo = getUI<QComboBox*>(importPolicy);
  // update preferences data accordingly
  convertPolicyCombo->setCurrentIndex(policy);
}

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

QWidget* PreferencesPopup::createUI(PreferencesItemId id,
                                    const QList<ComboBoxItem>& comboItems) {
  PreferencesItem item = m_pref->getItem(id);
  // create widget depends on the parameter types
  QWidget* widget = nullptr;
  switch (item.type) {
  case QMetaType::Bool:  // create CheckBox
  {
    CheckBox* cb = new CheckBox(getUIString(id), this);
    cb->setChecked(item.value.toBool());
    connect(cb, &CheckBox::stateChanged, this, &PreferencesPopup::onChange);
    widget = cb;
  } break;

  case QMetaType::Int:            // create either QComboBox or IntLineEdit
    if (!comboItems.isEmpty()) {  // create QComboBox
      QComboBox* combo = new QComboBox(this);
      for (const ComboBoxItem& item : comboItems)
        combo->addItem(item.first, item.second);
      combo->setCurrentIndex(combo->findData(item.value));
      connect(combo, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
              &PreferencesPopup::onChange);
      widget = combo;
    } else {  // create IntLineEdit
      DVGui::IntLineEdit* field = new DVGui::IntLineEdit(
          this, item.value.toInt(), item.min.toInt(), item.max.toInt());
      connect(field, &DVGui::IntLineEdit::editingFinished, this,
              &PreferencesPopup::onChange);
      widget = field;
    }
    break;

  case QMetaType::Double:     // create either MeasuredDoubleLineEdit or
                              // DoubleLineEdit
    if (id == DefLevelDpi) {  // currently DoubleLineEdit is only used in the
                              // dpi field
      DoubleLineEdit* field = new DoubleLineEdit(this, item.value.toDouble());
      field->setRange(item.min.toDouble(), item.max.toDouble());
      connect(field, &DoubleLineEdit::valueChanged, this,
              &PreferencesPopup::onChange);
      widget = field;
    } else {
      MeasuredDoubleLineEdit* field = new MeasuredDoubleLineEdit(this);
      field->setRange(item.min.toDouble(), item.max.toDouble());
      if (m_pref->getStringValue(linearUnits) == "pixel")
        field->setMeasure((id == DefLevelWidth) ? "camera.lx" : "camera.ly");
      else
        field->setMeasure((id == DefLevelWidth) ? "level.lx" : "level.ly");
      field->setValue(item.value.toDouble());
      connect(field, &MeasuredDoubleLineEdit::valueChanged, this,
              &PreferencesPopup::onChange);
      widget = field;
    }
    break;

  case QMetaType::QString:      // create QFontComboBox, QComboBox or FileField
    if (id == interfaceFont) {  // create QFontComboBox
      QFontComboBox* combo = new QFontComboBox(this);
      combo->setCurrentText(item.value.toString());
      // QFontComboBox uses currentFontChanged
      connect(
          combo, &QFontComboBox::currentFontChanged, this,
          [this](const QFont& font) { onInterfaceFontChanged(font.family()); });
      widget = combo;
    } else if (!comboItems.isEmpty()) {  // create QComboBox
      QComboBox* combo = new QComboBox(this);
      for (const ComboBoxItem& item : comboItems)
        combo->addItem(item.first, item.second);
      combo->setCurrentIndex(combo->findData(item.value));
      connect(combo, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
              &PreferencesPopup::onChange);
      widget = combo;
    } else {  // create FileField
      DVGui::FileField* field =
          new DVGui::FileField(this, item.value.toString());
      connect(field, &FileField::pathChanged, this,
              &PreferencesPopup::onChange);
      widget = field;
    }
    break;

  case QMetaType::QSize:  // create SizeField
  {
    SizeField* field = new SizeField(item.min.toSize(), item.max.toSize(),
                                     item.value.toSize(), this);
    connect(field, &SizeField::editingFinished, this,
            &PreferencesPopup::onChange);
    widget = field;
  } break;

  case QMetaType::QColor:  // create ColorField
  {
    ColorField* field =
        new ColorField(this, false, colorToTPixel(item.value.value<QColor>()));
    connect(field, &ColorField::colorChanged, this,
            &PreferencesPopup::onColorFieldChanged);
    widget = field;
  } break;

  case QMetaType::QVariantMap:  // used in colorCalibrationLutPaths
  {
    DVGui::FileField* field = new DVGui::FileField(
        this, QString("- Please specify 3DLUT file (.3dl) -"), false, true);
    QString lutPath = m_pref->getColorCalibrationLutPath(
        LutManager::instance()->getMonitorName());
    if (!lutPath.isEmpty()) field->setPath(lutPath);
    field->setFileMode(QFileDialog::ExistingFile);
    QStringList lutFileTypes = {"3dl"};
    field->setFilters(lutFileTypes);
    connect(field, &FileField::pathChanged, this,
            &PreferencesPopup::onLutPathChanged);
    widget = field;
  } break;

  default:
    qWarning() << "unsupported value type for preference item" << id;
    break;
  }

  if (widget) {
    m_controlIdMap[widget] = id;
  }
  return widget;
}

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

QGridLayout* PreferencesPopup::insertGroupBoxUI(PreferencesItemId id,
                                                QGridLayout* layout) {
  PreferencesItem item = m_pref->getItem(id);
  QGroupBox* box       = new QGroupBox(getUIString(id), this);
  box->setCheckable(true);
  box->setChecked(item.value.toBool());

  QGridLayout* lay = new QGridLayout();
  setupLayout(lay, 5);
  box->setLayout(lay);

  layout->addWidget(box, layout->rowCount(), 0, 1, 3);

  connect(box, &QGroupBox::clicked, this, &PreferencesPopup::onChange);
  m_controlIdMap[box] = id;
  return lay;
}

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

void PreferencesPopup::insertUI(PreferencesItemId id, QGridLayout* layout,
                                const QList<ComboBoxItem>& comboItems) {
  PreferencesItem item = m_pref->getItem(id);

  QWidget* widget = createUI(id, comboItems);
  if (!widget) return;

  bool isFileField = false;
  if (item.type == QMetaType::QVariantMap ||
      (item.type == QMetaType::QString && dynamic_cast<FileField*>(widget)))
    isFileField = true;

  // CheckBox contains label in itself
  if (item.type == QMetaType::Bool)
    layout->addWidget(widget, layout->rowCount(), 0, 1, 3, Qt::AlignLeft);
  else {  // insert labels for other types
    int row = layout->rowCount();
    layout->addWidget(new QLabel(getUIString(id), this), row, 0,
                      Qt::AlignRight | Qt::AlignVCenter);
    if (isFileField)
      layout->addWidget(widget, row, 1, 1, 2);
    else {
      bool isWideComboBox = false;
      for (auto cbItem : comboItems) {
        if (widget->fontMetrics().horizontalAdvance(cbItem.first) > 100) {
          isWideComboBox = true;
          break;
        }
      }
      if (id == interfaceFont) isWideComboBox = true;

      layout->addWidget(widget, row, 1, 1, (isWideComboBox) ? 2 : 1,
                        Qt::AlignLeft | Qt::AlignVCenter);
    }
  }
}

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

void PreferencesPopup::insertDualUIs(
    PreferencesItemId leftId, PreferencesItemId rightId, QGridLayout* layout,
    const QList<ComboBoxItem>& leftComboItems,
    const QList<ComboBoxItem>& rightComboItems) {
  // currently this function does not suppose that the checkbox is on the left
  assert(m_pref->getItem(leftId).type != QMetaType::Bool);
  int row = layout->rowCount();
  layout->addWidget(new QLabel(getUIString(leftId), this), row, 0,
                    Qt::AlignRight | Qt::AlignVCenter);
  QHBoxLayout* innerLay = new QHBoxLayout();
  innerLay->setContentsMargins(0, 0, 0, 0);
  innerLay->setSpacing(10);
  {
    innerLay->addWidget(createUI(leftId, leftComboItems), 0);
    if (m_pref->getItem(rightId).type != QMetaType::Bool)
      innerLay->addWidget(new QLabel(getUIString(rightId), this), 0,
                          Qt::AlignRight | Qt::AlignVCenter);
    innerLay->addWidget(createUI(rightId, rightComboItems), 0);
    innerLay->addStretch(1);
  }
  layout->addLayout(innerLay, row, 1, 1, 2);
}

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

void PreferencesPopup::insertFootNote(QGridLayout* layout) {
  QLabel* note = new QLabel(
      tr("* Changes will take effect the next time you run OpenToonz"));
  note->setStyleSheet("font-size: 10px; font: italic;");
  layout->addWidget(note, layout->rowCount(), 0, 1, 3,
                    Qt::AlignLeft | Qt::AlignVCenter);
}

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

QString PreferencesPopup::getUIString(PreferencesItemId id) {
  auto CtrlAltStr = []() {
    QString str =
        QKeySequence(Qt::CTRL + Qt::ALT).toString(QKeySequence::NativeText);
    if (str.endsWith("+")) str.chop(1);
    return str;
  };

  const static QMap<PreferencesItemId, QString> uiStringTable = {
      // General
      {rasterOptimizedMemory, tr("Minimize Raster Memory Fragmentation*")},
      {autosaveEnabled, tr("Save Automatically")},
      {autosavePeriod, tr("Interval (Minutes):")},
      {autosaveSceneEnabled, tr("Automatically Save the Scene File")},
      {autosaveOtherFilesEnabled, tr("Automatically Save Non-Scene Files")},
      {startupPopupEnabled, tr("Show Startup Window when OpenToonz Starts")},
      {undoMemorySize, tr("Undo Memory Size (MB):")},
      {taskchunksize, tr("Render Task Chunk Size:")},
      {replaceAfterSaveLevelAs,
       tr("Replace Toonz Level after SaveLevelAs command")},
      {backupEnabled, tr("Backup Scene and Animation Levels when Saving")},
      {backupKeepCount, tr("# of backups to keep:")},
      {sceneNumberingEnabled, tr("Add Info water mark in Rendered Frames")},
      {watchFileSystemEnabled,
       tr("Watch File System and Update File Browser Automatically")},
      //{ projectRoot,               tr("") },
      {customProjectRoot, tr("Custom Project Path(s):")},
      {pathAliasPriority, tr("Path Alias Priority:")},
      {lazyLoadRooms, tr("Lazy Load Rooms")},

      // Interface
      {CurrentStyleSheetName, tr("Theme:")},
      {pixelsOnly, tr("All imported images will use the same DPI")},
      //{ oldUnits,                               tr("") },
      //{ oldCameraUnits,                         tr("") },
      {linearUnits, tr("Unit:")},
      {cameraUnits, tr("Camera Unit:")},
      {CurrentRoomChoice, tr("Rooms*:")},
      {functionEditorToggle, tr("Function Editor*:")},
      {moveCurrentFrameByClickCellArea,
       tr("Move Current Frame by Clicking on Xsheet / Numerical Columns Cell "
          "Area")},
      {actualPixelViewOnSceneEditingMode,
       tr("Enable Actual Pixel View on Scene Editing Mode")},
      {showRasterImagesDarkenBlendedInViewer,
       tr("Show Raster Images Darken Blended")},
      {iconSize, tr("Level Strip Thumbnail Size*:")},
      {viewShrink, tr("Viewer Shrink:")},
      {viewStep, tr("Step:")},
      {viewerZoomCenter, tr("Zoom In/Out Center:")},
      {CurrentLanguageName, tr("Language*:")},
      {interfaceFont, tr("Font*:")},
      {interfaceFontStyle, tr("Style*:")},
      {colorCalibrationEnabled, tr("Color Calibration using 3D Look-up Table")},
      {colorCalibrationLutPaths,
       tr("3DLUT File for [%1]:")
           .arg(LutManager::instance()->getMonitorName())},
      {displayIn30bit, tr("30bit Display*")},
      {showIconsInMenu, tr("Show Icons In Menu*")},
      {showRoomBindButtons, tr("Show Room Bind Buttons*")},
      {viewerIndicatorEnabled, tr("Show Viewer Indicators")},

      // Visualization
      {show0ThickLines, tr("Show Lines with Thickness 0")},
      {regionAntialias, tr("Antialiased Region Boundaries")},
      {rasterizeAntialias, tr("Rasterize Vector with Anti Aliasing")},

      // Loading
      {importPolicy, tr("Default File Import Behavior:")},
      {renamePolicy, tr("Normalize Imported Image Sequences:")},
      {convertPolicy, tr("Convert Imported NAA Image Sequences to TLV:")},
      {autoExposeEnabled, tr("Expose Loaded Levels in Xsheet")},
      {autoRemoveUnusedLevels,
       tr("Automatically Remove Unused Levels From Scene Cast")},
      {subsceneFolderEnabled,
       tr("Create Sub-folder when Importing Sub-Xsheet")},
      {removeSceneNumberFromLoadedLevelName,
       tr("Automatically Remove Scene Number from Loaded Level Name")},
      {IgnoreImageDpi, tr("Use Camera DPI for All Imported Images")},
      {rasterLevelCachingBehavior, tr("Raster Level Caching Behavior:")},
      {columnIconLoadingPolicy, tr("Column Icon:")},
      //{ levelFormats,                           tr("") },

      // Saving
      {rasterBackgroundColor, tr("Matte color:")},
      {resetUndoOnSavingLevel, tr("Clear Undo History when Saving Levels")},

      // Import / Export
      {ffmpegPath, tr("FFmpeg Path:")},
      {ffmpegTimeout, tr("FFmpeg Timeout:")},
      {fastRenderPath, tr("Fast Render Path:")},
      {ffmpegMultiThread,
       tr("Allow Multi-Thread in FFMPEG Rendering (UNSTABLE)")},
      {quickTimeBackend,
       tr("Use QuickTime to decode/code .mov and .3gp (If Installed)")},
      {rhubarbPath, tr("Rhubarb Path:")},
      {rhubarbTimeout, tr("Rhubarb Timeout:")},

      // Drawing
      {DefRasterFormat, tr("Default Raster / Scan Level Format:")},
      //{scanLevelType, tr("Scan File Format:")},
      {DefLevelType, tr("Default Level Type:")},
      {newLevelSizeToCameraSizeEnabled,
       tr("New Levels Default to the Current Camera Size")},
      {DefLevelWidth, tr("Width:")},
      {DefLevelHeight, tr("Height:")},
      {DefLevelDpi, tr("DPI:")},
      {EnableAutocreation, tr("Enable Autocreation")},
      {NumberingSystem, tr("Numbering System:")},
      {EnableAutoStretch, tr("Enable Auto-stretch Frame")},
      {EnableCreationInHoldCells, tr("Enable Creation in Hold Cells")},
      {EnableAutoRenumber, tr("Enable Autorenumber")},
      {vectorSnappingTarget, tr("Vector Snapping:")},
      {saveUnpaintedInCleanup,
       tr("Keep Original Cleaned Up Drawings As Backup")},
      {minimizeSaveboxAfterEditing,
       tr("Minimize Savebox after Editing (Toonz Raster Level)")},
      {useNumpadForSwitchingStyles,
       tr("Use Numpad and Tab keys for Switching Styles")},
      {downArrowInLevelStripCreatesNewFrame,
       tr("Down Arrow at End of Level Strip Creates a New Frame")},
      {keepFillOnVectorSimplify,
       tr("Keep fill when using \"Replace Vectors\" command")},
      {useHigherDpiOnVectorSimplify,
       tr("Use higher DPI for calculations - Slower but more accurate")},

      // Tools
      // {dropdownShortcutsCycleOptions, tr("Dropdown Shortcuts:")}, //
      // removed
      {FillOnlysavebox, tr("Use the TLV Savebox to Limit Filling Operations")},
      {DefRegionWithPaint,
       tr("Define Filling Region Using both Lines and Areas")},
      {ReferFillPrevailing, tr("Paint Under Lines in Refer Fill")},
      {multiLayerStylePickerEnabled,
       tr("Style Picker: Switch Current Level by Picking on Multi Layer")},
      {cursorBrushType, tr("Basic Cursor Type:")},
      {cursorBrushStyle, tr("Cursor Style:")},
      {cursorOutlineEnabled, tr("Show Cursor Size Outlines")},
      {levelBasedToolsDisplay, tr("Toolbar Display Behaviour:")},
      {useCtrlAltToResizeBrush,
       tr("Brush Tool: Use %1 to Resize").arg(CtrlAltStr())},
      {useStrokeEndCursor, tr("Draw Cursor at End of Stroke")},
      {clickTwiceToCreateArcs,
       tr("Geometric Tool: Click Twice to Create Arcs")},
      {tempToolSwitchTimer,
       tr("Switch Tool Temporarily Keypress Length (ms):")},
      {animateToolHandleSize, tr("Handle Size (%):")},
      {animateToolColor, tr("Handle Color:")},

      // Xsheet
      {xsheetLayoutPreference, tr("Column Header Layout*:")},
      {xsheetStep, tr("Next/Previous Step Frames:")},
      {xsheetAutopanEnabled, tr("Xsheet Autopan during Playback")},
      {alwaysDragFrameCell, tr("Always Drag Frame Cell")},
      {DragCellsBehaviour, tr("Cell-dragging Behaviour:")},
      {deleteCommandBehavior, tr("Delete Command Behaviour:")},
      {pasteCellsBehavior, tr("Paste Cells Behaviour:")},
      {ignoreAlphaonColumn1Enabled,
       tr("Ignore Alpha Channel on Levels in Column 1")},
      {showKeyframesOnXsheetCellArea, tr("Show Keyframes on Cell Area")},
      {showXsheetCameraColumn, tr("Show Camera Column")},
      {useArrowKeyToShiftCellSelection,
       tr("Use Arrow Key to Shift Cell Selection")},
      {cellInputMethod, tr("Cell Input Method:")},
      {shortcutCommandsWhileRenamingCellEnabled,
       tr("Enable OpenToonz Commands' Shortcut Keys While Renaming Cell")},
      {showXSheetToolbar, tr("Show Toolbar in the Xsheet")},
      {showXsheetBreadcrumbs, tr("Show Sub-Xsheet Navigation Bar")},
      {expandFunctionHeader,
       tr("Expand Function Editor Header to Match Xsheet Header Height*")},
      {showColumnNumbers, tr("Show Column Numbers in Column Headers")},
      {unifyColumnVisibilityToggles,
       tr("Unify Preview and Camstand Visibility Toggles")},
      {parentColorsInXsheetColumn,
       tr("Show Column Parent's Color in the Xsheet")},
      {highlightLineEverySecond, tr("Highlight Line Every Second")},
      {syncLevelRenumberWithXsheet,
       tr("Sync Level Strip Drawing Number Changes with the Xsheet")},
      {currentTimelineEnabled, tr("Show Current Time Indicator")},
      {currentColumnColor, tr("Current Column Color:")},
      //{ levelNameOnEachMarkerEnabled, tr("Display Level Name on Each
      // Marker")
      //},
      {levelNameDisplayType, tr("Level Name Display:")},
      {showFrameNumberWithLetters,
       tr("Show \"ABC\" Appendix to the Frame Number in Xsheet Cell")},
      {linkColumnNameWithLevel, tr("Link Column Name with Level")},

      // Animation
      {keyframeType, tr("Default Interpolation:")},
      {animationStep, tr("Animation Step:")},
      {modifyExpressionOnMovingReferences,
       tr("[Experimental Feature] ") + tr("Automatically Modify Expression "
                                          "On Moving Referenced Objects")},

      // Preview
      {defaultViewerEnabled,
       tr("Preview Movie Formats in Default System Viewer")},
      {blanksCount, tr("Blank Frames:")},
      {blankColor, tr("Blank Frames Color:")},
      {rewindAfterPlayback, tr("Rewind after Playback")},
      {shortPlayFrameCount,
       tr("Number of Frames to Play \nfor Short Play Command:")},
      {generatedMovieViewEnabled, tr("Open Flipbook after Rendering")},
      {previewAlwaysOpenNewFlip, tr("Always Open New Flipbook Window ")},
      {fitToFlipbookWhenPreview,
       tr("Fit to Flipbook when Flipbook Window Open")},

      // Onion Skin
      {onionSkinEnabled, tr("Onion Skin ON")},
      {onionPaperThickness, tr("Paper Thickness:")},
      {backOnionColor, tr("Previous Frames Correction:")},
      {frontOnionColor, tr("Following Frames Correction:")},
      {onionInksOnly, tr("Display Lines Only")},
      {onionSkinDuringPlayback, tr("Show Onion Skin During Playback")},
      {useOnionColorsForShiftAndTraceGhosts,
       tr("Use Onion Skin Colors for Reference Drawings of Shift and Trace")},
      {animatedGuidedDrawing, tr("Vector Guided Style:")},

      // Colors
      {viewerBGColor, tr("Viewer BG Color:")},
      {previewBGColor, tr("Preview BG Color:")},
      {levelEditorBoxColor, tr("Level Editor Box Color:")},
      {chessboardColor1, tr("Chessboard Color 1:")},
      {chessboardColor2, tr("Chessboard Color 2:")},
      {transpCheckInkOnWhite, tr("Ink Color on White BG:")},
      {transpCheckInkOnBlack, tr("Ink Color on Black BG:")},
      {transpCheckPaint, tr("Paint Color:")},
      {inkCheckColor, tr("Ink Check Color:")},
      {ink1CheckColor, tr("Ink#1 Check Color:")},
      {paintCheckColor, tr("Paint Check Color:")},

      // Version Control
      {SVNEnabled, tr("Enable Version Control*")},
      {automaticSVNFolderRefreshEnabled,
       tr("Automatically Refresh Folder Contents")},
      {latestVersionCheckEnabled,
       tr("Check for the Latest Version of OpenToonz on Launch")},

      // Touch / Tablet Settings
      // Touch Gesture is a checkable command and not in preferences.ini
      {winInkEnabled, tr("Enable Windows Ink Support* (EXPERIMENTAL)")},
      {useQtNativeWinInk,
       tr("Use Qt's Native Windows Ink Support*\n(CAUTION: This options is "
          "for "
          "maintenance purpose. \n Do not activate this option or the tablet "
          "won't work properly.)")}};

  return uiStringTable.value(id, QString());
}

//-----------------------------------------------------------------------------
// returns list for combo items
// ComboBoxItem consists of an item label string and data to be stored in
// preferences

QList<ComboBoxItem> PreferencesPopup::getComboItemList(
    PreferencesItemId id) const {
  const static QMap<PreferencesItemId, QList<ComboBoxItem>> comboItemsTable = {
      {pathAliasPriority,
       {{tr("Project Folder Aliases (+drawings, +scenes, etc.)"),
         Preferences::ProjectFolderAliases},
        {tr("Scene Folder Alias ($scenefolder)"),
         Preferences::SceneFolderAlias},
        {tr("Use Project Folder Aliases Only"), Preferences::ProjectFolderOnly},
        {tr("Automatic by Scene"), Preferences::AutoByScene}}},
      {linearUnits,  // cameraUnits shares items with linearUnits
       {{tr("cm"), "cm"},
        {tr("mm"), "mm"},
        {tr("inch"), "inch"},
        {tr("field"), "field"},
        {tr("pixel"), "pixel"}}},
      {functionEditorToggle,
       {{tr("Graph Editor Opens in Popup"),
         Preferences::ShowGraphEditorInPopup},
        {tr("Spreadsheet Opens in Popup"),
         Preferences::ShowFunctionSpreadsheetInPopup},
        {tr("Toggle Between Graph Editor and Spreadsheet"),
         Preferences::ToggleBetweenGraphAndSpreadsheet}}},
      {viewerZoomCenter, {{tr("Mouse Cursor"), 0}, {tr("Viewer Center"), 1}}},
      {importPolicy,
       {{tr("Always ask before loading or importing"), 0},
        {tr("Always import the file to the current project"), 1},
        {tr("Always load the file from the current location"), 2}}},
      {renamePolicy,
       {{tr("Always ask before renaming"), 0},
        {tr("Normalize sequence names automatically"), 1},
        {tr("Keep original filenames"), 2}}},
      {convertPolicy,
       {{tr("Always ask before converting"), 0},
        {tr("Convert raster level automatically"), 1},
        {tr("Do not convert"), 2}}},
      {rasterLevelCachingBehavior,
       {{tr("On Demand"), 0},
        {tr("All Icons"), 1},
        {tr("All Icons & Images"), 2}}},
      {columnIconLoadingPolicy,
       {{tr("At Once"), Preferences::LoadAtOnce},
        {tr("On Demand"), Preferences::LoadOnDemand}}},
      {DefRasterFormat, {{"tif", "tif"}, {"png", "png"}}},
      //{scanLevelType, {{"tif", "tif"}, {"png", "png"}}},
      {DefLevelType,
       {{tr("Toonz Vector Level"), PLI_XSHLEVEL},
        {tr("Toonz Raster Level"), TZP_XSHLEVEL},
        {tr("Raster Level"), OVL_XSHLEVEL}}},
      {NumberingSystem,
       {{tr("Incremental"), 0}, {tr("Use Xsheet as Animation Sheet"), 1}}},
      {vectorSnappingTarget,
       {{tr("Strokes"), 0}, {tr("Guides"), 1}, {tr("All"), 2}}},
      //{dropdownShortcutsCycleOptions,
      // {{tr("Open the dropdown to display all options"), 0},
      //  {tr("Cycle through the available options"), 1}}},
      {cursorBrushType,
       {{tr("Small"), "Small"},
        {tr("Large"), "Large"},
        {tr("Crosshair"), "Crosshair"},
        {tr("Triangle Top Left"), "Triangle Top Left"},
        {tr("Triangle Top Right"), "Triangle Top Right"},
        {tr("Triangle Bottom Left"), "Triangle Bottom Left"},
        {tr("Triangle Bottom Right"), "Triangle Bottom Right"},
        {tr("Triangle Up"), "Triangle Up"},
        {tr("Triangle Down"), "Triangle Down"},
        {tr("Triangle Left"), "Triangle Left"},
        {tr("Triangle Right"), "Triangle Right"}}},
      {cursorBrushStyle,
       {{tr("Default"), "Default"},
        {tr("Left-Handed"), "Left-Handed"},
        {tr("Simple"), "Simple"}}},
      {levelBasedToolsDisplay,
       {{tr("Default"), 0},
        {tr("Enable Tools For Level Only"), 1},
        {tr("Show Tools For Level Only"), 2}}},
      {xsheetLayoutPreference,
       {{tr("Classic"), "Classic"},
        {tr("Classic-revised"), "Classic-revised"},
        {tr("Compact"), "Compact"},
        {tr("Minimum"), "Minimum"}}},
      {levelNameDisplayType,
       {{tr("Default"), Preferences::ShowLevelName_Default},
        {tr("Display on Each Marker"), Preferences::ShowLevelNameOnEachMarker},
        {tr("Display on Column Header"),
         Preferences::ShowLevelNameOnColumnHeader}}},
      {DragCellsBehaviour,
       {{tr("Cells Only"), 0},
        {tr("Cells and Column Data"), 1},
        {tr("Disable Dragging Cells"), 2}}},
      {deleteCommandBehavior,
       {{tr("Clear Cell / Frame"), 0},
        {tr("Remove and Shift Cells / Frames Up"), 1}}},
      {pasteCellsBehavior,
       {{tr("Insert Paste Whole Data"), 0},
        {tr("Overwrite Paste Cell Numbers"), 1}}},
      {cellInputMethod,
       {{tr("Input by Double Click Only"), 0},
        {tr("Input by Numpad"), 1},
        {tr("Input by Single Click"), 2}}},
      {keyframeType,  // note that the value starts from 1, not 0
       {{tr("Constant"), 1},
        {tr("Linear"), 2},
        {tr("Speed In / Speed Out"), 3},
        {tr("Ease In / Ease Out"), 4},
        {tr("Ease In / Ease Out %"), 5},
        {tr("Exponential"), 6},
        {tr("Expression "), 7},
        {tr("File"), 8}}},
      {animatedGuidedDrawing,
       {{tr("Arrow Markers"), 0}, {tr("Animated Guide"), 1}}}};

  return comboItemsTable.value(id, QList<ComboBoxItem>());
}

template <typename T>
inline T PreferencesPopup::getUI(PreferencesItemId id) {
  // Preference IDs can be missing if the UI was not fully initialized
  QWidget* widget = m_controlIdMap.key(id);
  if (!widget) {
    qWarning() << "Widget not found for preference item:" << id;
    return nullptr;
  }

  T ret = qobject_cast<T>(widget);
  if (!ret) {
    qWarning() << "Widget cast failed for preference item:" << id;
  }
  return ret;
}

//**********************************************************************************
//    PreferencesPopup's  constructor
//**********************************************************************************

PreferencesPopup::PreferencesPopup()
    : QDialog(TApp::instance()->getMainWindow())
    , m_formatProperties()
    , m_additionalStyleEdit(nullptr) {
  setWindowTitle(tr("Preferences"));
  setObjectName("PreferencesPopup");

  m_pref = Preferences::instance();

  // Category List
  QListWidget* categoryList = new QListWidget(this);
  QStringList categories;
  categories << tr("General") << tr("Interface") << tr("Preview/Render")
             << tr("Load/Import") << tr("Saving") << tr("Decoder/Encoder")
             << tr("Drawing") << tr("Tools") << tr("Xsheet") << tr("Onion Skin")
             << tr("Animation") << tr("Auto Lip-Sync") << tr("Colors")
             << tr("Vector Visualize") << tr("Version Control")
             << tr("Touch/Tablet Settings");
#ifdef _WIN32
  categories << tr("Addons");
#endif
  categoryList->addItems(categories);
  categoryList->setFixedWidth(160);
  categoryList->setCurrentRow(0);
  categoryList->setAlternatingRowColors(true);

  QStackedWidget* stackedWidget = new QStackedWidget(this);
  stackedWidget->addWidget(createGeneralPage());
  stackedWidget->addWidget(createInterfacePage());
  stackedWidget->addWidget(createPreviewPage());
  stackedWidget->addWidget(createLoadingPage());
  stackedWidget->addWidget(createSavingPage());
  stackedWidget->addWidget(createCodecPage());
  stackedWidget->addWidget(createDrawingPage());
  stackedWidget->addWidget(createToolsPage());
  stackedWidget->addWidget(createXsheetPage());
  stackedWidget->addWidget(createOnionSkinPage());
  stackedWidget->addWidget(createAnimationPage());
  stackedWidget->addWidget(createAutoLipSyncPage());
  stackedWidget->addWidget(createColorsPage());
  stackedWidget->addWidget(createVisualizationPage());
  stackedWidget->addWidget(createVersionControlPage());
  stackedWidget->addWidget(createTouchTabletPage());
#ifdef _WIN32
  stackedWidget->addWidget(createAddonsPage());
#endif  // WIN32

  QHBoxLayout* mainLayout = new QHBoxLayout();
  mainLayout->setContentsMargins(0, 0, 0, 0);
  mainLayout->setSpacing(0);
  {
    // Category
    QVBoxLayout* categoryLayout = new QVBoxLayout();
    categoryLayout->setContentsMargins(5, 5, 5, 5);
    categoryLayout->setSpacing(10);
    categoryLayout->addWidget(categoryList, 1);
    mainLayout->addLayout(categoryLayout, 0);
    mainLayout->addWidget(stackedWidget, 1);
  }
  setLayout(mainLayout);

#ifdef MACOSX
  setWindowFlags(Qt::Tool);
#endif

  connect(categoryList, &QListWidget::currentRowChanged, stackedWidget,
          &QStackedWidget::setCurrentIndex);
}

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

QWidget* PreferencesPopup::createGeneralPage() {
  m_projectRootDocuments = new CheckBox(tr("My Documents/OpenToonz*"), this);
  m_projectRootDesktop   = new CheckBox(tr("Desktop/OpenToonz*"), this);
  m_projectRootCustom    = new CheckBox(tr("Custom*"), this);
  QWidget* customField   = new QWidget(this);
  QGridLayout* customLay = new QGridLayout();
  setupLayout(customLay, 5);
  {
    insertUI(customProjectRoot, customLay);
    customLay->addWidget(
        new QLabel(
            tr("Advanced: Multiple paths can be separated by ** (No Spaces)"),
            this),
        customLay->rowCount(), 0, 1, 2, Qt::AlignLeft | Qt::AlignVCenter);
  }
  customField->setLayout(customLay);

  QWidget* widget  = new QWidget(this);
  QGridLayout* lay = new QGridLayout();
  setupLayout(lay);

  insertUI(startupPopupEnabled, lay);
  insertUI(undoMemorySize, lay);
  insertUI(rasterOptimizedMemory, lay);
  insertUI(watchFileSystemEnabled, lay);
  insertUI(lazyLoadRooms, lay);

  QGridLayout* projectRootLay =
      insertGroupBox(tr("Additional Project Locations"), lay);
  {
    projectRootLay->addWidget(m_projectRootDocuments, 0, 0, 1, 2);
    projectRootLay->addWidget(m_projectRootDesktop, 1, 0, 1, 2);
    projectRootLay->addWidget(m_projectRootCustom, 2, 0, 1, 2);
    projectRootLay->addWidget(customField, 3, 0, 1, 2);
  }

  insertUI(pathAliasPriority, lay, getComboItemList(pathAliasPriority));

  lay->setRowStretch(lay->rowCount(), 1);
  insertFootNote(lay);
  widget->setLayout(lay);

  int projectPaths = m_pref->getIntValue(projectRoot);
  m_projectRootDocuments->setChecked(projectPaths & 0x04);
  m_projectRootDesktop->setChecked(projectPaths & 0x02);
  m_projectRootCustom->setChecked(projectPaths & 0x01);
  if (!(projectPaths & 0x01)) customField->hide();

  QComboBox* pathAliasPriorityCB = getUI<QComboBox*>(pathAliasPriority);
  pathAliasPriorityCB->setToolTip(
      tr("This option defines which alias to be used\nif both are possible on "
         "coding file path."));
  pathAliasPriorityCB->setItemData(0, QString(" "), Qt::ToolTipRole);
  QString scenefolderTooltip =
      tr("Choosing this option will set initial location of all file browsers "
         "to $scenefolder.\n"
         "Also the initial output destination for new scenes will be set to "
         "$scenefolder as well.");
  pathAliasPriorityCB->setItemData(1, scenefolderTooltip, Qt::ToolTipRole);
  pathAliasPriorityCB->setItemData(2, QString(" "), Qt::ToolTipRole);
  QString autoBySceneToolTip =
      tr("Automatically sets folder based on scene type:\n"
         "Standalone -> $scenefolder\n"
         "Project -> project folder aliases (+drawing...)");
  pathAliasPriorityCB->setItemData(3, autoBySceneToolTip, Qt::ToolTipRole);

  QCheckBox* lazyLoadRoomsCheckBox = getUI<QCheckBox*>(lazyLoadRooms);
  connect(lazyLoadRoomsCheckBox, &QCheckBox::stateChanged,
          [lazyLoadRoomsCheckBox](int state) {
            QString status = Preferences::instance()->isLazyLoadRoomsEnabled()
                                 ? tr("enabled")
                                 : tr("disabled");
            QString description =
                Preferences::instance()->isLazyLoadRoomsEnabled()
                    ? tr("rooms will load on demand")
                    : tr("all rooms load at startup");
            QString lazyLoadRoomsToolTip =
                tr("Lazy loading %1 - %2").arg(status).arg(description);
            lazyLoadRoomsCheckBox->setToolTip(lazyLoadRoomsToolTip);
          });
  lazyLoadRoomsCheckBox->stateChanged(
      Preferences::instance()->isLazyLoadRoomsEnabled());

  m_onEditedFuncMap.insert(autosaveEnabled,
                           &PreferencesPopup::onAutoSaveChanged);
  m_onEditedFuncMap.insert(autosaveSceneEnabled,
                           &PreferencesPopup::onAutoSaveOptionsChanged);
  m_onEditedFuncMap.insert(autosaveOtherFilesEnabled,
                           &PreferencesPopup::onAutoSaveOptionsChanged);
  m_onEditedFuncMap.insert(watchFileSystemEnabled,
                           &PreferencesPopup::onWatchFileSystemClicked);
  m_onEditedFuncMap.insert(pathAliasPriority,
                           &PreferencesPopup::onPathAliasPriorityChanged);

  connect(m_pref, &Preferences::stopAutoSave, this,
          &PreferencesPopup::onAutoSaveExternallyChanged);
  connect(m_pref, &Preferences::startAutoSave, this,
          &PreferencesPopup::onAutoSaveExternallyChanged);
  connect(m_pref, &Preferences::autoSavePeriodChanged, this,
          &PreferencesPopup::onAutoSavePeriodExternallyChanged);

  connect(m_projectRootDocuments, &QCheckBox::stateChanged, this,
          &PreferencesPopup::onProjectRootChanged);
  connect(m_projectRootDesktop, &QCheckBox::stateChanged, this,
          &PreferencesPopup::onProjectRootChanged);
  connect(m_projectRootCustom, &QCheckBox::stateChanged, this,
          &PreferencesPopup::onProjectRootChanged);
  connect(m_projectRootCustom, &QCheckBox::clicked, customField,
          &QWidget::setVisible);

  return widget;
}

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

QWidget* PreferencesPopup::createInterfacePage() {
  QList<ComboBoxItem> styleSheetItemList;
  for (const QString& str : m_pref->getStyleSheetList()) {
    TFilePath path(str.toStdWString());
    QString name = QString::fromStdWString(path.getWideName());
    styleSheetItemList.push_back(ComboBoxItem(name, name));
  }

  QList<ComboBoxItem> roomItemList;
  for (const QString& roomName : m_pref->getRoomMap())
    roomItemList.push_back(ComboBoxItem(roomName, roomName));

  QList<ComboBoxItem> languageItemList;
  for (const QString& name : m_pref->getLanguageList())
    languageItemList.push_back(ComboBoxItem(name, name));

  QPushButton* additionalStyleSheetBtn =
      new QPushButton(tr("Edit Additional Style Sheet.."));
  QPushButton* check30bitBtn = new QPushButton(tr("Check Availability"));

  QWidget* widget  = new QWidget(this);
  QGridLayout* lay = new QGridLayout();
  setupLayout(lay);

  insertUI(CurrentLanguageName, lay, languageItemList);

  insertDualUIs(linearUnits, cameraUnits, lay, getComboItemList(linearUnits),
                getComboItemList(linearUnits));
  // cameraUnits share items with linearUnits

  lay->addWidget(new QLabel(tr("Pixels Only:"), this), 5, 0,
                 Qt::AlignRight | Qt::AlignVCenter);
  lay->addWidget(createUI(pixelsOnly), 5, 1, 1, 2, Qt::AlignLeft);

  insertUI(functionEditorToggle, lay, getComboItemList(functionEditorToggle));
  insertUI(iconSize, lay);

  insertUI(CurrentStyleSheetName, lay, styleSheetItemList);
  int row = lay->rowCount();
  lay->addWidget(additionalStyleSheetBtn, row - 1, 2, Qt::AlignRight);
  insertUI(CurrentRoomChoice, lay, roomItemList);
  insertUI(interfaceFont, lay);  // creates QFontComboBox
  insertUI(interfaceFontStyle, lay, buildFontStyleList());
  qobject_cast<QComboBox*>(m_controlIdMap.key(interfaceFontStyle))
      ->setSizeAdjustPolicy(QComboBox::AdjustToContents);

  QGridLayout* colorCalibLay = insertGroupBoxUI(colorCalibrationEnabled, lay);
  { insertUI(colorCalibrationLutPaths, colorCalibLay); }
  insertUI(displayIn30bit, lay);
  row = lay->rowCount();
  lay->addWidget(check30bitBtn, row - 1, 2, Qt::AlignRight);
  insertUI(showIconsInMenu, lay);
  insertUI(showRoomBindButtons, lay);

  lay->setRowStretch(lay->rowCount(), 1);
  insertFootNote(lay);
  widget->setLayout(lay);

  if (m_pref->getBoolValue(pixelsOnly)) {
    m_controlIdMap.key(linearUnits)->setDisabled(true);
    m_controlIdMap.key(cameraUnits)->setDisabled(true);
  }
  // pixels unit may deactivated externally on loading scene (see
  // IoCmd::loadScene())
  connect(TApp::instance()->getCurrentScene(), &TSceneHandle::pixelUnitSelected,
          this, &PreferencesPopup::onPixelUnitExternallySelected);
  connect(additionalStyleSheetBtn, &QPushButton::clicked, this,
          &PreferencesPopup::onEditAdditionalStyleSheet);
  connect(check30bitBtn, &QPushButton::clicked, this,
          &PreferencesPopup::onCheck30bitDisplay);

  m_onEditedFuncMap.insert(CurrentStyleSheetName,
                           &PreferencesPopup::onStyleSheetTypeChanged);
  m_onEditedFuncMap.insert(pixelsOnly, &PreferencesPopup::onPixelsOnlyChanged);
  m_preEditedFuncMap.insert(linearUnits, &PreferencesPopup::beforeUnitChanged);
  m_onEditedFuncMap.insert(linearUnits, &PreferencesPopup::onUnitChanged);
  m_preEditedFuncMap.insert(cameraUnits, &PreferencesPopup::beforeUnitChanged);
  m_onEditedFuncMap.insert(cameraUnits, &PreferencesPopup::onUnitChanged);
  m_preEditedFuncMap.insert(CurrentRoomChoice,
                            &PreferencesPopup::beforeRoomChoiceChanged);
  m_onEditedFuncMap.insert(colorCalibrationEnabled,
                           &PreferencesPopup::onColorCalibrationChanged);

  return widget;
}

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

QWidget* PreferencesPopup::createVisualizationPage() {
  QWidget* widget  = new QWidget(this);
  QGridLayout* lay = new QGridLayout();
  setupLayout(lay);

  insertUI(show0ThickLines, lay);
  insertUI(regionAntialias, lay);
  insertUI(rasterizeAntialias, lay);

  lay->setRowStretch(lay->rowCount(), 1);
  widget->setLayout(lay);
  return widget;
}

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

QWidget* PreferencesPopup::createLoadingPage() {
  m_levelFormatNames = new QComboBox;
  m_levelFormatNames->setSizeAdjustPolicy(QComboBox::AdjustToContents);
  m_editLevelFormat = new QPushButton(tr("Edit"));

  QPushButton* addLevelFormat    = new QPushButton("+");
  QPushButton* removeLevelFormat = new QPushButton("-");
  addLevelFormat->setFixedSize(20, 20);
  removeLevelFormat->setFixedSize(20, 20);
  rebuildFormatsList();

  QWidget* widget  = new QWidget(this);
  QGridLayout* lay = new QGridLayout();
  setupLayout(lay);

  insertUI(importPolicy, lay, getComboItemList(importPolicy));
  insertUI(renamePolicy, lay, getComboItemList(renamePolicy));
  insertUI(convertPolicy, lay, getComboItemList(convertPolicy));
  QGridLayout* autoExposeLay = insertGroupBoxUI(autoExposeEnabled, lay);
  { insertUI(autoRemoveUnusedLevels, autoExposeLay); }
  insertUI(subsceneFolderEnabled, lay);
  insertUI(removeSceneNumberFromLoadedLevelName, lay);
  insertUI(IgnoreImageDpi, lay);
  insertUI(rasterLevelCachingBehavior, lay,
           getComboItemList(rasterLevelCachingBehavior));
  insertUI(columnIconLoadingPolicy, lay,
           getComboItemList(columnIconLoadingPolicy));

  // levelFormats,// need to be handle separately
  int row = lay->rowCount();
  lay->addWidget(new QLabel(tr("Level Settings by File Format:")), row, 0,
                 Qt::AlignRight | Qt::AlignVCenter);
  QHBoxLayout* levelFormatLay = new QHBoxLayout();
  levelFormatLay->setContentsMargins(0, 0, 0, 0);
  levelFormatLay->setSpacing(5);
  {
    levelFormatLay->addWidget(m_levelFormatNames);
    levelFormatLay->addWidget(addLevelFormat);
    levelFormatLay->addWidget(removeLevelFormat);
    levelFormatLay->addWidget(m_editLevelFormat);
    levelFormatLay->addStretch(1);
  }
  lay->addLayout(levelFormatLay, row, 1, 1, 2);

  lay->setRowStretch(lay->rowCount(), 1);
  widget->setLayout(lay);

  connect(addLevelFormat, &QPushButton::clicked, this,
          &PreferencesPopup::onAddLevelFormat);
  connect(removeLevelFormat, &QPushButton::clicked, this,
          &PreferencesPopup::onRemoveLevelFormat);
  connect(m_editLevelFormat, &QPushButton::clicked, this,
          &PreferencesPopup::onEditLevelFormat);
  connect(TApp::instance()->getCurrentScene(),
          &TSceneHandle::importPolicyChanged, this,
          &PreferencesPopup::onImportPolicyExternallyChanged);
  connect(TApp::instance()->getCurrentScene(),
          &TSceneHandle::convertPolicyChanged, this,
          &PreferencesPopup::onConvertPolicyExternallyChanged);
  connect(TApp::instance()->getCurrentScene(),
          &TSceneHandle::renamePolicyChanged, this,
          &PreferencesPopup::onRenamePolicyExternallyChanged);

  return widget;
}

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

QWidget* PreferencesPopup::createSavingPage() {
  QWidget* widget  = new QWidget(this);
  QGridLayout* lay = new QGridLayout();
  setupLayout(lay);

  QGridLayout* autoSaveLay = insertGroupBoxUI(autosaveEnabled, lay);
  {
    insertUI(autosavePeriod, autoSaveLay);
    insertUI(autosaveSceneEnabled, autoSaveLay);
    insertUI(autosaveOtherFilesEnabled, autoSaveLay);
  }
  QGridLayout* backupLay = insertGroupBoxUI(backupEnabled, lay);
  { insertUI(backupKeepCount, backupLay); }

  insertUI(replaceAfterSaveLevelAs, lay);
  insertUI(resetUndoOnSavingLevel, lay);
  QLabel* matteColorLabel =
      new QLabel(tr("Matte color is used for background when overwriting "
                    "raster levels with transparent pixels\nin non "
                    "alpha-enabled image format."),
                 this);
  lay->addWidget(matteColorLabel, lay->rowCount(), 0, 1, 3, Qt::AlignLeft);
  insertUI(rasterBackgroundColor, lay);

  lay->setRowStretch(lay->rowCount(), 1);
  widget->setLayout(lay);
  return widget;
}

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

QWidget* PreferencesPopup::createCodecPage() {
  auto putLabel = [&](const QString& labelStr, QGridLayout* lay) {
    lay->addWidget(new QLabel(labelStr, this), lay->rowCount(), 0, 1, 3,
                   Qt::AlignLeft | Qt::AlignVCenter);
  };

  QWidget* widget  = new QWidget(this);
  QGridLayout* lay = new QGridLayout();
  setupLayout(lay);

  putLabel(tr("OpenToonz can use FFmpeg for additional file formats.\n") +
               tr("FFmpeg is not bundled with OpenToonz.\n") +
               tr("Please provide the path where FFmpeg is located on your "
                  "computer."),
           lay);
  insertUI(ffmpegPath, lay);

  putLabel(tr("Number of seconds to wait for FFmpeg to complete processing the "
              "output:"),
           lay);
  putLabel(
      tr("Note: FFmpeg begins working once all images have been processed."),
      lay);
  insertUI(ffmpegTimeout, lay);

  putLabel("", lay);
  putLabel(
      tr("Enabling multi-thread rendering will render significantly faster \n"
         "but a random crash might occur, use at your own risk."),
      lay);
  insertUI(ffmpegMultiThread, lay);
  insertUI(quickTimeBackend, lay);

  lay->setRowStretch(lay->rowCount(), 1);
  insertFootNote(lay);
  widget->setLayout(lay);
  return widget;
}

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

QWidget* PreferencesPopup::createAutoLipSyncPage() {
  auto putLabel = [&](const QString& labelStr, QGridLayout* lay) {
    lay->addWidget(new QLabel(labelStr, this), lay->rowCount(), 0, 1, 3,
                   Qt::AlignLeft | Qt::AlignVCenter);
  };

  QWidget* widget  = new QWidget(this);
  QGridLayout* lay = new QGridLayout();
  setupLayout(lay);

  putLabel(tr("OpenToonz can use Rhubarb for auto lip-syncing.\n") +
               tr("Rhubarb is not bundled with OpenToonz.\n") +
               tr("Please provide the path where Rhubarb is located on your "
                  "computer."),
           lay);

  insertUI(rhubarbPath, lay);

  putLabel(tr("Number of seconds to wait for Rhubarb to complete processing "
              "the audio:"),
           lay);
  insertUI(rhubarbTimeout, lay);

  lay->setRowStretch(lay->rowCount(), 1);
  insertFootNote(lay);
  widget->setLayout(lay);
  return widget;
}

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

QWidget* PreferencesPopup::createDrawingPage() {
  QWidget* widget  = new QWidget(this);
  QGridLayout* lay = new QGridLayout();
  setupLayout(lay);

  insertUI(DefRasterFormat, lay, getComboItemList(DefRasterFormat));
  insertUI(DefLevelType, lay, getComboItemList(DefLevelType));
  QGridLayout* defaultLevelSizeLay =
      insertGroupBox(tr("Default Level Size"), lay);
  {
    insertDualUIs(DefLevelWidth, DefLevelHeight, defaultLevelSizeLay);
    insertUI(DefLevelDpi, defaultLevelSizeLay);
    insertUI(newLevelSizeToCameraSizeEnabled, defaultLevelSizeLay);
  }

  QGridLayout* autoCreationLay = insertGroupBoxUI(EnableAutocreation, lay);
  {
    insertUI(NumberingSystem, autoCreationLay,
             getComboItemList(NumberingSystem));
    insertUI(EnableAutoStretch, autoCreationLay);
    insertUI(EnableCreationInHoldCells, autoCreationLay);
    insertUI(EnableAutoRenumber, autoCreationLay);
  }
  insertUI(saveUnpaintedInCleanup, lay);
  insertUI(useNumpadForSwitchingStyles, lay);
  insertUI(downArrowInLevelStripCreatesNewFrame, lay);

  lay->setRowStretch(lay->rowCount(), 1);
  widget->setLayout(lay);

  m_onEditedFuncMap.insert(DefLevelType,
                           &PreferencesPopup::onDefLevelTypeChanged);
  m_onEditedFuncMap.insert(newLevelSizeToCameraSizeEnabled,
                           &PreferencesPopup::onDefLevelTypeChanged);

  onDefLevelTypeChanged();

  if (m_pref->getBoolValue(pixelsOnly)) {
    m_controlIdMap.key(DefLevelDpi)->setDisabled(true);
    getUI<MeasuredDoubleLineEdit*>(DefLevelWidth)->setDecimals(0);
    getUI<MeasuredDoubleLineEdit*>(DefLevelHeight)->setDecimals(0);
  }

  return widget;
}

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

QWidget* PreferencesPopup::createToolsPage() {
  QWidget* widget  = new QWidget(this);
  QGridLayout* lay = new QGridLayout();
  setupLayout(lay);

  // insertUI(dropdownShortcutsCycleOptions, lay,
  //         getComboItemList(dropdownShortcutsCycleOptions));
  insertUI(levelBasedToolsDisplay, lay,
           getComboItemList(levelBasedToolsDisplay));
  QGridLayout* fillToolOptionsLay =
      insertGroupBox(tr("Fill Tool Options (Toonz Raster Level)"), lay);
  {
    insertUI(DefRegionWithPaint, fillToolOptionsLay);
    insertUI(ReferFillPrevailing, fillToolOptionsLay);
    insertUI(FillOnlysavebox, fillToolOptionsLay);
  }
  insertUI(minimizeSaveboxAfterEditing, lay);
  QGridLayout* cursorOptionsLay =
      insertGroupBox(tr("Brush Cursor Options"), lay);
  {
    insertUI(cursorBrushType, cursorOptionsLay,
             getComboItemList(cursorBrushType));
    insertUI(cursorBrushStyle, cursorOptionsLay,
             getComboItemList(cursorBrushStyle));
    insertUI(cursorOutlineEnabled, cursorOptionsLay);
    insertUI(useStrokeEndCursor, cursorOptionsLay);
  }

  insertUI(vectorSnappingTarget, lay, getComboItemList(vectorSnappingTarget));
  QGridLayout* replaceVectorsLay = insertGroupBox(
      tr("Replace Vectors with Simplified Vectors Command"), lay);
  {
    insertUI(keepFillOnVectorSimplify, replaceVectorsLay);
    insertUI(useHigherDpiOnVectorSimplify, replaceVectorsLay);
  }

  insertUI(multiLayerStylePickerEnabled, lay);
  insertUI(useCtrlAltToResizeBrush, lay);
  insertUI(clickTwiceToCreateArcs, lay);
  insertUI(tempToolSwitchTimer, lay);

  QGridLayout* animateToolLay = insertGroupBox(tr("Animate Tool"), lay);
  {
    // Use IntField to display 100% instead of 1.0
    IntField* handleSizeSlider = new IntField(this);
    handleSizeSlider->setRange(100, 600);  // scale range: 100% to 600%

    // Get the decimal value (e.g., 1.0) and multiply by 100 for the slider
    double currentVal = Preferences::instance()->getAnimateToolHandleSize();
    handleSizeSlider->setValue((int)(currentVal * 100.0));

    // Divide by 100 before saving to maintain the decimal standard
    connect(handleSizeSlider, &IntField::valueChanged, [=](bool dragging) {
      double decimalVal = handleSizeSlider->getValue() / 100.0;
      Preferences::instance()->setValue(animateToolHandleSize, decimalVal);
      onAnimateToolChanged();
    });

    int row = animateToolLay->rowCount();
    animateToolLay->addWidget(new QLabel(getUIString(animateToolHandleSize)),
                              row, 0);
    animateToolLay->addWidget(handleSizeSlider, row, 1);

    insertUI(animateToolColor, animateToolLay);
  }
  // -------------------------------------------

  lay->setRowStretch(lay->rowCount(), 1);
  widget->setLayout(lay);

  m_onEditedFuncMap.insert(FillOnlysavebox,
                           &PreferencesPopup::notifySceneChanged);
  m_onEditedFuncMap.insert(levelBasedToolsDisplay,
                           &PreferencesPopup::onLevelBasedToolsDisplayChanged);

  m_onEditedFuncMap.insert(animateToolHandleSize,
                           &PreferencesPopup::onAnimateToolChanged);
  m_onEditedFuncMap.insert(animateToolColor,
                           &PreferencesPopup::onAnimateToolChanged);

  return widget;
}

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

QWidget* PreferencesPopup::createXsheetPage() {
  QWidget* widget  = new QWidget(this);
  QGridLayout* lay = new QGridLayout();
  setupLayout(lay);

  insertUI(xsheetLayoutPreference, lay,
           getComboItemList(xsheetLayoutPreference));
  insertUI(levelNameDisplayType, lay, getComboItemList(levelNameDisplayType));
  insertUI(xsheetStep, lay);
  insertUI(moveCurrentFrameByClickCellArea, lay);
  insertUI(alwaysDragFrameCell, lay);
  insertUI(DragCellsBehaviour, lay, getComboItemList(DragCellsBehaviour));
  insertUI(deleteCommandBehavior, lay, getComboItemList(deleteCommandBehavior));
  insertUI(pasteCellsBehavior, lay, getComboItemList(pasteCellsBehavior));
  insertUI(cellInputMethod, lay, getComboItemList(cellInputMethod));

  QGridLayout* xshColHeaderLay = insertGroupBox(tr("Xsheet Column Area"), lay);
  {
    insertUI(linkColumnNameWithLevel, xshColHeaderLay);
    insertUI(showColumnNumbers, xshColHeaderLay);
    insertUI(unifyColumnVisibilityToggles, xshColHeaderLay);
    insertUI(parentColorsInXsheetColumn, xshColHeaderLay);
  }
  QGridLayout* xshCellAreaLay = insertGroupBox(tr("Xsheet Cell Area"), lay);
  {
    insertUI(highlightLineEverySecond, xshCellAreaLay);
    insertUI(currentTimelineEnabled, xshCellAreaLay);
    insertUI(showFrameNumberWithLetters, xshCellAreaLay);
  }

  QGridLayout* showKeyLay =
      insertGroupBoxUI(showKeyframesOnXsheetCellArea, lay);
  insertUI(showXsheetCameraColumn, showKeyLay);

  QGridLayout* xshToolbarLay = insertGroupBox(tr("Xsheet Tools"), lay);
  {
    insertUI(showXSheetToolbar, xshToolbarLay);
    insertUI(showXsheetBreadcrumbs, xshToolbarLay);
    insertUI(expandFunctionHeader, xshToolbarLay);
  }

  insertUI(useArrowKeyToShiftCellSelection, lay);
  insertUI(shortcutCommandsWhileRenamingCellEnabled, lay);
  insertUI(syncLevelRenumberWithXsheet, lay);
  insertUI(currentColumnColor, lay);

  lay->setRowStretch(lay->rowCount(), 1);
  insertFootNote(lay);
  widget->setLayout(lay);

  m_onEditedFuncMap.insert(showKeyframesOnXsheetCellArea,
                           &PreferencesPopup::onShowKeyframesOnCellAreaChanged);
  m_onEditedFuncMap.insert(showXsheetCameraColumn,
                           &PreferencesPopup::onShowKeyframesOnCellAreaChanged);
  m_onEditedFuncMap.insert(
      unifyColumnVisibilityToggles,
      &PreferencesPopup::onUnifyColumnVisibilityTogglesChanged);
  m_onEditedFuncMap.insert(showXsheetBreadcrumbs,
                           &PreferencesPopup::onShowXsheetBreadcrumbsClicked);

  QCheckBox* linkColumnNameWithLevelCheck =
      getUI<QCheckBox*>(linkColumnNameWithLevel);
  linkColumnNameWithLevelCheck->setToolTip(
      tr("This option will do the following:\n"
         "- When setting a cell in the empty column, level name will be copied "
         "to the column name\n"
         "- Typing the cell without level name in the empty column will try to "
         "use a level with the same name as the column\n"
         "The behavior may be changed in the future development."));
  return widget;
}

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

QWidget* PreferencesPopup::createAnimationPage() {
  QWidget* widget  = new QWidget(this);
  QGridLayout* lay = new QGridLayout();
  setupLayout(lay);

  insertUI(keyframeType, lay, getComboItemList(keyframeType));
  insertUI(animationStep, lay);
  insertUI(modifyExpressionOnMovingReferences, lay);

  lay->setRowStretch(lay->rowCount(), 1);
  widget->setLayout(lay);

  m_onEditedFuncMap.insert(
      modifyExpressionOnMovingReferences,
      &PreferencesPopup::onModifyExpressionOnMovingReferencesChanged);

  return widget;
}

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

QWidget* PreferencesPopup::createPreviewPage() {
  QWidget* widget  = new QWidget(this);
  QGridLayout* lay = new QGridLayout();
  setupLayout(lay);

  QGridLayout* viewerLay = insertGroupBox(tr("Viewer"), lay);
  {
    insertDualUIs(viewShrink, viewStep, viewerLay);
    insertUI(viewerZoomCenter, viewerLay, getComboItemList(viewerZoomCenter));
    insertUI(ignoreAlphaonColumn1Enabled, viewerLay);
    insertUI(actualPixelViewOnSceneEditingMode, viewerLay);
    insertUI(showRasterImagesDarkenBlendedInViewer, viewerLay);
    insertUI(viewerIndicatorEnabled, viewerLay);
  }
  QGridLayout* palyControlLay = insertGroupBox(tr("Play Control"), lay);
  {
    insertUI(xsheetAutopanEnabled, lay);
    insertUI(rewindAfterPlayback, palyControlLay);
    insertUI(blanksCount, palyControlLay);
    insertUI(blankColor, palyControlLay);
    insertUI(shortPlayFrameCount, palyControlLay);
  }
  QGridLayout* previewLay = insertGroupBox(tr("Preview"), lay);
  {
    insertUI(generatedMovieViewEnabled, previewLay);
    insertUI(fitToFlipbookWhenPreview, previewLay);
    insertUI(previewAlwaysOpenNewFlip, previewLay);
    insertUI(defaultViewerEnabled, previewLay);
  }
  QGridLayout* renderLay = insertGroupBox(tr("Render"), lay);
  {
    insertUI(sceneNumberingEnabled, renderLay);
    insertUI(taskchunksize, renderLay);
    renderLay->addWidget(
        new QLabel(tr("Please indicate where you would like exports from Fast "
                      "Render (MP4) to go."),
                   this),
        renderLay->rowCount(), 0, 1, 3, Qt::AlignLeft | Qt::AlignVCenter);
    insertUI(fastRenderPath, renderLay);
  }

  lay->setRowStretch(lay->rowCount(), 1);
  widget->setLayout(lay);

  m_onEditedFuncMap.insert(blanksCount, &PreferencesPopup::onBlankCountChanged);
  m_onEditedFuncMap.insert(blankColor, &PreferencesPopup::onBlankColorChanged);

  return widget;
}

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

QWidget* PreferencesPopup::createOnionSkinPage() {
  QWidget* widget  = new QWidget(this);
  QGridLayout* lay = new QGridLayout();
  setupLayout(lay);

  insertUI(onionSkinEnabled, lay);
  insertUI(onionPaperThickness, lay);
  insertUI(backOnionColor, lay);
  insertUI(frontOnionColor, lay);
  insertUI(onionInksOnly, lay);
  insertUI(onionSkinDuringPlayback, lay);
  insertUI(useOnionColorsForShiftAndTraceGhosts, lay);
  insertUI(animatedGuidedDrawing, lay, getComboItemList(animatedGuidedDrawing));

  lay->setRowStretch(lay->rowCount(), 1);
  widget->setLayout(lay);

  m_onEditedFuncMap.insert(onionSkinEnabled,
                           &PreferencesPopup::onOnionSkinVisibilityChanged);
  m_onEditedFuncMap.insert(onionPaperThickness,
                           &PreferencesPopup::notifySceneChanged);
  m_onEditedFuncMap.insert(backOnionColor,
                           &PreferencesPopup::onOnionColorChanged);
  m_onEditedFuncMap.insert(frontOnionColor,
                           &PreferencesPopup::onOnionColorChanged);
  m_onEditedFuncMap.insert(onionInksOnly,
                           &PreferencesPopup::notifySceneChanged);

  bool onionActive = m_pref->getBoolValue(onionSkinEnabled);
  if (!onionActive) {
    m_controlIdMap.key(onionPaperThickness)->setDisabled(true);
    m_controlIdMap.key(backOnionColor)->setDisabled(true);
    m_controlIdMap.key(frontOnionColor)->setDisabled(true);
    m_controlIdMap.key(onionInksOnly)->setDisabled(true);
  }

  return widget;
}

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

QWidget* PreferencesPopup::createColorsPage() {
  QWidget* widget  = new QWidget(this);
  QGridLayout* lay = new QGridLayout();
  setupLayout(lay);

  insertUI(viewerBGColor, lay);
  insertUI(previewBGColor, lay);
  insertUI(levelEditorBoxColor, lay);
  insertUI(chessboardColor1, lay);
  insertUI(chessboardColor2, lay);
  QGridLayout* tcLay = insertGroupBox(tr("Transparency Check"), lay);
  {
    insertUI(transpCheckInkOnWhite, tcLay);
    insertUI(transpCheckInkOnBlack, tcLay);
    insertUI(transpCheckPaint, tcLay);
  }

  QGridLayout* ipcLay = insertGroupBox(tr("Ink and Paint Check"), lay);
  {
    insertUI(inkCheckColor, ipcLay);
    insertUI(ink1CheckColor, ipcLay);
    insertUI(paintCheckColor, ipcLay);
  }

  lay->setRowStretch(lay->rowCount(), 1);
  widget->setLayout(lay);

  m_onEditedFuncMap.insert(viewerBGColor,
                           &PreferencesPopup::notifySceneChanged);
  m_onEditedFuncMap.insert(previewBGColor,
                           &PreferencesPopup::notifySceneChanged);
  m_onEditedFuncMap.insert(levelEditorBoxColor,
                           &PreferencesPopup::notifySceneChanged);
  m_onEditedFuncMap.insert(chessboardColor1,
                           &PreferencesPopup::notifySceneChanged);
  m_onEditedFuncMap.insert(chessboardColor2,
                           &PreferencesPopup::notifySceneChanged);
  m_onEditedFuncMap.insert(chessboardColor1,
                           &PreferencesPopup::onChessboardChanged);
  m_onEditedFuncMap.insert(chessboardColor2,
                           &PreferencesPopup::onChessboardChanged);

  m_onEditedFuncMap.insert(inkCheckColor,
                           &PreferencesPopup::onTranspCheckDataChanged);
  m_onEditedFuncMap.insert(ink1CheckColor,
                           &PreferencesPopup::onTranspCheckDataChanged);
  m_onEditedFuncMap.insert(paintCheckColor,
                           &PreferencesPopup::onTranspCheckDataChanged);
  return widget;
}

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

QWidget* PreferencesPopup::createVersionControlPage() {
  SVNConfigWriter* writer = new SVNConfigWriter();
  QWidget* widget         = new QWidget(this);
  QGridLayout* lay        = new QGridLayout();
  QHBoxLayout* svnUserLay = new QHBoxLayout();
  QHBoxLayout* svnRepLay  = new QHBoxLayout();

  QLabel* repLabel = new QLabel(QString("Repositories*: "));
  svnRepLay->addWidget(repLabel);
  QComboBox* repoCombo               = new QComboBox();
  QList<ComboBoxItem> repositoryList = PreferencesPopup::buildSvnRepList();
  for (const ComboBoxItem& item : repositoryList)
    repoCombo->addItem(item.first, item.second);
  QPushButton* addRep = new QPushButton("+");
  addRep->setFixedSize(20, 20);
  QPushButton* removeRep = new QPushButton("-");
  removeRep->setFixedSize(20, 20);
  QPushButton* editRep = new QPushButton("Edit");

  QLabel* userLabel = new QLabel(QString("Users*: "));
  svnUserLay->addWidget(userLabel);
  QComboBox* userCombo         = new QComboBox();
  QList<ComboBoxItem> userList = PreferencesPopup::buildSvnUserList();
  for (const ComboBoxItem& item : userList)
    userCombo->addItem(item.first, item.second);
  QPushButton* addUser = new QPushButton("+");
  addUser->setFixedSize(20, 20);
  QPushButton* removeUser = new QPushButton("-");
  removeUser->setFixedSize(20, 20);
  QPushButton* editUser = new QPushButton("Edit");

  svnRepLay->setSpacing(5);
  svnRepLay->addWidget(repoCombo);
  svnRepLay->addWidget(addRep);
  svnRepLay->addWidget(removeRep);
  svnRepLay->addWidget(editRep);

  svnUserLay->setSpacing(5);
  svnUserLay->addWidget(userCombo);
  svnUserLay->addWidget(addUser);
  svnUserLay->addWidget(removeUser);
  svnUserLay->addWidget(editUser);

  setupLayout(lay);
  lay->setColumnMinimumWidth(0, 300);

  insertUI(SVNEnabled, lay);
  lay->addLayout(svnUserLay, 3, 0);
  lay->addLayout(svnRepLay, 4, 0);
  insertUI(automaticSVNFolderRefreshEnabled, lay);
  insertUI(latestVersionCheckEnabled, lay);

  lay->setRowStretch(lay->rowCount(), 1);
  insertFootNote(lay);
  widget->setLayout(lay);

  m_onEditedFuncMap.insert(SVNEnabled, &PreferencesPopup::onSVNEnabledChanged);

  connect(addRep, &QPushButton::clicked, this, [repoCombo, writer]() {
    QString addedRepo = writer->writeRepository("");
    if (repoCombo->findText(addedRepo) == -1 && !addedRepo.isEmpty()) {
      repoCombo->addItem(addedRepo);
      repoCombo->setCurrentText(addedRepo);
    }
  });
  connect(removeRep, &QPushButton::clicked, this, [repoCombo, writer]() {
    writer->writeRepository(repoCombo->currentText(), QString(), QString(),
                            true);
    repoCombo->removeItem(repoCombo->currentIndex());
  });
  connect(editRep, &QPushButton::clicked, this, [repoCombo, writer]() {
    if (repoCombo->currentText().isEmpty()) return;
    writer->writeRepository(repoCombo->currentText());
  });

  connect(addUser, &QPushButton::clicked, this, [userCombo, writer]() {
    QString addedUser = writer->writeSvnUser("");
    if (userCombo->findText(addedUser) == -1 && !addedUser.isEmpty()) {
      userCombo->addItem(addedUser);
      userCombo->setCurrentText(addedUser);
    }
  });
  connect(removeUser, &QPushButton::clicked, this, [userCombo, writer]() {
    writer->writeSvnUser(userCombo->currentText(), QString(), true);
    userCombo->removeItem(userCombo->currentIndex());
  });
  connect(editUser, &QPushButton::clicked, this, [userCombo, writer]() {
    if (userCombo->currentText().isEmpty()) return;
    writer->writeSvnUser(userCombo->currentText());
  });

  return widget;
}

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

QWidget* PreferencesPopup::createTouchTabletPage() {
  bool winInkAvailable = false;
#ifdef _WIN32
  winInkAvailable = KisTabletSupportWin8::isAvailable();
#endif

  QAction* touchAction =
      CommandManager::instance()->getAction(MI_TouchGestureControl);
  CheckBox* enableTouchGestures =
      new CheckBox(tr("Enable Touch Gesture Controls"));
  enableTouchGestures->setChecked(touchAction->isChecked());

  QWidget* widget  = new QWidget(this);
  QGridLayout* lay = new QGridLayout();
  setupLayout(lay);

  lay->addWidget(enableTouchGestures, 0, 0, 1, 2);
  if (winInkAvailable) insertUI(winInkEnabled, lay);
#ifdef WITH_WINTAB
  insertUI(useQtNativeWinInk, lay);
#endif

  lay->setRowStretch(lay->rowCount(), 1);
  if (winInkAvailable) insertFootNote(lay);
  widget->setLayout(lay);

  connect(enableTouchGestures, &CheckBox::clicked, touchAction,
          &QAction::setChecked);
  connect(touchAction, &QAction::triggered, enableTouchGestures,
          &CheckBox::setChecked);

  return widget;
}

#ifdef _WIN32
#include <windows.h>
QWidget* PreferencesPopup::createAddonsPage() {
  QWidget* widget  = new QWidget(this);
  QGridLayout* lay = new QGridLayout();

  static auto CheckToonzPreview = []() -> bool {
    HKEY h;
    bool r = RegOpenKeyW(HKEY_CLASSES_ROOT,
                         L"CLSID\\{A20E2270-0FE1-421D-A34E-98C26C13F9DB}",
                         &h) == ERROR_SUCCESS;
    if (r) RegCloseKey(h);
    return r;
  };

  QGroupBox* groupBox =
      new QGroupBox(tr("Windows Explorer Thumbnails (Shell Extension)"));
  QVBoxLayout* gbLayout = new QVBoxLayout(groupBox);
  gbLayout->setMargin(10);

  QLabel* infoLabel =
      new QLabel(tr("Enable thumbnails for OpenToonz files (.tnz, .pli, .tlv) "
                    "in Windows Explorer. \n"
                    "Administrator privileges are required; you may need to "
                    "restart Explorer for changes to take effect."));
  gbLayout->addWidget(infoLabel);

  QPushButton* previewButton = new QPushButton(this);
  previewButton->setText(CheckToonzPreview() ? tr("Uninstall") : tr("Install"));

  gbLayout->addWidget(previewButton);

  connect(previewButton, &QPushButton::clicked, this, [previewButton]() {
    bool install = !CheckToonzPreview();
    QString dllPath_raw =
        QApplication::applicationDirPath() + "/toonzpreview.dll";

    QString dllPath = dllPath_raw.replace("/", "\\");

    if (install && !QFile::exists(dllPath)) {
      DVGui::warning("toonzpreview.dll not found!");
      return;
    }

    QString quotedDllPath = QString("\"%1\"").arg(dllPath);

    QStringList args =
        install ? QStringList{quotedDllPath} : QStringList{"/u", quotedDllPath};

    QString command = "regsvr32";

    QString paramStr = args.join(" ");

    SHELLEXECUTEINFO sei = {sizeof(sei)};

    sei.lpVerb = L"runas";

    std::wstring commandW  = command.toStdWString();
    std::wstring paramStrW = paramStr.toStdWString();

    sei.hProcess     = NULL;
    sei.lpFile       = commandW.c_str();   // regsvr32.exe
    sei.lpParameters = paramStrW.c_str();  // param
    sei.nShow        = SW_SHOWNORMAL;      // UAC
    sei.fMask        = SEE_MASK_NOCLOSEPROCESS;

    bool operationSuccess = false;
    if (ShellExecuteEx(&sei)) {
      if (sei.hProcess) {
        WaitForSingleObject(sei.hProcess, 60000);
      }
    }

    previewButton->setText(CheckToonzPreview() ? tr("Uninstall")
                                               : tr("Install"));
  });

  lay->addWidget(groupBox, lay->rowCount(), 0, 2, 4, Qt::AlignLeft);
  lay->setRowStretch(lay->rowCount(), 1);

  widget->setLayout(lay);

  return widget;
}
#endif  // _WIN32

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

void PreferencesPopup::onChange() {
  QWidget* senderWidget = qobject_cast<QWidget*>(sender());
  if (!senderWidget) return;
  PreferencesItemId id = m_controlIdMap.value(senderWidget);

  if (m_preEditedFuncMap.contains(id)) (this->*m_preEditedFuncMap[id])();

  if (CheckBox* cb = dynamic_cast<CheckBox*>(senderWidget))
    m_pref->setValue(id, cb->isChecked());
  else if (IntLineEdit* edit = dynamic_cast<IntLineEdit*>(senderWidget))
    m_pref->setValue(id, edit->getValue());
  else if (QComboBox* comboBox = dynamic_cast<QComboBox*>(senderWidget))
    m_pref->setValue(id, comboBox->currentData());
  else if (DoubleValueLineEdit* field =
               dynamic_cast<DoubleValueLineEdit*>(senderWidget))
    m_pref->setValue(id, field->getValue());
  else if (FileField* field = dynamic_cast<FileField*>(senderWidget))
    m_pref->setValue(id, field->getPath());
  else if (SizeField* field = dynamic_cast<SizeField*>(senderWidget))
    m_pref->setValue(id, field->getValue());
  else if (QGroupBox* groupBox = dynamic_cast<QGroupBox*>(senderWidget))
    m_pref->setValue(id, groupBox->isChecked());
  else
    return;

  if (m_onEditedFuncMap.contains(id)) (this->*m_onEditedFuncMap[id])();
}

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

void PreferencesPopup::onColorFieldChanged(const TPixel32& color,
                                           bool isDragging) {
  QWidget* senderWidget = qobject_cast<QWidget*>(sender());
  if (!senderWidget) return;
  PreferencesItemId id = m_controlIdMap.value(senderWidget);

  if (m_preEditedFuncMap.contains(id)) (this->*m_preEditedFuncMap[id])();

  // do not save to file while dragging
  m_pref->setValue(id, QColor(color.r, color.g, color.b, color.m), !isDragging);

  // very dirty implementation but I want to avoid calling invalidateIcons
  // while dragging transparency colors sliders..!
  if (id == transpCheckInkOnWhite || id == transpCheckInkOnBlack ||
      id == transpCheckPaint)
    return;

  if (m_onEditedFuncMap.contains(id)) (this->*m_onEditedFuncMap[id])();
}

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

OpenPopupCommandHandler<PreferencesPopup> openPreferencesPopup(MI_Preferences);

void PreferencesPopup::onAnimateToolChanged() {
  ToolHandle* toolHandle = TApp::instance()->getCurrentTool();
  if (toolHandle && toolHandle->getTool()) {
    toolHandle->getTool()->invalidate();
  }
}
