//  TemplateDesigner.cpp
//
//  Copyright (C) 2018  Jaye Evins <evins@snaught.com>
//
//  This file is part of gLabels-qt.
//
//  gLabels-qt is free software: you can redistribute it and/or modify
//  it under the terms of the GNU General Public License as published by
//  the Free Software Foundation, either version 3 of the License, or
//  (at your option) any later version.
//
//  gLabels-qt is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU General Public License for more details.
//
//  You should have received a copy of the GNU General Public License
//  along with gLabels-qt.  If not, see <http://www.gnu.org/licenses/>.
//


#include "TemplateDesigner.hpp"

#include "SelectProductDialog.hpp"
#include "model/Db.hpp"
#include "model/Distance.hpp"
#include "model/FrameCd.hpp"
#include "model/FrameContinuous.hpp"
#include "model/FrameEllipse.hpp"
#include "model/FramePath.hpp"
#include "model/FrameRect.hpp"
#include "model/FrameRound.hpp"
#include "model/Markup.hpp"
#include "model/Model.hpp"
#include "model/PageRenderer.hpp"
#include "model/Settings.hpp"

#include <QDebug>
#include <QMessageBox>
#include <QPrintDialog>
#include <QPrinter>
#include <QVBoxLayout>

#include <algorithm>
#include <iostream>
#include <iterator>


//
// Private types and constants
//
namespace
{
        using namespace glabels;

        enum PageId
        {
                IntroPageId,
                NamePageId,
                PageSizePageId,
                ShapePageId,
                RectPageId,
                RoundPageId,
                EllipsePageId,
                CdPageId,
                PathPageId,
                ContinuousPageId,
                NLayoutsPageId,
                OneLayoutPageId,
                TwoLayoutPageId,
                ApplyPageId
        };


        const QString defaultPageSize[] =
        {
                /* ISO */ "A4",
                /* US  */ "US Letter"
        };


        const double maxPageSize[] =
        {
                /* PT */ 5000,
                /* IN */ 70,
                /* MM */ 1800,
                /* CM */ 180,
                /* PC */ 420
        };

        const model::Distance defaultMargin    = model::Distance::in(0.125);
        const model::Distance defaultWaste     = model::Distance::in(0);

        const model::Distance defaultRectW     = model::Distance::in(3.5);
        const model::Distance defaultRectH     = model::Distance::in(2.0);
        const model::Distance defaultRectR     = model::Distance::in(0);

        const model::Distance defaultRoundR    = model::Distance::in(0.75);

        const model::Distance defaultEllipseW  = model::Distance::in(3.5);
        const model::Distance defaultEllipseH  = model::Distance::in(2.0);

        const model::Distance defaultCdR1      = model::Distance::in(2.3125);
        const model::Distance defaultCdR2      = model::Distance::in(0.8125);
        const model::Distance defaultCdClip    = model::Distance::in(0);

}


namespace glabels
{

        ///
        /// Constructor
        ///
        TemplateDesigner::TemplateDesigner( QWidget* parent )
                : QWizard(parent)
        {
                setWindowTitle( tr("Product Template Designer") );
                setPixmap( QWizard::LogoPixmap, QIcon::fromTheme( "glabels" ).pixmap( 48 ) );
                setWizardStyle( QWizard::ModernStyle );
                setOption( QWizard::IndependentPages, false );
                setOption( QWizard::NoBackButtonOnStartPage, true );

                setPage( IntroPageId,      new TemplateDesignerIntroPage() );
                setPage( NamePageId,       new TemplateDesignerNamePage() );
                setPage( PageSizePageId,   new TemplateDesignerPageSizePage() );
                setPage( ShapePageId,      new TemplateDesignerShapePage() );
                setPage( RectPageId,       new TemplateDesignerRectPage() );
                setPage( RoundPageId,      new TemplateDesignerRoundPage() );
                setPage( EllipsePageId,    new TemplateDesignerEllipsePage() );
                setPage( CdPageId,         new TemplateDesignerCdPage() );
                setPage( PathPageId,       new TemplateDesignerPathPage() );
                setPage( ContinuousPageId, new TemplateDesignerContinuousPage() );
                setPage( NLayoutsPageId,   new TemplateDesignerNLayoutsPage() );
                setPage( OneLayoutPageId,  new TemplateDesignerOneLayoutPage() );
                setPage( TwoLayoutPageId,  new TemplateDesignerTwoLayoutPage() );
                setPage( ApplyPageId,      new TemplateDesignerApplyPage() );
        }


        ///
        /// Control wizard's non-linear page order
        ///
        int TemplateDesigner::nextId() const
        {
                switch (currentId())
                {

                case IntroPageId:
                        if ( mIsTemplatePathBased )
                        {
                                return PathPageId;
                        }
                        else if ( mIsTemplateContinuousBased )
                        {
                                return ContinuousPageId;
                        }
                        else
                        {
                                return NamePageId;
                        }

                case NamePageId:
                        return PageSizePageId;

                case PageSizePageId:
                        return ShapePageId;

                case ShapePageId:
                        if ( field( "shape.rect" ).toBool() )
                        {
                                return RectPageId;
                        }
                        else if ( field( "shape.round" ).toBool() )
                        {
                                return RoundPageId;
                        }
                        else if ( field( "shape.ellipse" ).toBool() )
                        {
                                return EllipsePageId;
                        }
                        else
                        {
                                return CdPageId;
                        }

                case RectPageId:
                        if ( field( "pageSize.pageSize" ) != tr("Roll") )
                        {
                                return NLayoutsPageId;
                        }
                        else
                        {
                                return OneLayoutPageId;
                        }

                case RoundPageId:
                        if ( field( "pageSize.pageSize" ) != tr("Roll") )
                        {
                                return NLayoutsPageId;
                        }
                        else
                        {
                                return OneLayoutPageId;
                        }

                case EllipsePageId:
                        if ( field( "pageSize.pageSize" ) != tr("Roll") )
                        {
                                return NLayoutsPageId;
                        }
                        else
                        {
                                return OneLayoutPageId;
                        }

                case CdPageId:
                        if ( field( "pageSize.pageSize" ) != tr("Roll") )
                        {
                                return NLayoutsPageId;
                        }
                        else
                        {
                                return OneLayoutPageId;
                        }

                case PathPageId:
                        return IntroPageId;

                case ContinuousPageId:
                        return IntroPageId;

                case NLayoutsPageId:
                        if ( field( "nLayouts.one" ).toBool() )
                        {
                                return OneLayoutPageId;
                        }
                        else
                        {
                                return TwoLayoutPageId;
                        }

                case OneLayoutPageId:
                        return ApplyPageId;

                case TwoLayoutPageId:
                        return ApplyPageId;

                case ApplyPageId:
                default:
                        return -1;
                }
        }


        ///
        /// Determine width of individual item
        ///
        double TemplateDesigner::itemWidth()
        {
                // Note: all distance units are the same in wizard, so no conversions needed
                if ( field( "shape.rect" ).toBool() )
                {
                        return field( "rect.w" ).toDouble();
                }
                else if ( field( "shape.round" ).toBool() )
                {
                        return 2 * field( "round.r" ).toDouble();
                }
                else if ( field( "shape.ellipse" ).toBool() )
                {
                        return field( "ellipse.w" ).toDouble();
                }
                else
                {
                        if ( field( "cd.xClip" ).toDouble() == 0 )
                        {
                                return 2 * field( "cd.r1" ).toDouble();
                        }
                        else
                        {
                                return field( "cd.xClip" ).toDouble();
                        }
                }
        }


        ///
        /// Determine height of individual item
        ///
        double TemplateDesigner::itemHeight()
        {
                // Note: all distance units are the same in wizard, so no conversions needed
                if ( field( "shape.rect" ).toBool() )
                {
                        return field( "rect.h" ).toDouble();
                }
                else if ( field( "shape.round" ).toBool() )
                {
                        return 2 * field( "round.r" ).toDouble();
                }
                else if ( field( "shape.ellipse" ).toBool() )
                {
                        return field( "ellipse.h" ).toDouble();
                }
                else
                {
                        if ( field( "cd.xClip" ).toDouble() == 0 )
                        {
                                return 2 * field( "cd.r1" ).toDouble();
                        }
                        else
                        {
                                return field( "cd.yClip" ).toDouble();
                        }
                }
        }


        ///
        /// Determine X Waste of individual item
        ///
        double TemplateDesigner::itemXWaste()
        {
                // Note: all distance units are the same in wizard, so no conversions needed
                if ( field( "shape.rect" ).toBool() )
                {
                        return field( "rect.xWaste" ).toDouble();
                }
                else if ( field( "shape.round" ).toBool() )
                {
                        return 2 * field( "round.waste" ).toDouble();
                }
                else if ( field( "shape.ellipse" ).toBool() )
                {
                        return field( "ellipse.waste" ).toDouble();
                }
                else
                {
                        return field( "cd.waste" ).toDouble();
                }
        }


        ///
        /// Determine Y Waste of individual item
        ///
        double TemplateDesigner::itemYWaste()
        {
                // Note: all distance units are the same in wizard, so no conversions needed
                if ( field( "shape.rect" ).toBool() )
                {
                        return field( "rect.yWaste" ).toDouble();
                }
                else if ( field( "shape.round" ).toBool() )
                {
                        return 2 * field( "round.waste" ).toDouble();
                }
                else if ( field( "shape.ellipse" ).toBool() )
                {
                        return field( "ellipse.waste" ).toDouble();
                }
                else
                {
                        return field( "cd.waste" ).toDouble();
                }
        }


        ///
        /// Build template from wizard pages
        ///
        model::Template TemplateDesigner::buildTemplate()
        {
                model::Units units = model::Settings::units();

                QString brand = field( "name.brand" ).toString();
                QString part = field( "name.part" ).toString();
                QString description = field( "name.description" ).toString();
                QString paperId = model::Db::lookupPaperIdFromName( field( "pageSize.pageSize" ).toString() );
                model::Distance pageW( field( "pageSize.w" ).toDouble(), units );
                model::Distance pageH( field( "pageSize.h" ).toDouble(), units );
                model::Distance pageRollW( field( "pageSize.rollW" ).toDouble(), units );

                auto t = model::Template( brand, part, description, paperId, pageW, pageH, pageRollW, "", true );

                if ( field( "shape.rect" ).toBool() )
                {
                        model::Distance w( field( "rect.w" ).toDouble(), units );
                        model::Distance h( field( "rect.h" ).toDouble(), units );
                        model::Distance r( field( "rect.r" ).toDouble(), units );
                        model::Distance xWaste( field( "rect.xWaste" ).toDouble(), units );
                        model::Distance yWaste( field( "rect.yWaste" ).toDouble(), units );
                        model::Distance xMargin( field( "rect.xMargin" ).toDouble(), units );
                        model::Distance yMargin( field( "rect.yMargin" ).toDouble(), units );

                        model::FrameRect frame( w, h, r, xWaste, yWaste );
                        frame.addMarkup( model::MarkupMargin( xMargin, yMargin ) );
                        addLayouts( frame );
                        t.addFrame( frame );
                }
                else if ( field( "shape.round" ).toBool() )
                {
                        model::Distance r( field( "round.r" ).toDouble(), units );
                        model::Distance waste( field( "round.waste" ).toDouble(), units );
                        model::Distance margin( field( "round.margin" ).toDouble(), units );

                        model::FrameRound frame( r, waste );
                        frame.addMarkup( model::MarkupMargin( margin ) );
                        addLayouts( frame );
                        t.addFrame( frame );
                }
                else if ( field( "shape.ellipse" ).toBool() )
                {
                        model::Distance w( field( "ellipse.w" ).toDouble(), units );
                        model::Distance h( field( "ellipse.h" ).toDouble(), units );
                        model::Distance waste( field( "ellipse.waste" ).toDouble(), units );
                        model::Distance margin( field( "ellipse.margin" ).toDouble(), units );

                        model::FrameEllipse frame( w, h, waste );
                        frame.addMarkup( model::MarkupMargin( margin ) );
                        addLayouts( frame );
                        t.addFrame( frame );
                }
                else
                {
                        model::Distance r1( field( "cd.r1" ).toDouble(), units );
                        model::Distance r2( field( "cd.r2" ).toDouble(), units );
                        model::Distance xClip( field( "cd.xClip" ).toDouble(), units );
                        model::Distance yClip( field( "cd.yClip" ).toDouble(), units );
                        model::Distance waste( field( "cd.waste" ).toDouble(), units );
                        model::Distance margin( field( "cd.margin" ).toDouble(), units );

                        model::FrameCd frame( r1, r2, xClip, yClip, waste );
                        frame.addMarkup( model::MarkupMargin( margin ) );
                        addLayouts( frame );
                        t.addFrame( frame );
                }

                return t;
        }


        ///
        /// Add layouts to frame
        ///
        void TemplateDesigner::addLayouts( model::Frame& frame )
        {
                model::Units units = model::Settings::units();

                if ( field( "nLayouts.one" ).toBool() )
                {
                        int nx = field( "oneLayout.nx" ).toInt();
                        int ny = field( "oneLayout.ny" ).toInt();
                        model::Distance x0( field( "oneLayout.x0" ).toDouble(), units );
                        model::Distance y0( field( "oneLayout.y0" ).toDouble(), units );
                        model::Distance dx( field( "oneLayout.dx" ).toDouble(), units );
                        model::Distance dy( field( "oneLayout.dy" ).toDouble(), units );

                        frame.addLayout( model::Layout( nx, ny, x0, y0, dx, dy ) );
                }
                else
                {
                        int nx1 = field( "twoLayout.nx1" ).toInt();
                        int ny1 = field( "twoLayout.ny1" ).toInt();
                        model::Distance x01( field( "twoLayout.x01" ).toDouble(), units );
                        model::Distance y01( field( "twoLayout.y01" ).toDouble(), units );
                        model::Distance dx1( field( "twoLayout.dx1" ).toDouble(), units );
                        model::Distance dy1( field( "twoLayout.dy1" ).toDouble(), units );

                        int nx2 = field( "twoLayout.nx2" ).toInt();
                        int ny2 = field( "twoLayout.ny2" ).toInt();
                        model::Distance x02( field( "twoLayout.x02" ).toDouble(), units );
                        model::Distance y02( field( "twoLayout.y02" ).toDouble(), units );
                        model::Distance dx2( field( "twoLayout.dx2" ).toDouble(), units );
                        model::Distance dy2( field( "twoLayout.dy2" ).toDouble(), units );

                        frame.addLayout( model::Layout( nx1, ny1, x01, y01, dx1, dy1 ) );
                        frame.addLayout( model::Layout( nx2, ny2, x02, y02, dx2, dy2 ) );
                }
        }


        ///
        /// Print test sheet
        ///
        void TemplateDesigner::printTestSheet()
        {
                auto sheet = new model::Model();
                sheet->setTmplate( buildTemplate() );

                model::PageRenderer renderer( sheet );
                renderer.setNCopies( sheet->frame()->nLabels() );
                renderer.setStartItem( 0 );
                renderer.setPrintOutlines( true );

                QPrinter printer( QPrinter::HighResolution );

                QPrintDialog printDialog( &printer, this );
                printDialog.setOption( QAbstractPrintDialog::PrintToFile,        true );
                printDialog.setOption( QAbstractPrintDialog::PrintSelection,     false );
                printDialog.setOption( QAbstractPrintDialog::PrintPageRange,     false );
                printDialog.setOption( QAbstractPrintDialog::PrintShowPageSize,  true );
                printDialog.setOption( QAbstractPrintDialog::PrintCollateCopies, false );
                printDialog.setOption( QAbstractPrintDialog::PrintCurrentPage,   false );

                if ( printDialog.exec() == QDialog::Accepted )
                {
                        renderer.print( &printer );
                }

                delete sheet;
        }


        ///
        /// Load wizard from template
        ///
        void TemplateDesigner::loadFromTemplate( const model::Template& tmplate )
        {
                mIsBasedOnCopy = true;

                model::Units units = model::Settings::units();

                setField( "name.brand",       tmplate.brand() );
                setField( "name.part",        tmplate.part() + QString(" (%1)").arg( tr("Copy") ) );
                setField( "name.description", tmplate.description() );

                setField( "pageSize.pageSize", model::Db::lookupPaperNameFromId( tmplate.paperId() ) );
                setField( "pageSize.w",        tmplate.pageWidth().inUnits( units ) );
                setField( "pageSize.h",        tmplate.pageHeight().inUnits( units ) );
                setField( "pageSize.rollW",    tmplate.rollWidth().inUnits( units ) );

                auto frame = tmplate.frame();
                if ( auto frameRect = dynamic_cast<const model::FrameRect*>( frame ) )
                {
                        setField( "shape.rect", true );

                        setField( "rect.w",      frameRect->w().inUnits( units ) );
                        setField( "rect.h",      frameRect->h().inUnits( units ) );
                        setField( "rect.r",      frameRect->r().inUnits( units ) );
                        setField( "rect.xWaste", frameRect->xWaste().inUnits( units ) );
                        setField( "rect.yWaste", frameRect->yWaste().inUnits( units ) );
                }
                else if ( auto frameRound = dynamic_cast<const model::FrameRound*>( frame ) )
                {
                        setField( "shape.round", true );

                        setField( "round.r",     frameRound->r().inUnits( units ) );
                        setField( "round.waste", frameRound->waste().inUnits( units ) );
                }
                else if ( auto frameEllipse = dynamic_cast<const model::FrameEllipse*>( frame ) )
                {
                        setField( "shape.ellipse", true );

                        setField( "ellipse.w",     frameEllipse->w().inUnits( units ) );
                        setField( "ellipse.h",     frameEllipse->h().inUnits( units ) );
                        setField( "ellipse.waste", frameEllipse->waste().inUnits( units ) );
                }
                else if ( auto frameCd = dynamic_cast<const model::FrameCd*>( frame ) )
                {
                        setField( "shape.cd", true );

                        setField( "cd.r1",    frameCd->r1().inUnits( units ) );
                        setField( "cd.r2",    frameCd->r2().inUnits( units ) );
                        setField( "cd.xClip", frameCd->w().inUnits( units ) );
                        setField( "cd.yClip", frameCd->h().inUnits( units ) );
                        setField( "cd.waste", frameCd->waste().inUnits( units ) );
                }

                for( auto& markup : frame->markups() )
                {
                        if ( auto markupMargin = dynamic_cast<const model::MarkupMargin*>( markup.get() ) )
                        {
                                setField( "rect.xMargin",   markupMargin->xSize().inUnits( units ) );
                                setField( "rect.yMargin",   markupMargin->ySize().inUnits( units ) );
                                setField( "round.margin",   markupMargin->xSize().inUnits( units ) );
                                setField( "ellipse.margin", markupMargin->xSize().inUnits( units ) );
                                setField( "cd.margin",      markupMargin->xSize().inUnits( units ) );
                        }
                }

                auto layouts = frame->layouts();
                if ( layouts.size() == 1 )
                {
                        setField( "oneLayout.nx", layouts.front().nx() );
                        setField( "oneLayout.ny", layouts.front().ny() );
                        setField( "oneLayout.x0", layouts.front().x0().inUnits( units ) );
                        setField( "oneLayout.y0", layouts.front().y0().inUnits( units ) );
                        setField( "oneLayout.dx", layouts.front().dx().inUnits( units ) );
                        setField( "oneLayout.dy", layouts.front().dy().inUnits( units ) );
                }
                else if ( layouts.size() > 1 )
                {
                        auto it = layouts.begin();
                        setField( "twoLayout.nx1", it->nx() );
                        setField( "twoLayout.ny1", it->ny() );
                        setField( "twoLayout.x01", it->x0().inUnits( units ) );
                        setField( "twoLayout.y01", it->y0().inUnits( units ) );
                        setField( "twoLayout.dx1", it->dx().inUnits( units ) );
                        setField( "twoLayout.dy1", it->dy().inUnits( units ) );

                        std::advance( it, 1 );
                        setField( "twoLayout.nx2", it->nx() );
                        setField( "twoLayout.ny2", it->ny() );
                        setField( "twoLayout.x02", it->x0().inUnits( units ) );
                        setField( "twoLayout.y02", it->y0().inUnits( units ) );
                        setField( "twoLayout.dx2", it->dx().inUnits( units ) );
                        setField( "twoLayout.dy2", it->dy().inUnits( units ) );
                }
        }


        ///
        /// Is the wizard based on a copy?
        ///
        bool TemplateDesigner::isBasedOnCopy()
        {
                return mIsBasedOnCopy;
        }


        ///
        /// Intro Page
        ///
        TemplateDesignerIntroPage::TemplateDesignerIntroPage( QWidget* parent ) : QWizardPage(parent)
        {
                setTitle( tr("Welcome") );
                setSubTitle( tr("Welcome to the gLabels Product Template Designer.") );
                setPixmap( QWizard::WatermarkPixmap, QPixmap( ":images/TemplateDesigner/wizard-banner.png" ) );

                QWidget* widget = new QWidget;
                setupUi( widget );

                connect( copyButton, &QCommandLinkButton::clicked, this, &TemplateDesignerIntroPage::onCopyButtonClicked );
                connect( newButton, &QCommandLinkButton::clicked, this, &TemplateDesignerIntroPage::onNewButtonClicked );

                QVBoxLayout* layout = new QVBoxLayout;
                layout->addWidget( widget );
                setLayout( layout );
        }


        bool TemplateDesignerIntroPage::isComplete() const
        {
                // Use CommandLinkButtons on intro page to advance
                return false;
        }


        void TemplateDesignerIntroPage::onCopyButtonClicked()
        {
                SelectProductDialog dialog;
                dialog.exec();

                auto tmplate = dialog.tmplate();
                if ( !tmplate.isNull() )
                {
                        if ( auto td = dynamic_cast<TemplateDesigner*>( wizard() ) )
                        {
                                if ( dynamic_cast<const model::FramePath*>(tmplate.frame()) )
                                {
                                        td->mIsTemplatePathBased = true;
                                }
                                else if ( dynamic_cast<const model::FrameContinuous*>(tmplate.frame()) )
                                {
                                        td->mIsTemplateContinuousBased = true;
                                }
                                else
                                {
                                        td->mIsTemplatePathBased = false;
                                        td->mIsTemplateContinuousBased = false;
                                        td->loadFromTemplate( tmplate );
                                }
                                td->next();
                        }
                }
        }


        void TemplateDesignerIntroPage::onNewButtonClicked()
        {
                wizard()->next();
        }


        ///
        /// Name and Description Page
        ///
        TemplateDesignerNamePage::TemplateDesignerNamePage( QWidget* parent ) : QWizardPage(parent)
        {
                setTitle( tr("Name and Description") );
                setSubTitle( tr("Please enter the following identifying information about the product.") );

                QWidget* widget = new QWidget;
                setupUi( widget );

                warningLabel->setText( "" );

                registerField( "name.brand",       brandEntry );
                registerField( "name.part",        partEntry );
                registerField( "name.description", descriptionEntry );

                connect( brandEntry, &QLineEdit::textChanged, this, &TemplateDesignerNamePage::onChanged );
                connect( partEntry, &QLineEdit::textChanged, this, &TemplateDesignerNamePage::onChanged );

                QVBoxLayout* layout = new QVBoxLayout;
                layout->addWidget( widget );
                setLayout( layout );
        }


        bool TemplateDesignerNamePage::isComplete() const
        {
                return mCanContinue;
        }


        void TemplateDesignerNamePage::onChanged()
        {
                bool isDuplicate = model::Db::isSystemTemplateKnown( brandEntry->text(), partEntry->text() );
                if ( isDuplicate )
                {
                        QString warningText = tr("Brand and part number match an existing built-in product template!");
                        warningLabel->setText( "<b style='color:red'>" + warningText + "</b>" );
                }
                else
                {
                        warningLabel->setText( "" );
                }

                mCanContinue = !brandEntry->text().isEmpty() && !partEntry->text().isEmpty() && !isDuplicate;
                emit completeChanged();
        }


        ///
        /// Page Size Page
        ///
        TemplateDesignerPageSizePage::TemplateDesignerPageSizePage( QWidget* parent ) : QWizardPage(parent)
        {
                setTitle( tr("Page Size") );
                setSubTitle( tr("Please select the product page size.") );

                QWidget* widget = new QWidget;
                setupUi( widget );

                pageSizeCombo->insertItem( 0, tr("Other") );
                pageSizeCombo->insertItem( 1, tr("Roll") );
                pageSizeCombo->insertItems( 2, model::Db::paperNames() );
                pageSizeCombo->setCurrentText( defaultPageSize[ model::Settings::preferedPageSizeFamily() ] );

                bool isOther = pageSizeCombo->currentText() == tr("Other");
                bool isRoll  = pageSizeCombo->currentText() == tr("Roll");

                wSpin->setSuffix( " " + model::Settings::units().toTrName() );
                wSpin->setDecimals( model::Settings::units().resolutionDigits() );
                wSpin->setSingleStep( model::Settings::units().resolution() );
                wSpin->setMaximum( maxPageSize[ model::Settings::units().toEnum() ] );
                wSpin->setEnabled( isOther || isRoll );

                hSpin->setSuffix( " " + model::Settings::units().toTrName() );
                hSpin->setDecimals( model::Settings::units().resolutionDigits() );
                hSpin->setSingleStep( model::Settings::units().resolution() );
                hSpin->setMaximum( maxPageSize[ model::Settings::units().toEnum() ] );
                hSpin->setEnabled( isOther || isRoll );

                rollWSpin->setSuffix( " " + model::Settings::units().toTrName() );
                rollWSpin->setDecimals( model::Settings::units().resolutionDigits() );
                rollWSpin->setSingleStep( model::Settings::units().resolution() );
                rollWSpin->setMaximum( maxPageSize[ model::Settings::units().toEnum() ] );
                rollWSpin->setEnabled( isRoll );

                if ( pageSizeCombo->currentText() != tr("Other") )
                {
                        auto paper = model::Db::lookupPaperFromName( pageSizeCombo->currentText() );
                        wSpin->setValue( paper.width().inUnits( model::Settings::units() ) );
                        hSpin->setValue( paper.height().inUnits( model::Settings::units() ) );
                }

                registerField( "pageSize.pageSize", pageSizeCombo, "currentText" );
                registerField( "pageSize.w",        wSpin,         "value" );
                registerField( "pageSize.h",        hSpin,         "value" );
                registerField( "pageSize.rollW",    rollWSpin,     "value" );

                connect( pageSizeCombo, &QComboBox::currentTextChanged, this, &TemplateDesignerPageSizePage::onComboChanged );

                QVBoxLayout* layout = new QVBoxLayout;
                layout->addWidget( widget );
                setLayout( layout );
        }


        void TemplateDesignerPageSizePage::initializePage()
        {
        }


        void TemplateDesignerPageSizePage::cleanupPage()
        {
                // Leave current settings alone
        }


        void TemplateDesignerPageSizePage::onComboChanged()
        {
                bool isOther = pageSizeCombo->currentText() == tr("Other");
                bool isRoll  = pageSizeCombo->currentText() == tr("Roll");

                if ( !isOther && !isRoll )
                {
                        auto paper = model::Db::lookupPaperFromName( pageSizeCombo->currentText() );
                        wSpin->setValue( paper.width().inUnits( model::Settings::units() ) );
                        hSpin->setValue( paper.height().inUnits( model::Settings::units() ) );
                }

                if ( !isRoll )
                {
                        rollWSpin->setValue( 0 );
                }

                wLabel->setEnabled( isOther || isRoll );
                wSpin->setEnabled( isOther || isRoll );

                hLabel->setEnabled( isOther || isRoll );
                hSpin->setEnabled( isOther || isRoll );

                rollWLabel->setEnabled( isRoll );
                rollWSpin->setEnabled( isRoll );
        }


        ///
        /// Shape Page
        ///
        TemplateDesignerShapePage::TemplateDesignerShapePage( QWidget* parent ) : QWizardPage(parent)
        {
                setTitle( tr("Product Shape") );
                setSubTitle( tr("Please select the basic product shape.") );

                QWidget* widget = new QWidget;
                setupUi( widget );

                registerField( "shape.rect",    rectRadio );
                registerField( "shape.round",   roundRadio );
                registerField( "shape.ellipse", ellipseRadio );
                registerField( "shape.cd",      cdRadio );

                QVBoxLayout* layout = new QVBoxLayout;
                layout->addWidget( widget );
                setLayout( layout );
        }


        void TemplateDesignerShapePage::initializePage()
        {
        }


        void TemplateDesignerShapePage::cleanupPage()
        {
                // Leave current settings alone
        }


        ///
        /// Rectangular Product Page
        ///
        TemplateDesignerRectPage::TemplateDesignerRectPage( QWidget* parent ) : QWizardPage(parent)
        {
                setTitle( tr("Product Size") );
                setSubTitle( tr("Please adjust the size parameters of a single product item.") );

                QWidget* widget = new QWidget;
                setupUi( widget );

                wSpin->setSuffix( " " + model::Settings::units().toTrName() );
                wSpin->setDecimals( model::Settings::units().resolutionDigits() );
                wSpin->setSingleStep( model::Settings::units().resolution() );

                hSpin->setSuffix( " " + model::Settings::units().toTrName() );
                hSpin->setDecimals( model::Settings::units().resolutionDigits() );
                hSpin->setSingleStep( model::Settings::units().resolution() );

                rSpin->setSuffix( " " + model::Settings::units().toTrName() );
                rSpin->setDecimals( model::Settings::units().resolutionDigits() );
                rSpin->setSingleStep( model::Settings::units().resolution() );

                xWasteSpin->setSuffix( " " + model::Settings::units().toTrName() );
                xWasteSpin->setDecimals( model::Settings::units().resolutionDigits() );
                xWasteSpin->setSingleStep( model::Settings::units().resolution() );

                yWasteSpin->setSuffix( " " + model::Settings::units().toTrName() );
                yWasteSpin->setDecimals( model::Settings::units().resolutionDigits() );
                yWasteSpin->setSingleStep( model::Settings::units().resolution() );

                xMarginSpin->setSuffix( " " + model::Settings::units().toTrName() );
                xMarginSpin->setDecimals( model::Settings::units().resolutionDigits() );
                xMarginSpin->setSingleStep( model::Settings::units().resolution() );

                yMarginSpin->setSuffix( " " + model::Settings::units().toTrName() );
                yMarginSpin->setDecimals( model::Settings::units().resolutionDigits() );
                yMarginSpin->setSingleStep( model::Settings::units().resolution() );

                registerField( "rect.w",       wSpin,      "value" );
                registerField( "rect.h",       hSpin,      "value" );
                registerField( "rect.r",       rSpin,      "value" );
                registerField( "rect.xWaste",  xWasteSpin, "value" );
                registerField( "rect.yWaste",  yWasteSpin, "value" );
                registerField( "rect.xMargin", xMarginSpin, "value" );
                registerField( "rect.yMargin", yMarginSpin, "value" );

                QVBoxLayout* layout = new QVBoxLayout;
                layout->addWidget( widget );
                setLayout( layout );
        }


        void TemplateDesignerRectPage::initializePage()
        {
                if ( auto td = dynamic_cast<TemplateDesigner*>( wizard() ) )
                {
                        // set realistic limits based on previously chosen page size
                        double wMax = field("pageSize.w").toDouble();
                        double hMax = field("pageSize.h").toDouble();

                        wSpin->setMaximum( wMax );
                        hSpin->setMaximum( hMax );
                        rSpin->setMaximum( std::min(wMax,hMax)/2.0 );
                        xWasteSpin->setMaximum( std::min(wMax,hMax)/4.0 );
                        yWasteSpin->setMaximum( std::min(wMax,hMax)/4.0 );
                        xMarginSpin->setMaximum( std::min(wMax,hMax)/4.0 );
                        yMarginSpin->setMaximum( std::min(wMax,hMax)/4.0 );

                        static bool alreadyInitialized = false;
                        if ( !td->isBasedOnCopy() && !alreadyInitialized )
                        {
                                alreadyInitialized = true;

                                // Set some realistic defaults
                                wSpin->setValue( defaultRectW.inUnits( model::Settings::units() ) );
                                hSpin->setValue( defaultRectH.inUnits( model::Settings::units() ) );
                                rSpin->setValue( defaultRectR.inUnits( model::Settings::units() ) );
                                xWasteSpin->setValue( defaultWaste.inUnits( model::Settings::units() ) );
                                yWasteSpin->setValue( defaultWaste.inUnits( model::Settings::units() ) );
                                xMarginSpin->setValue( defaultMargin.inUnits( model::Settings::units() ) );
                                yMarginSpin->setValue( defaultMargin.inUnits( model::Settings::units() ) );
                        }
                }
        }


        void TemplateDesignerRectPage::cleanupPage()
        {
                // Leave current settings alone
        }


        ///
        /// Round Product Page
        ///
        TemplateDesignerRoundPage::TemplateDesignerRoundPage( QWidget* parent ) : QWizardPage(parent)
        {
                setTitle( tr("Product Size") );
                setSubTitle( tr("Please adjust the size parameters of a single product item.") );

                QWidget* widget = new QWidget;
                setupUi( widget );

                rSpin->setSuffix( " " + model::Settings::units().toTrName() );
                rSpin->setDecimals( model::Settings::units().resolutionDigits() );
                rSpin->setSingleStep( model::Settings::units().resolution() );

                wasteSpin->setSuffix( " " + model::Settings::units().toTrName() );
                wasteSpin->setDecimals( model::Settings::units().resolutionDigits() );
                wasteSpin->setSingleStep( model::Settings::units().resolution() );

                marginSpin->setSuffix( " " + model::Settings::units().toTrName() );
                marginSpin->setDecimals( model::Settings::units().resolutionDigits() );
                marginSpin->setSingleStep( model::Settings::units().resolution() );

                registerField( "round.r",      rSpin,      "value" );
                registerField( "round.waste",  wasteSpin,  "value" );
                registerField( "round.margin", marginSpin, "value" );

                QVBoxLayout* layout = new QVBoxLayout;
                layout->addWidget( widget );
                setLayout( layout );
        }


        void TemplateDesignerRoundPage::initializePage()
        {
                if ( auto td = dynamic_cast<TemplateDesigner*>( wizard() ) )
                {
                        // set realistic limits based on previously chosen page size
                        double wMax = field("pageSize.w").toDouble();
                        double hMax = field("pageSize.h").toDouble();

                        rSpin->setMaximum( std::min(wMax,hMax)/2.0 );
                        wasteSpin->setMaximum( std::min(wMax,hMax)/4.0 );
                        marginSpin->setMaximum( std::min(wMax,hMax)/4.0 );

                        static bool alreadyInitialized = false;
                        if ( !td->isBasedOnCopy() && !alreadyInitialized )
                        {
                                alreadyInitialized = true;

                                // Set some realistic defaults
                                rSpin->setValue( defaultRoundR.inUnits( model::Settings::units() ) );
                                wasteSpin->setValue( defaultWaste.inUnits( model::Settings::units() ) );
                                marginSpin->setValue( defaultMargin.inUnits( model::Settings::units() ) );
                        }
                }
        }


        void TemplateDesignerRoundPage::cleanupPage()
        {
                // Leave current settings alone
        }


        ///
        /// Elliptical Product Page
        ///
        TemplateDesignerEllipsePage::TemplateDesignerEllipsePage( QWidget* parent ) : QWizardPage(parent)
        {
                setTitle( tr("Product Size") );
                setSubTitle( tr("Please adjust the size parameters of a single product item.") );

                QWidget* widget = new QWidget;
                setupUi( widget );

                wSpin->setSuffix( " " + model::Settings::units().toTrName() );
                wSpin->setDecimals( model::Settings::units().resolutionDigits() );
                wSpin->setSingleStep( model::Settings::units().resolution() );

                hSpin->setSuffix( " " + model::Settings::units().toTrName() );
                hSpin->setDecimals( model::Settings::units().resolutionDigits() );
                hSpin->setSingleStep( model::Settings::units().resolution() );

                wasteSpin->setSuffix( " " + model::Settings::units().toTrName() );
                wasteSpin->setDecimals( model::Settings::units().resolutionDigits() );
                wasteSpin->setSingleStep( model::Settings::units().resolution() );

                marginSpin->setSuffix( " " + model::Settings::units().toTrName() );
                marginSpin->setDecimals( model::Settings::units().resolutionDigits() );
                marginSpin->setSingleStep( model::Settings::units().resolution() );

                registerField( "ellipse.w",      wSpin,      "value" );
                registerField( "ellipse.h",      hSpin,      "value" );
                registerField( "ellipse.waste",  wasteSpin,  "value" );
                registerField( "ellipse.margin", marginSpin, "value" );

                QVBoxLayout* layout = new QVBoxLayout;
                layout->addWidget( widget );
                setLayout( layout );
        }


        void TemplateDesignerEllipsePage::initializePage()
        {
                if ( auto td = dynamic_cast<TemplateDesigner*>( wizard() ) )
                {
                        // set realistic limits based on previously chosen page size
                        double wMax = field("pageSize.w").toDouble();
                        double hMax = field("pageSize.h").toDouble();

                        wSpin->setMaximum( wMax );
                        hSpin->setMaximum( hMax );
                        wasteSpin->setMaximum( std::min(wMax,hMax)/4.0 );
                        marginSpin->setMaximum( std::min(wMax,hMax)/4.0 );

                        static bool alreadyInitialized = false;
                        if ( !td->isBasedOnCopy() && !alreadyInitialized )
                        {
                                alreadyInitialized = true;

                                // Set some realistic defaults
                                wSpin->setValue( defaultEllipseW.inUnits( model::Settings::units() ) );
                                hSpin->setValue( defaultEllipseH.inUnits( model::Settings::units() ) );
                                wasteSpin->setValue( defaultWaste.inUnits( model::Settings::units() ) );
                                marginSpin->setValue( defaultMargin.inUnits( model::Settings::units() ) );
                        }
                }
        }


        void TemplateDesignerEllipsePage::cleanupPage()
        {
                // Leave current settings alone
        }


        ///
        /// CD/DVD Product Page
        ///
        TemplateDesignerCdPage::TemplateDesignerCdPage( QWidget* parent ) : QWizardPage(parent)
        {
                setTitle( tr("Product Size") );
                setSubTitle( tr("Please adjust the size parameters of a single product item.") );

                QWidget* widget = new QWidget;
                setupUi( widget );

                r1Spin->setSuffix( " " + model::Settings::units().toTrName() );
                r1Spin->setDecimals( model::Settings::units().resolutionDigits() );
                r1Spin->setSingleStep( model::Settings::units().resolution() );

                r2Spin->setSuffix( " " + model::Settings::units().toTrName() );
                r2Spin->setDecimals( model::Settings::units().resolutionDigits() );
                r2Spin->setSingleStep( model::Settings::units().resolution() );

                xClipSpin->setSuffix( " " + model::Settings::units().toTrName() );
                xClipSpin->setDecimals( model::Settings::units().resolutionDigits() );
                xClipSpin->setSingleStep( model::Settings::units().resolution() );

                yClipSpin->setSuffix( " " + model::Settings::units().toTrName() );
                yClipSpin->setDecimals( model::Settings::units().resolutionDigits() );
                yClipSpin->setSingleStep( model::Settings::units().resolution() );

                wasteSpin->setSuffix( " " + model::Settings::units().toTrName() );
                wasteSpin->setDecimals( model::Settings::units().resolutionDigits() );
                wasteSpin->setSingleStep( model::Settings::units().resolution() );

                marginSpin->setSuffix( " " + model::Settings::units().toTrName() );
                marginSpin->setDecimals( model::Settings::units().resolutionDigits() );
                marginSpin->setSingleStep( model::Settings::units().resolution() );

                registerField( "cd.r1",     r1Spin,     "value" );
                registerField( "cd.r2",     r2Spin,     "value" );
                registerField( "cd.xClip",  xClipSpin,  "value" );
                registerField( "cd.yClip",  yClipSpin,  "value" );
                registerField( "cd.waste",  wasteSpin,  "value" );
                registerField( "cd.margin", marginSpin, "value" );

                QVBoxLayout* layout = new QVBoxLayout;
                layout->addWidget( widget );
                setLayout( layout );
        }


        void TemplateDesignerCdPage::initializePage()
        {
                if ( auto td = dynamic_cast<TemplateDesigner*>( wizard() ) )
                {
                        // set realistic limits based on previously chosen page size
                        double wMax = field("pageSize.w").toDouble();
                        double hMax = field("pageSize.h").toDouble();

                        r1Spin->setMaximum( std::min(wMax,hMax)/2.0 );
                        r2Spin->setMaximum( std::min(wMax,hMax)/4.0 );
                        xClipSpin->setMaximum( std::min(wMax,hMax)/4.0 );
                        yClipSpin->setMaximum( std::min(wMax,hMax)/4.0 );
                        wasteSpin->setMaximum( std::min(wMax,hMax)/4.0 );
                        marginSpin->setMaximum( std::min(wMax,hMax)/4.0 );

                        static bool alreadyInitialized = false;
                        if ( !td->isBasedOnCopy() && !alreadyInitialized )
                        {
                                alreadyInitialized = true;

                                // Set some realistic defaults
                                r1Spin->setValue( defaultCdR1.inUnits( model::Settings::units() ) );
                                r2Spin->setValue( defaultCdR2.inUnits( model::Settings::units() ) );
                                xClipSpin->setValue( defaultCdClip.inUnits( model::Settings::units() ) );
                                yClipSpin->setValue( defaultCdClip.inUnits( model::Settings::units() ) );
                                wasteSpin->setValue( defaultWaste.inUnits( model::Settings::units() ) );
                                marginSpin->setValue( defaultMargin.inUnits( model::Settings::units() ) );
                        }
                }
        }


        void TemplateDesignerCdPage::cleanupPage()
        {
                // Leave current settings alone
        }


        ///
        /// Path Product Page
        ///
        TemplateDesignerPathPage::TemplateDesignerPathPage( QWidget* parent ) : QWizardPage(parent)
        {
                setTitle( tr("Unsupported Product Style") );
                setSubTitle( tr("Path based product templates are not currently supported by the Product Template Designer.") );

                QWidget* widget = new QWidget;
                setupUi( widget );

                QVBoxLayout* layout = new QVBoxLayout;
                layout->addWidget( widget );
                setLayout( layout );
        }


        bool TemplateDesignerPathPage::isComplete() const
        {
                // Must "Cancel" or "Back" from this page
                return false;
        }


        ///
        /// Continuous Product Page
        ///
        TemplateDesignerContinuousPage::TemplateDesignerContinuousPage( QWidget* parent ) : QWizardPage(parent)
        {
                setTitle( tr("Unsupported Product Style") );
                setSubTitle( tr("Continuous tape product templates are not currently supported by the Product Template Designer.") );

                QWidget* widget = new QWidget;
                setupUi( widget );

                QVBoxLayout* layout = new QVBoxLayout;
                layout->addWidget( widget );
                setLayout( layout );
        }


        bool TemplateDesignerContinuousPage::isComplete() const
        {
                // Must "Cancel" or "Back" from this page
                return false;
        }


        ///
        /// Number of Layouts Page
        ///
        TemplateDesignerNLayoutsPage::TemplateDesignerNLayoutsPage( QWidget* parent ) : QWizardPage(parent)
        {
                setTitle( tr("Number of Layouts") );
                setSubTitle( tr("Please select the number of layouts required.") );

                QWidget* widget = new QWidget;
                setupUi( widget );

                registerField( "nLayouts.one", oneLayoutRadio );
                registerField( "nLayouts.two", twoLayoutsRadio );

                QVBoxLayout* layout = new QVBoxLayout;
                layout->addWidget( widget );
                setLayout( layout );
        }


        void TemplateDesignerNLayoutsPage::initializePage()
        {
        }


        void TemplateDesignerNLayoutsPage::cleanupPage()
        {
                // Leave current settings alone
        }


        ///
        /// One Layout Page
        ///
        TemplateDesignerOneLayoutPage::TemplateDesignerOneLayoutPage( QWidget* parent ) : QWizardPage(parent)
        {
                setTitle( tr("Layout") );
                setSubTitle( tr("Please enter parameters for your single layout.") );

                QWidget* widget = new QWidget;
                setupUi( widget );

                x0Spin->setSuffix( " " + model::Settings::units().toTrName() );
                x0Spin->setDecimals( model::Settings::units().resolutionDigits() );
                x0Spin->setSingleStep( model::Settings::units().resolution() );

                y0Spin->setSuffix( " " + model::Settings::units().toTrName() );
                y0Spin->setDecimals( model::Settings::units().resolutionDigits() );
                y0Spin->setSingleStep( model::Settings::units().resolution() );

                dxSpin->setSuffix( " " + model::Settings::units().toTrName() );
                dxSpin->setDecimals( model::Settings::units().resolutionDigits() );
                dxSpin->setSingleStep( model::Settings::units().resolution() );

                dySpin->setSuffix( " " + model::Settings::units().toTrName() );
                dySpin->setDecimals( model::Settings::units().resolutionDigits() );
                dySpin->setSingleStep( model::Settings::units().resolution() );

                registerField( "oneLayout.nx", nxSpin );
                registerField( "oneLayout.ny", nySpin );
                registerField( "oneLayout.x0", x0Spin, "value" );
                registerField( "oneLayout.y0", y0Spin, "value" );
                registerField( "oneLayout.dx", dxSpin, "value" );
                registerField( "oneLayout.dy", dySpin, "value" );

                connect( nxSpin, SIGNAL(valueChanged(int)), this, SLOT(onChanged()) );
                connect( nySpin, SIGNAL(valueChanged(int)), this, SLOT(onChanged()) );
                connect( x0Spin, SIGNAL(valueChanged(double)), this, SLOT(onChanged()) );
                connect( y0Spin, SIGNAL(valueChanged(double)), this, SLOT(onChanged()) );
                connect( dxSpin, SIGNAL(valueChanged(double)), this, SLOT(onChanged()) );
                connect( dySpin, SIGNAL(valueChanged(double)), this, SLOT(onChanged()) );

                connect( printButton, SIGNAL(clicked()), this, SLOT(onPrintButtonClicked()) );

                QVBoxLayout* layout = new QVBoxLayout;
                layout->addWidget( widget );
                setLayout( layout );
        }


        void TemplateDesignerOneLayoutPage::initializePage()
        {
                if ( auto td = dynamic_cast<TemplateDesigner*>( wizard() ) )
                {
                        // set realistic limits based on previously chosen values
                        double pageW = field("pageSize.w").toDouble();
                        double pageH = field("pageSize.h").toDouble();
                        double w = td->itemWidth();
                        double h = td->itemHeight();
                        double xWaste = td->itemXWaste();
                        double yWaste = td->itemYWaste();

                        int    nxMax = std::max( pageW/(w + 2*xWaste), 1.0 );
                        int    nyMax = std::max( pageH/(h + 2*yWaste), 1.0 );
                        double x0Min = xWaste;
                        double x0Max = pageW - w - 2*xWaste;
                        double y0Min = yWaste;
                        double y0Max = pageH - h - 2*yWaste;
                        double dxMin = w + 2*xWaste;
                        double dxMax = pageW - w - 2*xWaste;
                        double dyMin = h + 2*yWaste;
                        double dyMax = pageH - h - 2*yWaste;

                        nxSpin->setRange( 1, nxMax );
                        nySpin->setRange( 1, nyMax );
                        x0Spin->setRange( x0Min, x0Max );
                        y0Spin->setRange( y0Min, y0Max );
                        dxSpin->setRange( dxMin, dxMax );
                        dySpin->setRange( dyMin, dyMax );

                        static bool alreadyInitialized = false;
                        if ( !td->isBasedOnCopy() && !alreadyInitialized )
                        {
                                alreadyInitialized = true;

                                // Set some realistic defaults based on symmetric sheet using previously chosen values
                                nxSpin->setValue( nxMax );
                                nySpin->setValue( nyMax );
                                x0Spin->setValue( (pageW - (nxMax-1)*dxMin - w) / 2 );
                                y0Spin->setValue( (pageH - (nyMax-1)*dyMin - h) / 2 );
                                dxSpin->setValue( dxMin );
                                dySpin->setValue( dyMin );
                        }

                        preview->setTemplate( td->buildTemplate() );
                }
        }


        void TemplateDesignerOneLayoutPage::cleanupPage()
        {
                // Leave current settings alone
        }


        void TemplateDesignerOneLayoutPage::onChanged()
        {
                if ( auto td = dynamic_cast<TemplateDesigner*>( wizard() ) )
                {
                        preview->setTemplate( td->buildTemplate() );
                }
        }


        void TemplateDesignerOneLayoutPage::onPrintButtonClicked()
        {
                if ( auto td = dynamic_cast<TemplateDesigner*>( wizard() ) )
                {
                        td->printTestSheet();
                }
        }


        ///
        /// Two Layout Page
        ///
        TemplateDesignerTwoLayoutPage::TemplateDesignerTwoLayoutPage( QWidget* parent ) : QWizardPage(parent)
        {
                setTitle( tr("Layouts") );
                setSubTitle( tr("Please enter parameters for your two layouts.") );

                QWidget* widget = new QWidget;
                setupUi( widget );

                x0Spin1->setSuffix( " " + model::Settings::units().toTrName() );
                x0Spin1->setDecimals( model::Settings::units().resolutionDigits() );
                x0Spin1->setSingleStep( model::Settings::units().resolution() );

                y0Spin1->setSuffix( " " + model::Settings::units().toTrName() );
                y0Spin1->setDecimals( model::Settings::units().resolutionDigits() );
                y0Spin1->setSingleStep( model::Settings::units().resolution() );

                dxSpin1->setSuffix( " " + model::Settings::units().toTrName() );
                dxSpin1->setDecimals( model::Settings::units().resolutionDigits() );
                dxSpin1->setSingleStep( model::Settings::units().resolution() );

                dySpin1->setSuffix( " " + model::Settings::units().toTrName() );
                dySpin1->setDecimals( model::Settings::units().resolutionDigits() );
                dySpin1->setSingleStep( model::Settings::units().resolution() );

                x0Spin2->setSuffix( " " + model::Settings::units().toTrName() );
                x0Spin2->setDecimals( model::Settings::units().resolutionDigits() );
                x0Spin2->setSingleStep( model::Settings::units().resolution() );

                y0Spin2->setSuffix( " " + model::Settings::units().toTrName() );
                y0Spin2->setDecimals( model::Settings::units().resolutionDigits() );
                y0Spin2->setSingleStep( model::Settings::units().resolution() );

                dxSpin2->setSuffix( " " + model::Settings::units().toTrName() );
                dxSpin2->setDecimals( model::Settings::units().resolutionDigits() );
                dxSpin2->setSingleStep( model::Settings::units().resolution() );

                dySpin2->setSuffix( " " + model::Settings::units().toTrName() );
                dySpin2->setDecimals( model::Settings::units().resolutionDigits() );
                dySpin2->setSingleStep( model::Settings::units().resolution() );

                registerField( "twoLayout.nx1", nxSpin1 );
                registerField( "twoLayout.ny1", nySpin1 );
                registerField( "twoLayout.x01", x0Spin1, "value" );
                registerField( "twoLayout.y01", y0Spin1, "value" );
                registerField( "twoLayout.dx1", dxSpin1, "value" );
                registerField( "twoLayout.dy1", dySpin1, "value" );

                registerField( "twoLayout.nx2", nxSpin2 );
                registerField( "twoLayout.ny2", nySpin2 );
                registerField( "twoLayout.x02", x0Spin2, "value" );
                registerField( "twoLayout.y02", y0Spin2, "value" );
                registerField( "twoLayout.dx2", dxSpin2, "value" );
                registerField( "twoLayout.dy2", dySpin2, "value" );

                connect( nxSpin1, SIGNAL(valueChanged(int)), this, SLOT(onChanged()) );
                connect( nySpin1, SIGNAL(valueChanged(int)), this, SLOT(onChanged()) );
                connect( x0Spin1, SIGNAL(valueChanged(double)), this, SLOT(onChanged()) );
                connect( y0Spin1, SIGNAL(valueChanged(double)), this, SLOT(onChanged()) );
                connect( dxSpin1, SIGNAL(valueChanged(double)), this, SLOT(onChanged()) );
                connect( dySpin1, SIGNAL(valueChanged(double)), this, SLOT(onChanged()) );

                connect( nxSpin2, SIGNAL(valueChanged(int)), this, SLOT(onChanged()) );
                connect( nySpin2, SIGNAL(valueChanged(int)), this, SLOT(onChanged()) );
                connect( x0Spin2, SIGNAL(valueChanged(double)), this, SLOT(onChanged()) );
                connect( y0Spin2, SIGNAL(valueChanged(double)), this, SLOT(onChanged()) );
                connect( dxSpin2, SIGNAL(valueChanged(double)), this, SLOT(onChanged()) );
                connect( dySpin2, SIGNAL(valueChanged(double)), this, SLOT(onChanged()) );

                connect( printButton, SIGNAL(clicked()), this, SLOT(onPrintButtonClicked()) );

                QVBoxLayout* layout = new QVBoxLayout;
                layout->addWidget( widget );
                setLayout( layout );
        }


        void TemplateDesignerTwoLayoutPage::initializePage()
        {
                if ( auto td = dynamic_cast<TemplateDesigner*>( wizard() ) )
                {
                        // set realistic limits based on previously chosen values
                        double pageW = field("pageSize.w").toDouble();
                        double pageH = field("pageSize.h").toDouble();
                        double w = td->itemWidth();
                        double h = td->itemHeight();
                        double xWaste = td->itemXWaste();
                        double yWaste = td->itemYWaste();

                        int    nxMax = std::max( pageW/(w + 2*xWaste), 1.0 );
                        int    nyMax = std::max( pageH/(h + 2*yWaste), 1.0 );
                        double x0Min = xWaste;
                        double x0Max = pageW - w - 2*xWaste;
                        double y0Min = yWaste;
                        double y0Max = pageH - h - 2*yWaste;
                        double dxMin = w + 2*xWaste;
                        double dxMax = pageW - w - 2*xWaste;
                        double dyMin = h + 2*yWaste;
                        double dyMax = pageH - h - 2*yWaste;

                        nxSpin1->setRange( 1, nxMax );
                        nySpin1->setRange( 1, nyMax );
                        x0Spin1->setRange( x0Min, x0Max );
                        y0Spin1->setRange( y0Min, y0Max );
                        dxSpin1->setRange( dxMin, dxMax );
                        dySpin1->setRange( dyMin, dyMax );

                        nxSpin2->setRange( 1, nxMax );
                        nySpin2->setRange( 1, nyMax );
                        x0Spin2->setRange( x0Min, x0Max );
                        y0Spin2->setRange( y0Min, y0Max );
                        dxSpin2->setRange( dxMin, dxMax );
                        dySpin2->setRange( dyMin, dyMax );

                        static bool alreadyInitialized = false;
                        if ( !td->isBasedOnCopy() && !alreadyInitialized )
                        {
                                alreadyInitialized = true;

                                // Set some realistic defaults based on symmetric sheet using previously chosen values
                                nxSpin1->setValue( nxMax );
                                nySpin1->setValue( nyMax - nyMax/2 );
                                x0Spin1->setValue( (pageW - (nxMax-1)*dxMin - w) / 2 );
                                y0Spin1->setValue( (pageH - (nyMax-1)*dyMin - h) / 2 );
                                dxSpin1->setValue( dxMin );
                                dySpin1->setValue( 2*dyMin );

                                nxSpin2->setValue( nxMax );
                                nySpin2->setValue( nyMax/2 );
                                x0Spin2->setValue( (pageW - (nxMax-1)*dxMin - w) / 2 );
                                y0Spin2->setValue( (pageH - (nyMax-1)*dyMin - h) / 2 + dyMin );
                                dxSpin2->setValue( dxMin );
                                dySpin2->setValue( 2*dyMin );
                        }

                        preview->setTemplate( td->buildTemplate() );
                }
        }


        void TemplateDesignerTwoLayoutPage::cleanupPage()
        {
                // Leave current settings alone
        }


        void TemplateDesignerTwoLayoutPage::onChanged()
        {
                if ( auto td = dynamic_cast<TemplateDesigner*>( wizard() ) )
                {
                        preview->setTemplate( td->buildTemplate() );
                }
        }


        void TemplateDesignerTwoLayoutPage::onPrintButtonClicked()
        {
                if ( auto td = dynamic_cast<TemplateDesigner*>( wizard() ) )
                {
                        td->printTestSheet();
                }
        }


        ///
        /// Apply Page
        ///
        TemplateDesignerApplyPage::TemplateDesignerApplyPage( QWidget* parent ) : QWizardPage(parent)
        {
                setTitle( tr("Save Product Template") );
                setSubTitle( tr("Click \"Save\" to save your custom product template!") );

                setFinalPage( true );
                setButtonText( QWizard::FinishButton, "Save" );

                QWidget* widget = new QWidget;
                setupUi( widget );

                QVBoxLayout* layout = new QVBoxLayout;
                layout->addWidget( widget );
                setLayout( layout );
        }


        bool TemplateDesignerApplyPage::validatePage()
        {
                //
                // Save button pressed
                //
                QString brand = field( "name.brand" ).toString();
                QString part = field( "name.part" ).toString();
                QString filename = model::Db::userTemplateFileName( brand, part );

                if ( QFileInfo::exists(filename) )
                {
                        QMessageBox msgBox( wizard() );
                        msgBox.setWindowTitle( tr("Save Product Template") );
                        msgBox.setIcon( QMessageBox::Warning );
                        // TRANSLATORS
                        //:   %1 = brand name of product (e.g. Avery), %2 = part number of product (e.g. 5026).
                        msgBox.setText( tr("User product template (%1 %2) already exists.").arg(brand).arg(part) );
                        msgBox.setInformativeText( tr("Do you want to replace it?") );
                        msgBox.setStandardButtons( QMessageBox::Yes | QMessageBox::No );
                        msgBox.setDefaultButton( QMessageBox::No );

                        if ( msgBox.exec() == QMessageBox::No )
                        {
                                return false;
                        }

                        model::Db::deleteUserTemplateByBrandPart( brand, part );
                }

                if ( auto td = dynamic_cast<TemplateDesigner*>( wizard() ) )
                {
                        model::Db::registerUserTemplate( td->buildTemplate() );
                }
                return true;
        }


} // namespace glabels
