Blob Blame Raw


#include "exportlevelpopup.h"

// Tnz6 includes
#include "tapp.h"
#include "filebrowser.h"
#include "columnselection.h"
#include "menubarcommandids.h"
#include "iocommand.h"
#include "exportlevelcommand.h"
#include "formatsettingspopups.h"

// TnzQt includes
#include "toonzqt/checkbox.h"
#include "toonzqt/doublefield.h"
#include "toonzqt/colorfield.h"
#include "toonzqt/tselectionhandle.h"
#include "toonzqt/gutil.h"
#include "toonzqt/planeviewer.h"
#include "toonzqt/framenavigator.h"
#include "toonzqt/imageutils.h"

// TnzLib includes
#include "toonz/txsheet.h"
#include "toonz/tcamera.h"
#include "toonz/txshsimplelevel.h"
#include "toonz/txshcell.h"
#include "toonz/tstageobjecttree.h"
#include "toonz/toonzscene.h"
#include "toonz/sceneproperties.h"
#include "toonz/tproject.h"
#include "toonz/stage.h"
#include "toonz/preferences.h"

#include "toonz/txshlevelhandle.h"
#include "toonz/txsheethandle.h"
#include "toonz/tscenehandle.h"

// TnzCore includes
#include "tiio.h"
#include "tproperty.h"

// Qt includes
#include <QDir>

#include <QLabel>
#include <QPushButton>
#include <QComboBox>
#include <QGroupBox>
#include <QTabBar>
#include <QStackedWidget>
#include <QSplitter>
#include <QToolBar>

//********************************************************************************
//    Export callbacks  definition
//********************************************************************************

namespace {

struct MultiExportOverwriteCB final : public IoCmd::OverwriteCallbacks {
  bool m_yesToAll;
  bool m_stopped;

  MultiExportOverwriteCB() : m_yesToAll(false), m_stopped(false) {}
  bool overwriteRequest(const TFilePath &fp) override {
    if (m_yesToAll) return true;
    if (m_stopped) return false;

    int ret = DVGui::MsgBox(
        QObject::tr("Warning: file %1 already exists.").arg(toQString(fp)),
        QObject::tr("Continue Exporting"), QObject::tr("Continue to All"),
        QObject::tr("Stop Exporting"), 1);

    m_yesToAll = (ret == 2);
    m_stopped  = (ret == 0) || (ret == 3);
    return !m_stopped;
  }
};

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

struct MultiExportProgressCB final : public IoCmd::ProgressCallbacks {
  QString m_processedName;
  DVGui::ProgressDialog m_pb;

public:
  MultiExportProgressCB() : m_pb("", QObject::tr("Cancel"), 0, 0) {
    m_pb.show();
  }
  static QString msg() {
    return QObject::tr("Exporting level of %1 frames in %2");
  }

  void setProcessedName(const QString &name) override {
    m_processedName = name;
  }
  void setRange(int min, int max) override {
    m_pb.setMaximum(max);
    buildString();
  }
  void setValue(int val) override { m_pb.setValue(val); }
  bool canceled() const override { return m_pb.wasCanceled(); }
  void buildString() {
    m_pb.setLabelText(
        msg().arg(QString::number(m_pb.maximum())).arg(m_processedName));
  }
};

}  // namespace

//********************************************************************************
//    Swatch  definition
//********************************************************************************

class ExportLevelPopup::Swatch final : public PlaneViewer {
public:
  Swatch(QWidget *parent = 0) : PlaneViewer(parent) {}

  TImageP image() const { return m_img; }
  TImageP &image() { return m_img; }

protected:
  void showEvent(QShowEvent *se) override;
  void keyPressEvent(QKeyEvent *ke) override;
  void keyPressEvent(QShowEvent *se);
  void paintGL() override;

  void setActualPixelSize();

private:
  struct ShortcutZoomer final : public ImageUtils::ShortcutZoomer {
    ShortcutZoomer(Swatch *swatch) : ImageUtils::ShortcutZoomer(swatch) {}

  private:
    bool zoom(bool zoomin, bool resetZoom) override {
      return false;
    }  // Already covered by PlaneViewer
    bool setActualPixelSize() override {
      static_cast<Swatch *>(getWidget())->setActualPixelSize();
      return true;
    }
  };

private:
  TImageP m_img;  //!< Image shown in the swatch.
};

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

void ExportLevelPopup::Swatch::showEvent(QShowEvent *se) {
  // Set current scene's chessboard color
  TPixel32 pix1, pix2;
  Preferences::instance()->getChessboardColors(pix1, pix2);

  setBgColor(pix1, pix2);
}

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

void ExportLevelPopup::Swatch::keyPressEvent(QKeyEvent *ke) {
  if (ShortcutZoomer(this).exec(ke)) return;

  PlaneViewer::keyPressEvent(ke);
}

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

void ExportLevelPopup::Swatch::paintGL() {
  glPushAttrib(GL_COLOR_BUFFER_BIT);

  glEnable(GL_BLEND);
  glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);

  // Note GL_ONE instead of GL_SRC_ALPHA: it's needed since the input
  // image is supposedly premultiplied - and it works because the
  // viewer's background is opaque.
  // See tpixelutils.h's overPixT function for comparison.

  pushGLWorldCoordinates();
  {
    drawBackground();
    if (m_img) draw(m_img);
  }
  popGLCoordinates();

  glPopAttrib();
}

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

void ExportLevelPopup::Swatch::setActualPixelSize() {
  double dpix, dpiy;
  if (TRasterImageP ri = image())
    ri->getDpi(dpix, dpiy);
  else if (TToonzImageP ti = image())
    ti->getDpi(dpix, dpiy);
  else
    assert(false);

  if (dpix == 0.0 || dpiy == 0.0) dpix = dpiy = Stage::inch;

  setViewZoom(dpix / Stage::inch);
}

//********************************************************************************
//    ExportLevelPopup  implementation
//********************************************************************************

ExportLevelPopup::ExportLevelPopup()
    : FileBrowserPopup(tr("Export Level"), Options(CUSTOM_LAYOUT)) {
  setOkText(tr("Export"));

  TabBarContainter *tabBarContainer = new TabBarContainter;
  QTabBar *tabBar                   = new QTabBar;
  QStackedWidget *stackedWidget     = new QStackedWidget;
  QFrame *exportOptionsPage         = new QFrame;
  // Options / Swatch splitter
  QSplitter *splitter     = new QSplitter(Qt::Vertical);
  QScrollArea *scrollArea = new QScrollArea(splitter);
  QFrame *topWidget       = new QFrame(scrollArea);
  m_exportOptions         = new ExportLevelPopup::ExportOptions(splitter);
  m_swatch                = new Swatch(splitter);
  FrameNavigator *frameNavigator = new FrameNavigator;

  // Additional buttons between file name and Ok/Cancel
  QWidget *fileFormatContainer = new QWidget;
  // Format selection combo
  QLabel *formatLabel = new QLabel(tr("Format:"));
  m_format            = new QComboBox(this);
  // Retas compliant checkbox
  m_retas = new DVGui::CheckBox(tr("Retas Compliant"));
  // Format options button
  m_formatOptions = new QPushButton(tr("Options"));
  //-----

  tabBar->addTab(tr("File Browser"));
  tabBar->addTab(tr("Export Options"));

  tabBar->setDrawBase(false);
  tabBar->setExpanding(false);

  splitter->setSizePolicy(QSizePolicy(QSizePolicy::MinimumExpanding,
                                      QSizePolicy::MinimumExpanding));

  scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
  scrollArea->setWidgetResizable(true);
  scrollArea->setMinimumWidth(450);

  m_swatch->setMinimumHeight(150);
  m_swatch->setFocusPolicy(Qt::WheelFocus);

  static const int toolbarHeight = 22;
  frameNavigator->setFixedHeight(toolbarHeight);
  m_levelFrameIndexHandle.setFrame(
      0);  // Due to TFrameHandle's initialization, the initial frame
  frameNavigator->setFrameHandle(
      &m_levelFrameIndexHandle);  // is -1. Don't ask me why. Patching to 0.

  formatLabel->setFixedHeight(DVGui::WidgetHeight);
  m_format->setFixedHeight(DVGui::WidgetHeight);
  m_format->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Maximum);

  QStringList formats;
  formats << "tga"
          << "tif"
          << "png"
          << "jpg"
          << "bmp";
  formats.sort();
  m_format->addItems(formats);

  m_retas->setMinimumHeight(DVGui::WidgetHeight);
  m_formatOptions->setMinimumSize(60, 25);

  // layout
  QVBoxLayout *mainLayout = new QVBoxLayout();
  mainLayout->setMargin(0);
  mainLayout->setSpacing(3);
  {
    mainLayout->addWidget(tabBarContainer, 0);

    QHBoxLayout *tabBarLayout = new QHBoxLayout;
    tabBarLayout->setMargin(0);
    {
      tabBarLayout->addSpacing(6);
      tabBarLayout->addWidget(tabBar);
    }
    tabBarContainer->setLayout(tabBarLayout);

    // File Browser
    stackedWidget->addWidget(m_browser);
    // Export Options Page
    QVBoxLayout *eoPageLayout = new QVBoxLayout;
    eoPageLayout->setMargin(0);
    eoPageLayout->setSpacing(0);
    {
      // top area - options
      QVBoxLayout *topVLayout =
          new QVBoxLayout(topWidget);  // Needed to justify at top
      {
        topVLayout->addWidget(m_exportOptions);
        topVLayout->addStretch(1);
      }
      topWidget->setLayout(topVLayout);

      scrollArea->setWidget(topWidget);
      splitter->addWidget(scrollArea);

      // bottom area - swatch
      splitter->addWidget(m_swatch);
      splitter->setStretchFactor(0, 1);

      eoPageLayout->addWidget(splitter, 1);

      // toolbar
      QHBoxLayout *toolbarLayout = new QHBoxLayout;
      {
        toolbarLayout->addStretch(1);
        toolbarLayout->addWidget(frameNavigator, 0, Qt::AlignCenter);
        toolbarLayout->addStretch(1);
      }
      eoPageLayout->addLayout(toolbarLayout, 0);
    }
    exportOptionsPage->setLayout(eoPageLayout);
    stackedWidget->addWidget(exportOptionsPage);

    mainLayout->addWidget(stackedWidget, 1);

    //-------------- Buttons Toolbar ---------------------
    QHBoxLayout *bottomLay = new QHBoxLayout();
    bottomLay->setMargin(5);
    bottomLay->setSpacing(3);
    {
      bottomLay->addWidget(m_nameFieldLabel);
      bottomLay->addWidget(m_nameField, 1);

      QHBoxLayout *fileFormatLayout = new QHBoxLayout;
      fileFormatLayout->setMargin(0);
      fileFormatLayout->setSpacing(8);
      fileFormatLayout->setAlignment(Qt::AlignHCenter);
      {
        fileFormatLayout->addWidget(formatLabel);
        fileFormatLayout->addWidget(m_format);
        fileFormatLayout->addWidget(m_retas);
        fileFormatLayout->addWidget(m_formatOptions);
      }
      fileFormatContainer->setLayout(fileFormatLayout);
      bottomLay->addWidget(fileFormatContainer);

      bottomLay->addWidget(m_okButton);
      bottomLay->addWidget(m_cancelButton);
    }
    mainLayout->addLayout(bottomLay, 0);
  }
  setLayout(mainLayout);

  // Establish connections
  bool ret = true;
  ret      = connect(tabBar, SIGNAL(currentChanged(int)), stackedWidget,
                SLOT(setCurrentIndex(int)));
  ret = connect(m_format, SIGNAL(currentIndexChanged(const QString &)),
                SLOT(onformatChanged(const QString &))) &&
        ret;
  ret = connect(m_retas, SIGNAL(stateChanged(int)), SLOT(onRetas(int))) && ret;
  ret = connect(m_formatOptions, SIGNAL(clicked()), SLOT(onOptionsClicked())) &&
        ret;
  ret = connect(&m_levelFrameIndexHandle, SIGNAL(frameSwitched()),
                SLOT(updatePreview())) &&
        ret;
  ret = connect(m_exportOptions, SIGNAL(optionsChanged()),
                SLOT(updatePreview())) &&
        ret;
  assert(ret);
}

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

ExportLevelPopup::~ExportLevelPopup() {
  // The child FrameNavigator must be destroyed BEFORE m_levelFrameIndexHandle.
  // This is by FrameNavigator's implicit contract that the supplied frame
  // handle's
  // lifetime exceeds its own.

  QWidget().setLayout(layout());  // Reparents all layout-managed child widgets
}  // (i.e. all) to a temporary

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

void ExportLevelPopup::showEvent(QShowEvent *se) {
  // Save current selection - will be restored in the hideEvent()
  {
    TSelectionHandle *selectionHandle = TApp::instance()->getCurrentSelection();
    TSelection *selection             = selectionHandle->getSelection();

    selectionHandle->pushSelection();
    selectionHandle->setSelection(selection);
  }

  // WARNING: What happens it the restored selection is NO MORE VALID ?
  //          Consider that this popup is NOT MODAL !!
  //
  //          The same applies to all the other popups in
  //          filebrowserpopup.cpp...

  updateOnSelection();  // Here the selection is used
  updatePreview();

  // Establish connections
  TApp *app = TApp::instance();

  bool ret = true;
  {
    ret = connect(app->getCurrentSelection(),
                  SIGNAL(selectionChanged(TSelection *)),
                  SLOT(updateOnSelection())) &&
          ret;

    ret = connect(app->getCurrentLevel(), SIGNAL(xshLevelSwitched(TXshLevel *)),
                  this, SLOT(updatePreview())) &&
          ret;
    ret = connect(app->getCurrentLevel(), SIGNAL(xshLevelChanged()), this,
                  SLOT(updatePreview())) &&
          ret;
  }
  assert(ret);

  QDialog::showEvent(se);
}

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

void ExportLevelPopup::hideEvent(QHideEvent *he) {
  QDialog::hideEvent(he);

  TApp *app = TApp::instance();

  app->getCurrentSelection()->disconnect(this);
  app->getCurrentLevel()->disconnect(this);

  // Restore original selection
  TApp::instance()->getCurrentSelection()->popSelection();

  // Reset popup state
  m_swatch->image() = TImageP();
}

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

TPropertyGroup *ExportLevelPopup::getFormatProperties(const std::string &ext) {
  if (m_formatProperties.find(ext) != m_formatProperties.end())
    return m_formatProperties[ext];

  TPropertyGroup *props   = Tiio::makeWriterProperties(ext);
  m_formatProperties[ext] = props;
  return props;
}

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

IoCmd::ExportLevelOptions ExportLevelPopup::getOptions(const std::string &ext) {
  IoCmd::ExportLevelOptions opts = m_exportOptions->getOptions();

  opts.m_props    = getFormatProperties(ext);
  opts.m_forRetas = (m_retas->checkState() == Qt::Checked);

  return opts;
}

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

void ExportLevelPopup::onformatChanged(const QString &text) { checkAlpha(); }

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

void ExportLevelPopup::checkAlpha() {
  QString ext = m_format->currentText();

  if (ext == "bmp" || ext == "jpg") {
    m_exportOptions->m_bgColorField->setAlphaActive(false);
    return;
  }

  TPropertyGroup *props = getFormatProperties(ext.toStdString());
  if (!props) return;

  bool withAlpha = false;
  for (int i = 0; i < props->getPropertyCount(); ++i) {
    TProperty *p = props->getProperty(i);

    const QString &name = p->getQStringName();
    const QString &val  = QString::fromStdString(p->getValueAsString());

    if (name == "Bits Per Pixel") {
      withAlpha = (val.contains("32") || val.contains("64"));
      break;
    } else if (name == "Alpha Channel") {
      withAlpha = (val == "1");
      break;
    }
  }

  m_exportOptions->m_bgColorField->setAlphaActive(withAlpha);
}

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

void ExportLevelPopup::onOptionsClicked() {
  std::string ext       = m_format->currentText().toStdString();
  TPropertyGroup *props = getFormatProperties(ext);
  setModal(false);

  if (DVGui::Dialog *dialog =
          openFormatSettingsPopup(this, ext, props, m_browser->getFolder())) {
    bool ret;
    ret = connect(dialog, SIGNAL(dialogClosed()), SLOT(checkAlpha())),
    assert(ret);
  }
}

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

void ExportLevelPopup::onRetas(int) {
  if (m_retas->checkState() == Qt::Checked) {
    m_format->setCurrentIndex(m_format->findText("tga"));
    m_format->setEnabled(false);
  } else
    m_format->setEnabled(true);
}

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

void ExportLevelPopup::updateOnSelection() {
  // Disable name field in case of multiple selection
  TSelection *sel = TApp::instance()->getCurrentSelection()->getSelection();
  TColumnSelection *colSelection = dynamic_cast<TColumnSelection *>(sel);
  m_nameField->setEnabled(!colSelection ||
                          colSelection->getIndices().size() <= 1);

  // Enable tlv output in case all inputs are pli
  TApp *app = TApp::instance();

  bool allPlis;
  if (colSelection && colSelection->getIndices().size() >= 1) {
    TXsheet *xsh                    = app->getCurrentXsheet()->getXsheet();
    const std::set<int> &colIndices = colSelection->getIndices();

    std::set<int>::const_iterator it, end = colIndices.end();
    for (it = colIndices.begin(); it != end; ++it) {
      int r0, r1, c = *it;
      xsh->getCellRange(c, r0, r1);

      if (r0 <= r1) {
        TXshSimpleLevel *sl = xsh->getCell(r0, c).getSimpleLevel();
        assert(sl);

        if (!sl || (sl->getType() != PLI_XSHLEVEL)) break;
      }
    }

    allPlis = (it == end);
  } else {
    TXshSimpleLevel *sl = app->getCurrentLevel()->getSimpleLevel();
    allPlis             = sl && (sl->getType() == PLI_XSHLEVEL);
  }

  int tlvIdx = m_format->findText("tlv");
  if (allPlis) {
    if (tlvIdx < 0) m_format->addItem("tlv");
  } else {
    if (tlvIdx > 0) m_format->removeItem(tlvIdx);
  }

  m_exportOptions->updateOnSelection();
}

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

void ExportLevelPopup::updatePreview() {
  // Build export preview arguments
  TXshSimpleLevel *sl = TApp::instance()->getCurrentLevel()->getSimpleLevel();
  int frameIdx        = m_levelFrameIndexHandle.getFrame();

  const std::string &ext                = m_format->currentText().toStdString();
  const IoCmd::ExportLevelOptions &opts = getOptions(ext);

  // Update displayed image in the preview
  m_swatch->image() =
      sl ? IoCmd::exportedImage(ext, *sl, sl->index2fid(frameIdx), opts)
         : TImageP();

  m_swatch->update();
}

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

bool ExportLevelPopup::execute() {
  if (m_selectedPaths.empty()) return false;

  TFilePath fp(*m_selectedPaths.begin());

  // Build export options
  const std::string &ext                = m_format->currentText().toStdString();
  const IoCmd::ExportLevelOptions &opts = getOptions(ext);

  // Retrieve current column selection
  TApp *app                      = TApp::instance();
  TSelection *selection          = app->getCurrentSelection()->getSelection();
  TColumnSelection *colSelection = dynamic_cast<TColumnSelection *>(selection);
  if (colSelection && colSelection->getIndices().size() > 1) {
    fp = TFilePath(m_browser->getFolder() + TFilePath("a"))
             .withType(ext)
             .withFrame();

    bool ret = true;
    MultiExportOverwriteCB overwriteCB;
    MultiExportProgressCB progressCB;

    TXsheet *xsh                    = app->getCurrentXsheet()->getXsheet();
    const std::set<int> &colIndices = colSelection->getIndices();

    std::set<int>::const_iterator it, end = colIndices.end();
    for (it = colIndices.begin(); it != end; ++it) {
      if (progressCB.canceled()) break;

      int r0, r1, c = *it;
      xsh->getCellRange(c, r0, r1);

      if (r1 >= r0)  // There exists a not-empty cell
      {
        TXshSimpleLevel *sl = xsh->getCell(r0, c).getSimpleLevel();
        assert(sl);

        ret = ret && IoCmd::exportLevel(fp.withName(sl->getName()), sl, opts,
                                        &overwriteCB, &progressCB);
      }
    }

    return ret;
  } else {
    if (!isValidFileName(QString::fromStdString(fp.getName()))) {
      DVGui::error(
          tr("The file name cannot be empty or contain any of the following "
             "characters:(new line)  \\ / : * ? \"  |"));
      return false;
    }

    return IoCmd::exportLevel(fp.withType(ext).withFrame(), 0, opts);
  }
}

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

void ExportLevelPopup::initFolder() {
  TFilePath fp;

  TProject *project =
      TProjectManager::instance()->getCurrentProject().getPointer();
  ToonzScene *scene = TApp::instance()->getCurrentScene()->getScene();

  if (scene) fp = scene->decodeFilePath(TFilePath("+drawings"));

  QDir drawingsDir(QString::fromStdWString(fp.getWideString()));
  if (!drawingsDir.exists()) fp = project->getProjectFolder();

  m_browser->setFolder(fp);
}

//********************************************************************************
//    ExportLevelPopup::ExportOptions  implementation
//********************************************************************************

ExportLevelPopup::ExportOptions::ExportOptions(QWidget *parent)
    : QFrame(parent) {
  struct locals {
    static inline QGridLayout *newGridLayout() {
      QGridLayout *layout = new QGridLayout;

      layout->setColumnMinimumWidth(0, 50);
      layout->setColumnStretch(3, 1);

      return layout;
    }
  };

  static const double dmax = (std::numeric_limits<double>::max)(), dmin = -dmax;

  QGridLayout *layout = locals::newGridLayout();
  setLayout(layout);

  {
    int row = 0;

    //----------- Common Options ------------------

    QLabel *bgColorLabel = new QLabel(tr("Background Color:"));
    m_bgColorField =
        new DVGui::ColorField(0, false, TPixel32::Black, 35, false);
    layout->addWidget(bgColorLabel, row, 1, Qt::AlignRight);
    layout->addWidget(m_bgColorField, row++, 2, Qt::AlignLeft);

    m_noAntialias = new QCheckBox(tr("No Antialias"));
    layout->addWidget(m_noAntialias, row++, 2, Qt::AlignLeft);

    //-------------- Vector Options ---------------------

    m_pliOptions = new QWidget;
    layout->addWidget(m_pliOptions, row++, 0, 1, 4);
    {
      QGridLayout *layout = locals::newGridLayout();
      m_pliOptions->setLayout(layout);

      layout->setMargin(0);

      int row = 0;

      //-------------- Vectors Export Box ---------------------

      DVGui::Separator *vectorCameraBoxSeparator = new DVGui::Separator;
      vectorCameraBoxSeparator->setName(tr("Vectors Export Box"));
      layout->addWidget(vectorCameraBoxSeparator, row++, 0, 1, 4);

      QGridLayout *exportBoxLayout = new QGridLayout;
      layout->addLayout(exportBoxLayout, row++, 1, 1, 2, Qt::AlignLeft);

      {
        m_widthFld  = new DVGui::MeasuredDoubleLineEdit;
        m_heightFld = new DVGui::MeasuredDoubleLineEdit;
        m_hResFld   = new DVGui::IntLineEdit;
        m_vResFld   = new DVGui::IntLineEdit;
        m_dpiLabel  = new QLabel;
        m_resScale  = new DVGui::MeasuredDoubleLineEdit;

        m_widthFld->setRange(0, dmax);
        m_heightFld->setRange(0, dmax);
        m_hResFld->setRange(0, (std::numeric_limits<int>::max)());
        m_vResFld->setRange(0, (std::numeric_limits<int>::max)());
        m_resScale->setRange(0, dmax);

        m_resScale->setMeasure("percentage");
        m_resScale->setValue(1.0);

        m_widthFld->setFixedSize(50, 20);
        m_heightFld->setFixedSize(50, 20);
        m_vResFld->setFixedSize(50, 20);
        m_hResFld->setFixedSize(50, 20);
        m_resScale->setFixedSize(50, 20);

        exportBoxLayout->addWidget(new QLabel(tr("Width:")), 0, 0,
                                   Qt::AlignRight);
        exportBoxLayout->addWidget(m_widthFld, 0, 1, Qt::AlignLeft);
        exportBoxLayout->addWidget(new QLabel(tr("Height:")), 0, 2,
                                   Qt::AlignRight);
        exportBoxLayout->addWidget(m_heightFld, 0, 3, Qt::AlignLeft);
        exportBoxLayout->addWidget(m_dpiLabel, 0, 4, Qt::AlignRight);
        exportBoxLayout->addWidget(new QLabel(tr("H Resolution:")), 1, 0,
                                   Qt::AlignRight);
        exportBoxLayout->addWidget(m_hResFld, 1, 1, Qt::AlignLeft);
        exportBoxLayout->addWidget(new QLabel(tr("V Resolution:")), 1, 2,
                                   Qt::AlignRight);
        exportBoxLayout->addWidget(m_vResFld, 1, 3, Qt::AlignLeft);
        exportBoxLayout->addWidget(new QLabel(tr("Scale:")), 1, 4,
                                   Qt::AlignRight);
        exportBoxLayout->addWidget(m_resScale, 1, 5, Qt::AlignLeft);

        exportBoxLayout->setColumnStretch(6, 1);
      }

      //-------------- Vectors Thickness ---------------------

      DVGui::Separator *vectorThicknessSeparator = new DVGui::Separator;
      vectorThicknessSeparator->setName(tr("Vectors Thickness"));
      layout->addWidget(vectorThicknessSeparator, row++, 0, 1, 4);

      QGridLayout *thicknessLayout = new QGridLayout;
      layout->addLayout(thicknessLayout, row++, 1, 1, 2, Qt::AlignLeft);
      {
        QHBoxLayout *thicknessModeLayout = new QHBoxLayout;
        thicknessLayout->addLayout(thicknessModeLayout, 0, 0, Qt::AlignLeft);
        {
          thicknessModeLayout->addWidget(new QLabel(tr("Mode:")));

          m_thicknessTransformMode = new QComboBox;
          thicknessModeLayout->addWidget(m_thicknessTransformMode);

          m_thicknessTransformMode->addItems(
              QStringList() << tr("Scale Thickness") << tr("Add Thickness")
                            << tr("Constant Thickness"));

          thicknessModeLayout->addStretch(1);
        }

        QHBoxLayout *thicknessParamLayout = new QHBoxLayout;
        thicknessLayout->addLayout(thicknessParamLayout, 1, 0, Qt::AlignLeft);
        {
          m_fromThicknessScale        = new DVGui::MeasuredDoubleLineEdit;
          m_fromThicknessDisplacement = new DVGui::MeasuredDoubleLineEdit;
          m_toThicknessScale          = new DVGui::MeasuredDoubleLineEdit;
          m_toThicknessDisplacement   = new DVGui::MeasuredDoubleLineEdit;

          m_fromThicknessScale->setMeasure("percentage");
          m_toThicknessScale->setMeasure("percentage");

          m_fromThicknessScale->setRange(0, dmax);
          m_fromThicknessDisplacement->setRange(dmin, dmax);
          m_toThicknessScale->setRange(0, dmax);
          m_toThicknessDisplacement->setRange(dmin, dmax);

          m_fromThicknessScale->setValue(1.0);
          m_toThicknessScale->setValue(1.0);

          m_fromThicknessDisplacement->setValue(0.0);
          m_toThicknessDisplacement->setValue(0.0);

          thicknessParamLayout->addWidget(new QLabel(tr("Start:")));
          thicknessParamLayout->addWidget(m_fromThicknessScale);
          thicknessParamLayout->addWidget(m_fromThicknessDisplacement);

          thicknessParamLayout->addWidget(new QLabel(tr("End:")));
          thicknessParamLayout->addWidget(m_toThicknessScale);
          thicknessParamLayout->addWidget(m_toThicknessDisplacement);

          thicknessParamLayout->addStretch(1);

          onThicknessTransformModeChanged();
        }

        thicknessLayout->setColumnStretch(1, 1);
      }
    }
  }

  // Install connections
  bool ret = true;
  {
    ret = connect(m_widthFld, SIGNAL(editingFinished()), SLOT(updateXRes())) &&
          ret;
    ret = connect(m_heightFld, SIGNAL(editingFinished()), SLOT(updateYRes())) &&
          ret;
    ret = connect(m_hResFld, SIGNAL(editingFinished()), SLOT(updateYRes())) &&
          ret;  // Note that x and y here
    ret = connect(m_vResFld, SIGNAL(editingFinished()), SLOT(updateXRes())) &&
          ret;  // are exchanged
    ret =
        connect(m_resScale, SIGNAL(editingFinished()), SLOT(scaleRes())) && ret;

    ret = connect(m_thicknessTransformMode, SIGNAL(currentIndexChanged(int)),
                  SLOT(onThicknessTransformModeChanged())) &&
          ret;

    // Option changes
    ret = connect(m_bgColorField, SIGNAL(colorChanged(const TPixel32 &, bool)),
                  SIGNAL(optionsChanged())) &&
          ret;
    ret = connect(m_noAntialias, SIGNAL(clicked(bool)),
                  SIGNAL(optionsChanged())) &&
          ret;

    // Queued connections for camera fields, to guarantee that whenever signals
    // editingFinished()
    // and valueChanged() are emitted at the same time, the former takes
    // precedence (applies value corrections)
    ret = connect(m_widthFld, SIGNAL(valueChanged()), SIGNAL(optionsChanged()),
                  Qt::QueuedConnection) &&
          ret;
    ret = connect(m_heightFld, SIGNAL(valueChanged()), SIGNAL(optionsChanged()),
                  Qt::QueuedConnection) &&
          ret;
    ret = connect(m_hResFld, SIGNAL(textChanged(const QString &)),
                  SIGNAL(optionsChanged()), Qt::QueuedConnection) &&
          ret;
    ret = connect(m_vResFld, SIGNAL(textChanged(const QString &)),
                  SIGNAL(optionsChanged()), Qt::QueuedConnection) &&
          ret;

    ret = connect(m_thicknessTransformMode, SIGNAL(currentIndexChanged(int)),
                  SIGNAL(optionsChanged())) &&
          ret;
    ret = connect(m_fromThicknessScale, SIGNAL(valueChanged()),
                  SIGNAL(optionsChanged())) &&
          ret;
    ret = connect(m_fromThicknessDisplacement, SIGNAL(valueChanged()),
                  SIGNAL(optionsChanged())) &&
          ret;
    ret = connect(m_toThicknessScale, SIGNAL(valueChanged()),
                  SIGNAL(optionsChanged())) &&
          ret;
    ret = connect(m_toThicknessDisplacement, SIGNAL(valueChanged()),
                  SIGNAL(optionsChanged())) &&
          ret;
  }
  assert(ret);

  updateCameraDefault();  // Default values must be available even
                          // if the widget has not yet been shown
}

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

void ExportLevelPopup::ExportOptions::showEvent(QShowEvent *se) {
  updateCameraDefault();
  updateOnSelection();
}

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

IoCmd::ExportLevelOptions ExportLevelPopup::ExportOptions::getOptions() const {
  IoCmd::ExportLevelOptions opts;

  opts.m_bgColor     = m_bgColorField->getColor();
  opts.m_noAntialias = m_noAntialias->isChecked();

  opts.m_camera.setSize(
      TDimensionD(m_widthFld->getValue(), m_heightFld->getValue()));
  opts.m_camera.setRes(
      TDimension(m_hResFld->getValue(), m_vResFld->getValue()));

  switch (m_thicknessTransformMode->currentIndex()) {
    enum { SCALE, ADD, CONSTANT };

  case SCALE:
    opts.m_thicknessTransform[0][0] = opts.m_thicknessTransform[1][0] = 0.0;
    opts.m_thicknessTransform[0][1] = m_fromThicknessScale->getValue();
    opts.m_thicknessTransform[1][1] = m_toThicknessScale->getValue();
    break;

  case ADD:
    opts.m_thicknessTransform[0][1] = opts.m_thicknessTransform[1][1] = 1.0;
    opts.m_thicknessTransform[0][0] =
        m_fromThicknessDisplacement->getValue() * Stage::inch;
    opts.m_thicknessTransform[1][0] =
        m_toThicknessDisplacement->getValue() * Stage::inch;
    break;

  case CONSTANT:
    opts.m_thicknessTransform[0][1] = opts.m_thicknessTransform[1][1] = 0.0;
    opts.m_thicknessTransform[0][0] =
        m_fromThicknessDisplacement->getValue() * Stage::inch;
    opts.m_thicknessTransform[1][0] =
        m_toThicknessDisplacement->getValue() * Stage::inch;
    break;
  }

  return opts;
}

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

void ExportLevelPopup::ExportOptions::updateOnSelection() {
  TApp *app = TApp::instance();

  TSelection *selection          = app->getCurrentSelection()->getSelection();
  TColumnSelection *colSelection = dynamic_cast<TColumnSelection *>(selection);

  if (colSelection && colSelection->getIndices().size() > 1) {
    bool enabled = false;

    TXsheet *xsh                    = app->getCurrentXsheet()->getXsheet();
    const std::set<int> &colIndices = colSelection->getIndices();

    std::set<int>::const_iterator it, end = colIndices.end();
    for (it = colIndices.begin(); it != end; ++it) {
      int r0, r1, c = *it;
      xsh->getCellRange(c, r0, r1);

      if (r0 <= r1)  // There exists a not-empty cell
      {
        TXshSimpleLevel *sl = xsh->getCell(r0, c).getSimpleLevel();
        assert(sl);

        enabled = enabled || (sl && (sl->getType() == PLI_XSHLEVEL));
      }
    }

    m_pliOptions->setEnabled(enabled);
    return;
  }

  TXshSimpleLevel *sl = TApp::instance()->getCurrentLevel()->getSimpleLevel();
  m_pliOptions->setEnabled(
      sl && (sl->getType() != TZP_XSHLEVEL && sl->getType() != OVL_XSHLEVEL));
}

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

void ExportLevelPopup::ExportOptions::updateCameraDefault() {
  TXsheet *xsheet = TApp::instance()->getCurrentXsheet()->getXsheet();
  assert(xsheet);

  TStageObjectId cameraId = xsheet->getStageObjectTree()->getCurrentCameraId();
  TStageObject *cameraObj = xsheet->getStageObject(cameraId);
  assert(cameraObj);

  TCamera *camera = cameraObj->getCamera();
  assert(camera);

  TDimensionD cameraSize = camera->getSize();
  m_widthFld->setValue(cameraSize.lx);
  m_heightFld->setValue(cameraSize.ly);

  TDimension cameraRes = camera->getRes();
  m_hResFld->setValue(cameraRes.lx);
  m_vResFld->setValue(cameraRes.ly);

  updateYRes();
}

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

void ExportLevelPopup::ExportOptions::updateXRes() {
  double ly = m_heightFld->getValue(), yres = m_vResFld->getValue(),
         dpi = ly > 0 ? yres / ly : 0;

  m_hResFld->setValue(tround(m_widthFld->getValue() * dpi));
  updateDpi();
}

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

void ExportLevelPopup::ExportOptions::updateYRes() {
  double lx = m_widthFld->getValue(), xres = m_hResFld->getValue(),
         dpi = lx > 0 ? xres / lx : 0;

  m_vResFld->setValue(tround(m_heightFld->getValue() * dpi));
  updateDpi();
}

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

void ExportLevelPopup::ExportOptions::scaleRes() {
  m_hResFld->setValue(tround(m_resScale->getValue() * m_hResFld->getValue()));
  m_resScale->setValue(1.0);

  updateYRes();
}

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

void ExportLevelPopup::ExportOptions::updateDpi() {
  double lx = m_widthFld->getValue(), xres = m_hResFld->getValue(),
         xdpi = lx > 0 ? xres / lx : 0;

  m_dpiLabel->setText(tr("DPI: ") + QString::number(xdpi, 'g', 4));
}

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

void ExportLevelPopup::ExportOptions::onThicknessTransformModeChanged() {
  bool scaleMode = (m_thicknessTransformMode->currentIndex() == 0);

  m_fromThicknessScale->setVisible(scaleMode);
  m_toThicknessScale->setVisible(scaleMode);

  m_fromThicknessDisplacement->setVisible(!scaleMode);
  m_toThicknessDisplacement->setVisible(!scaleMode);
}

//********************************************************************************
//    Export Level Command  instantiation
//********************************************************************************

OpenPopupCommandHandler<ExportLevelPopup> exportLevelPopupCommand(
    MI_ExportLevel);