15.1.670. tablet_qt/tasks/demoquestionnaire.cpp

/*
    Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
    Created by Rudolf Cardinal (rnc1001@cam.ac.uk).

    This file is part of CamCOPS.

    CamCOPS 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.

    CamCOPS 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 CamCOPS. If not, see <https://www.gnu.org/licenses/>.
*/

// #define DEBUG_BIG_HEADER_ONLY_PAGE
// #define DEBUG_DIAGNOSTIC_SET_CREATION_SPEED
// #define DEBUG_DISABLE_MOST_SLIDERS

#include "demoquestionnaire.h"
#include "core/camcopsapp.h"
#include "common/textconst.h"
#include "common/uiconst.h"
#include "diagnosis/icd10.h"
#include "diagnosis/icd9cm.h"
#include "lib/uifunc.h"
#include "lib/stringfunc.h"
#include "tasklib/taskfactory.h"
#include "tasklib/taskregistrar.h"

#include "questionnairelib/questionnaire.h"

#include "questionnairelib/qupage.h"
#include "questionnairelib/quelement.h"

#include "questionnairelib/quflowcontainer.h"
#include "questionnairelib/quhorizontalcontainer.h"
#include "questionnairelib/quverticalcontainer.h"
#include "questionnairelib/qugridcontainer.h"

#include "questionnairelib/quaudioplayer.h"
#include "questionnairelib/quboolean.h"
#include "questionnairelib/qubutton.h"
#include "questionnairelib/qucanvas.h"
#include "questionnairelib/qucountdown.h"
#include "questionnairelib/qudatetime.h"
#include "questionnairelib/qudiagnosticcode.h"
#include "questionnairelib/questionnairefunc.h"
#include "questionnairelib/qugridcell.h"
#include "questionnairelib/quheading.h"
#include "questionnairelib/quhorizontalline.h"
#include "questionnairelib/quimage.h"
#include "questionnairelib/qulineedit.h"
#include "questionnairelib/qulineeditdouble.h"
#include "questionnairelib/qulineeditinteger.h"
#include "questionnairelib/qumcq.h"
#include "questionnairelib/qumcqgrid.h"
#include "questionnairelib/qumcqgriddouble.h"
#include "questionnairelib/qumcqgridsingleboolean.h"
#include "questionnairelib/qumultipleresponse.h"
#include "questionnairelib/quphoto.h"
#include "questionnairelib/qupickerinline.h"
#include "questionnairelib/qupickerpopup.h"
#include "questionnairelib/quslider.h"
#include "questionnairelib/quspacer.h"
#include "questionnairelib/quspinboxdouble.h"
#include "questionnairelib/quspinboxinteger.h"
#include "questionnairelib/qutext.h"
#include "questionnairelib/qutextedit.h"
#include "questionnairelib/quthermometer.h"

const QString DemoQuestionnaire::DEMOQUESTIONNAIRE_TABLENAME(QStringLiteral("demoquestionnaire"));

const int SOUNDTEST_1_VOLUME = 50;


void initializeDemoQuestionnaire(TaskFactory& factory)
{
    static TaskRegistrar<DemoQuestionnaire> registered(factory);
}


DemoQuestionnaire::DemoQuestionnaire(CamcopsApp& app,
                                     DatabaseManager& db, const int load_pk) :
    Task(app, db, DEMOQUESTIONNAIRE_TABLENAME, true, false, false)
{
    using stringfunc::strseq;

    addFields(strseq(QStringLiteral("mcq"), 1, 10), QMetaType::fromType<int>());  // 9-10: v2
    addFields(strseq(QStringLiteral("mcqbool"), 1, 3), QMetaType::fromType<bool>());
    addFields(strseq(QStringLiteral("multipleresponse"), 1, 6), QMetaType::fromType<bool>());
    addFields(strseq(QStringLiteral("booltext"), 1, 22), QMetaType::fromType<bool>());
    addFields(strseq(QStringLiteral("boolimage"), 1, 2), QMetaType::fromType<bool>());
    addFields(strseq(QStringLiteral("slider"), 1, 2), QMetaType::fromType<double>());
    addFields(strseq(QStringLiteral("picker"), 1, 2), QMetaType::fromType<int>());
    addFields(strseq(QStringLiteral("mcqtext_"), 1, 3, {"a", "b"}), QMetaType::fromType<QString>());
    addField(QStringLiteral("typedvar_text"), QMetaType::fromType<QString>());
    addField(QStringLiteral("typedvar_text_multiline"), QMetaType::fromType<QString>());
    addField(QStringLiteral("typedvar_text_rich"), QMetaType::fromType<QString>());  // v2
    addField(QStringLiteral("typedvar_int"), QMetaType::fromType<int>());
    addField(QStringLiteral("typedvar_real"), QMetaType::fromType<double>());
    addField(QStringLiteral("spinbox_int"), QMetaType::fromType<int>());  // v2
    addField(QStringLiteral("spinbox_real"), QMetaType::fromType<double>());  // v2
    addField(QStringLiteral("date_time"), QMetaType::fromType<QDateTime>());
    addField(QStringLiteral("date_only"), QMetaType::fromType<QDate>());
    addField(QStringLiteral("time_only"), QMetaType::fromType<QTime>());  // v2
    addField(QStringLiteral("thermometer"), QMetaType::fromType<int>());
    addField(QStringLiteral("diagnosticcode_code"), QMetaType::fromType<QString>());
    addField(QStringLiteral("diagnosticcode_description"), QMetaType::fromType<QString>());
    addField(QStringLiteral("diagnosticcode2_code"), QMetaType::fromType<QString>());  // v2
    addField(QStringLiteral("diagnosticcode2_description"), QMetaType::fromType<QString>());  // v2
    addField(QStringLiteral("photo_blobid"), QMetaType::fromType<int>());  // FK to BLOB table
    // addField(QStringLiteral("photo_rotation"), QMetaType::fromType<QString>());  // DEFUNCT in v2
    addField(QStringLiteral("canvas_blobid"), QMetaType::fromType<int>());  // FK to BLOB table
    addField(QStringLiteral("canvas2_blobid"), QMetaType::fromType<int>());  // FK to BLOB table; v2

    load(load_pk);  // MUST ALWAYS CALL from derived Task constructor.
}


// ============================================================================
// Class overrides
// ============================================================================

QString DemoQuestionnaire::shortname() const
{
    return QStringLiteral("Demo");
}


QString DemoQuestionnaire::longname() const
{
    return tr("Demonstration task");
}


QString DemoQuestionnaire::description() const
{
    return tr("Tutorial and illustration of questionnaire task elements.");
}


// ============================================================================
// Instance overrides
// ============================================================================

bool DemoQuestionnaire::isComplete() const
{
    return true;
}


QStringList DemoQuestionnaire::summary() const
{
    return QStringList{tr("Demonstration questionnaire; no summary")};
}


OpenableWidget* DemoQuestionnaire::editor(const bool read_only)
{
    const QString& longtext = TextConst::LOREM_IPSUM_1;
    const QString& lipsum2 = TextConst::LOREM_IPSUM_2;
    const QString url(QStringLiteral("https://doc.qt.io/qt-6.5/richtext-html-subset.html"));
    const QString html(tr(
        "Text with embedded HTML markup, providing <b>bold</b>, "
        "<i>italic</i>, and others as per Qt rich text syntax at "
        "<a href=\"%1\">%1</a>."
    ).arg(url));

    // ------------------------------------------------------------------------
    // Text
    // ------------------------------------------------------------------------

    QuPagePtr page_text((new QuPage{
        new QuText(tr("We’ll demonstrate the elements from which questionnaire"
                      " tasks can be made. Press the ‘Next’ button at the top "
                      "right of the screen.") + "\n"),
        (new QuText(tr("normal text")))->addTag(QStringLiteral("tag1")),
        (new QuText(tr("bold text")))->setBold(),
        (new QuText(tr("italic text")))->setItalic(),
        (new QuText(html))->setOpenLinks(),
        (new QuText(tr("big text")))->setBig(),
        (new QuText(tr("warning text")))->setWarning(),
        new QuText(tr("Below here: space fillers, just to test scrolling")),
        (new QuText(longtext))->setBig(),
    })->setTitle(tr("Text [With a long title: %1]")
                 .arg(TextConst::LOREM_IPSUM_3)));
    for (int i = 0; i < 20; ++i) {
        page_text->addElement((new QuText(tr("big text")))->setBig());
    }
    page_text->addElement(
        (new QuText(tr("... was that enough to scroll vertically?")))->setBold()
    );

#ifdef DEBUG_BIG_HEADER_ONLY_PAGE
    QuPagePtr page_text_header_only((new QuPage{
        new QuText(tr("Very long title, to check sizing.")),
    })->setTitle(textconst::LOREM_IPSUM_1));
#endif

    // ------------------------------------------------------------------------
    // Image
    // ------------------------------------------------------------------------

    QuPagePtr page_image((new QuPage{
        new QuHeading(tr("Image:")),
        new QuImage(uifunc::iconFilename(uiconst::ICON_CAMCOPS)),
        new QuHeading(tr("... heading under image, to check vertical size")),
    })->setTitle(tr("Headings, images")));

    // ------------------------------------------------------------------------
    // Headings, containers, text alignment, lines
    // ------------------------------------------------------------------------

    const Qt::Alignment bottomleft = Qt::AlignLeft | Qt::AlignBottom;
    const Qt::Alignment centre = Qt::AlignHCenter | Qt::AlignVCenter;
    const Qt::Alignment topright = Qt::AlignRight | Qt::AlignTop;

    auto horiz1 = new QuHorizontalContainer();
    horiz1->addElement((new QuText(tr("Text 1 (right/top)")))->setTextAndWidgetAlignment(topright));
    horiz1->addElement((new QuText(tr("Text 2 (centre/vcentre)")))->setTextAndWidgetAlignment(centre));
    horiz1->addElement((new QuText(tr("Text 3 (left/bottom)")))->setTextAndWidgetAlignment(bottomleft));
    horiz1->addElement(new QuText(tr("Text 4: ") + longtext));
    horiz1->setOverrideWidgetAlignment(false);
    horiz1->setAddStretchRight(false);

    auto vert1 = new QuVerticalContainer;
    vert1->addElement((new QuText(tr("Text 1 (right/top)")))->setTextAndWidgetAlignment(topright));
    vert1->addElement((new QuText(tr("Text 2 (centre/vcentre)")))->setTextAndWidgetAlignment(centre));
    vert1->addElement((new QuText(tr("Text 3 (left/bottom)")))->setTextAndWidgetAlignment(bottomleft));
    vert1->addElement(new QuText(tr("Text 4: ") + lipsum2));
    vert1->setOverrideWidgetAlignment(false);

    QuPagePtr page_headings_layout((new QuPage{
        new QuHeading(tr("This is a heading")),
        new QuHeading(tr("Horizontal line, line, spacer, line:")),
        new QuHorizontalLine(),
        new QuHorizontalLine(),
        new QuSpacer(),
        new QuHorizontalLine(),
        new QuHeading(tr("Flow container (generally preferred to horizontal "
                         "container; better on small screens):")),
        new QuFlowContainer{
            (new QuText(tr("Text 1 (right/top)")))->setTextAlignment(topright),
            (new QuText(tr("Text 2 (centre/vcentre)")))->setTextAlignment(centre),
            (new QuText(tr("Text 3 (left/bottom)")))->setTextAlignment(bottomleft),
            new QuText(tr("Text 4: ") + lipsum2),
        },
        new QuHeading(tr("Horizontal container (with stretch on right):")),
        (new QuHorizontalContainer{
            new QuText(tr("Text 1")),
            new QuText(tr("Text 2")),
            new QuText(tr("Text 3")),
        })->setAddStretchRight(true),
        new QuHeading(tr("Horizontal container (without stretch on right; blank widget alignment):")),
        (new QuHorizontalContainer{
            (new QuText(tr("Text 1 (right/top)")))->setTextAlignment(topright),
            (new QuText(tr("Text 2 (centre/vcentre)")))->setTextAlignment(centre),
            (new QuText(tr("Text 3 (left/bottom)")))->setTextAlignment(bottomleft),
        })->setAddStretchRight(false)->setContainedWidgetAlignments(Qt::Alignment()),
        new QuHeading(tr("Horizontal container (no stretch on right, showing alignments):")),
        horiz1,
        new QuHeading(tr("Vertical container:")),
        vert1,
        new QuHeading(tr("Grid container:")),
        new QuGridContainer{
            QuGridCell(new QuText("<b>row 0, col 0:</b> " + lipsum2), 0, 0),
            QuGridCell(new QuText("<b>row 0, col 1 [+1]:</b> " + lipsum2), 0, 1, 1, 2),
            QuGridCell(new QuText("<b>row 1, col 0 [+1]:</b> " + lipsum2), 1, 0, 1, 2),
            QuGridCell(new QuText(
                "<b>row 1 [+1], col 2, with top-right alignment:</b> " + lipsum2),
                1, 2, 2, 1,
                Qt::AlignRight | Qt::AlignTop),
            QuGridCell(new QuText("<b>row 2, col 0:</b> " + lipsum2), 2, 0),
            QuGridCell(new QuText("<b>row 2, col 1:</b> " + lipsum2), 2, 1),
        },
        new QuHeading(tr("Another grid (2:1 columns):")),
        (new QuGridContainer{
            QuGridCell(new QuText("<b>r0 c0</b> " + lipsum2), 0, 0, 1, 1),
            QuGridCell(new QuText("<b>r0 c1 [+1]</b> " + lipsum2), 0, 1, 1, 2),
            QuGridCell(new QuText("<b>r1 c0</b> " + lipsum2), 1, 0, 1, 1),
            QuGridCell(new QuText("<b>r1 c1 [+1]</b> " + lipsum2), 1, 1, 1, 2),
        })
            ->setColumnStretch(0, 2)
            ->setColumnStretch(1, 1),
        new QuHeading(tr("Another grid (5 equal columns), with image alignment settings (L/T, HC/VC, R/B):")),
        (new QuGridContainer{
            QuGridCell(new QuImage(uifunc::iconFilename(uiconst::ICON_CAMCOPS)),
                       0, 0, 1, 1, Qt::AlignLeft | Qt::AlignTop),
            QuGridCell(new QuText(lipsum2 + lipsum2 + lipsum2 + lipsum2),
                       0, 1, 1, 1),
            QuGridCell(new QuImage(uifunc::iconFilename(uiconst::ICON_CAMCOPS)),
                       0, 2, 1, 1, Qt::AlignHCenter | Qt::AlignVCenter),
            QuGridCell(new QuText(lipsum2 + lipsum2 + lipsum2 + lipsum2),
                       0, 3, 1, 1),
            QuGridCell(new QuImage(uifunc::iconFilename(uiconst::ICON_CAMCOPS)),
                       0, 4, 1, 1, Qt::AlignRight | Qt::AlignBottom),
        })
            ->setColumnStretch(0, 1)
            ->setColumnStretch(1, 1)
            ->setColumnStretch(2, 1)
            ->setColumnStretch(3, 1)
            ->setColumnStretch(4, 1),
        new QuHeading(tr("Another grid (1:1 columns):")),
        (new QuGridContainer{
            QuGridCell(new QuText("<b>r0 c0</b> " + lipsum2), 0, 0),
            QuGridCell(new QuText("<b>r0 c1</b> " + lipsum2), 0, 1),
            QuGridCell(new QuText("<b>r1 c0</b> " + lipsum2), 1, 0),
            QuGridCell(new QuText("<b>r1 c1</b> " + lipsum2), 1, 1),
        })
            ->setColumnStretch(0, 1)
            ->setColumnStretch(1, 1),
        new QuHeading(tr("Another grid (1:1:1 columns, expanding horizontally, "
                         "fixed column style = default):")),
        (new QuGridContainer{
            QuGridCell(new QuText(tr("1. Short")), 0, 0),
            QuGridCell(new QuText(tr("2. Medium sort of length")), 0, 1),
            QuGridCell(new QuText(tr("3. Longer ") + lipsum2), 0, 2),
        })
            ->setColumnStretch(0, 1)
            ->setColumnStretch(1, 1)
            ->setColumnStretch(2, 1)
            ->setExpandHorizontally(true)
            ->setFixedGrid(true),
        new QuHeading(tr("Another grid (1:1:1 columns, non-expanding, "
                         "non-fixed style):")),
        (new QuGridContainer{
            QuGridCell(new QuText(tr("1. Short")), 0, 0),
            QuGridCell(new QuText(tr("2. Medium sort of length")), 0, 1),
            QuGridCell(new QuText(tr("3. Longer ") + lipsum2), 0, 2),
        })
            ->setColumnStretch(0, 1)
            ->setColumnStretch(1, 1)
            ->setColumnStretch(2, 1)
            ->setExpandHorizontally(false)
            ->setFixedGrid(false),
        new QuHeading(tr("More automated grid (of label/element pairs):")),
        questionnairefunc::defaultGridRawPointer({
            {"<b>LHS:</b> " + lipsum2,
             new QuText("<b>RHS:</b> " + lipsum2)},
            {"<b>LHS:</b> " + lipsum2,
             new QuText("<b>RHS:</b> " + lipsum2)},
            {"<b>LHS:</b> " + lipsum2,
             new QuText("<b>RHS:</b> " + lipsum2)},
        }),
    })->setTitle(tr("Headings, containers, text alignment, lines")));

    // ------------------------------------------------------------------------
    // Audio players, countdown
    // ------------------------------------------------------------------------

    QuPagePtr page_audio_countdown((new QuPage{
        new QuHeading(tr("Simple audio player:")),
        new QuText(tr(
            "Excerpt from Mozart WA, <i>Vesperae solennes de confessore</i> "
            "(K.339), fifth movement, <i>Laudate Dominum</i>, by the Advent "
            "Chamber Orchestra (see docs).")),
        (new QuAudioPlayer(uiconst::DEMO_SOUND_URL_2))->setVolume(SOUNDTEST_1_VOLUME),
        new QuHeading(tr("Audio player with volume control:")),
        new QuText(tr(
            "Excerpt from Bach JS, <i>Brandenburg Concerto No. 3, third "
            "movement (Allegro)</i>, by the Advent Chamber Orchestra "
            "(see docs).")),
        (new QuAudioPlayer(uiconst::DEMO_SOUND_URL_1))->setOfferVolumeControl(),
        new QuHeading(tr("Countdown:")),
        new QuCountdown(20),
    })->setTitle(tr("Audio players, countdowns")));

    // ------------------------------------------------------------------------
    // Boolean
    // ------------------------------------------------------------------------

    const QString booltext1(QStringLiteral("booltext1"));
    const QString boolimage1(QStringLiteral("boolimage1"));
    QuPagePtr page_boolean((new QuPage{
        new QuText(tr(
            "On this page, some questions must be completed before the ‘Next’ "
            "button appears. <b>Make the yellow disappear to continue!</b>")),
        new QuHeading(tr("Boolean text, not allowing ‘unset’, with clickable "
                         "content:")),
        new QuBoolean(tr("Click me to toggle (null → true → false → true → …)"),
                      fieldRef(booltext1)),
        new QuHeading(tr("Boolean text, allowing ‘unset’, on the "
                         "<i>same</i> field, with a smaller icon, "
                         "and non-clickable content:")),
        (new QuBoolean(tr("Click me (null → true → false → null → …)"),
                      fieldRef(booltext1)))
                      ->setBigIndicator(false)
                      ->setAllowUnset()
                      ->setContentClickable(false),
        new QuHeading(tr("Same field, with text-style widget:")),
        (new QuBoolean(tr("Boolean-as-text"),
                      fieldRef(booltext1)))
                      ->setAsTextButton(),
        new QuHeading(tr("Text field from the Boolean field used above:")),
        new QuText(fieldRef(booltext1)),
        new QuHeading(tr("Another boolean field, using an image:")),
        new QuBoolean(uifunc::iconFilename(uiconst::ICON_CAMCOPS),
                      QSize(), fieldRef(boolimage1)),
        new QuHeading(tr("... clone with non-clickable image:")),
        (new QuBoolean(uifunc::iconFilename(uiconst::ICON_CAMCOPS),
                      QSize(), fieldRef(boolimage1)))->setContentClickable(false),
        // Now the ACE-III address example:
        new QuGridContainer{
            QuGridCell(new QuVerticalContainer{
                new QuFlowContainer{
                    aceBoolean(QStringLiteral("address_1"), QStringLiteral("booltext2")),
                    aceBoolean(QStringLiteral("address_2"), QStringLiteral("booltext3")),
                },
                new QuFlowContainer{
                    aceBoolean(QStringLiteral("address_3"), QStringLiteral("booltext4")),
                    aceBoolean(QStringLiteral("address_4"), QStringLiteral("booltext5")),
                    aceBoolean(QStringLiteral("address_5"), QStringLiteral("booltext6")),
                },
                aceBoolean(QStringLiteral("address_6"), QStringLiteral("booltext7")),
                aceBoolean(QStringLiteral("address_7"), QStringLiteral("booltext8")),
            }, 0, 0),
            QuGridCell(new QuVerticalContainer{
                new QuFlowContainer{
                    aceBoolean(QStringLiteral("address_1"), QStringLiteral("booltext9")),
                    aceBoolean(QStringLiteral("address_2"), QStringLiteral("booltext10")),
                },
                new QuFlowContainer{
                    aceBoolean(QStringLiteral("address_3"), QStringLiteral("booltext11")),
                    aceBoolean(QStringLiteral("address_4"), QStringLiteral("booltext12")),
                    aceBoolean(QStringLiteral("address_5"), QStringLiteral("booltext13")),
                },
                aceBoolean(QStringLiteral("address_6"), QStringLiteral("booltext14")),
                aceBoolean(QStringLiteral("address_7"), QStringLiteral("booltext15")),
            }, 0, 1),
            QuGridCell(new QuVerticalContainer{
                new QuFlowContainer{
                    aceBoolean(QStringLiteral("address_1"), QStringLiteral("booltext16")),
                    aceBoolean(QStringLiteral("address_2"), QStringLiteral("booltext17")),
                },
                new QuFlowContainer{
                    aceBoolean(QStringLiteral("address_3"), QStringLiteral("booltext18")),
                    aceBoolean(QStringLiteral("address_4"), QStringLiteral("booltext19")),
                    aceBoolean(QStringLiteral("address_5"), QStringLiteral("booltext20")),
                },
                aceBoolean(QStringLiteral("address_6"), QStringLiteral("booltext21")),
                aceBoolean(QStringLiteral("address_7"), QStringLiteral("booltext22")),
            }, 1, 0),
        },
        (new QuBoolean(
            uifunc::resourceFilename("ace3/penguin.png"),
            QSize(),
            fieldRef(QStringLiteral("boolimage2"))
        ))->setBigIndicator(false),

    })->setTitle(tr("Booleans; multiple views on a single field")));

    // ------------------------------------------------------------------------
    // MCQ
    // ------------------------------------------------------------------------

    const QString soption(tr("option"));
    auto makeoptn = [&soption](int n) {
        return QString(QStringLiteral("%1_%2")).arg(soption, QString::number(n));
    };
    const NameValueOptions options_A{
        {makeoptn(1), 1},
        {makeoptn(2), 2},
        {makeoptn(3) + ", " + tr("with much longer text: ") + longtext, 3},
    };
    const NameValueOptions options_B{
        {makeoptn(1), 1},
        {makeoptn(2), 2},
        {makeoptn(3), 3},
        {makeoptn(4), 4},
        {makeoptn(5), 5},
        {makeoptn(6), 6},
        {makeoptn(7), 7},
        {makeoptn(8), 8},
        {makeoptn(9), 9},
        {makeoptn(10), 10},
        {makeoptn(11), 11},
        {makeoptn(12), 12},
        {makeoptn(13), 13},
        {makeoptn(14), 14},
        {makeoptn(15), 15},
        {makeoptn(16), 16},
        {makeoptn(17), 17},
    };
    const NameValueOptions options_C{
        {makeoptn(1), 1},
        {makeoptn(2), 2},
        // {"option_NULL", QVariant()},  // will assert
        {makeoptn(99), 99},
    };
    const NameValueOptions options_D{
        {tr("Not at all"), 0},
        {tr("Several days"), 1},
        {tr("More than half the days"), 2},
        {tr("Nearly every day"), 3},
    };
    const NameValueOptions options_E{
        {"A", "A"},
        {"B", "B"},
        {"C", "C"},
        {"D", "D"},
    };
    const NameValueOptions options_F{
        {"X", "X"},
        {"Y", "Y"},
        {"Z", "Z"},
    };
    const NameValueOptions options_G{
        {tr("%1 (Red)").arg(makeoptn(1)), 1},
        {tr("%1 (Yellow)").arg(makeoptn(2)), 2},
        {tr("%1 (Green)").arg(makeoptn(3)), 3}
    };


    QStringList mcq_styles = {"color:white; background-color:red;",
                              "color:black; background-color:yellow;",
                              "color:white; background-color:green;"};

    QuPagePtr page_mcq((new QuPage{
        new QuHeading(tr("Plain MCQ:")),
        new QuMcq(fieldRef(QStringLiteral("mcq1")), options_A),
        new QuHeading(tr("Same MCQ/field, reconfigured (randomized, "
                         "instructions, horizontal, as text button):")),
        (new QuMcq(fieldRef(QStringLiteral("mcq1")), options_A))
                            ->setRandomize(true)
                            ->setShowInstruction(true)
                            ->setHorizontal(true)
                            ->setAsTextButton(true),
        new QuHeading(tr("Same MCQ/field, reconfigured:")),
        (new QuMcq(fieldRef(QStringLiteral("mcq1")), options_A))
                            ->setAsTextButton(true),
        new QuHeading(tr("Same MCQ/field, randomized with styles:")),
            (new QuMcq(fieldRef(QStringLiteral("mcq1")), options_G, &mcq_styles))
                   ->setRandomize(true),
        new QuHeading(tr("A second MCQ:")),
        new QuMcq(fieldRef(QStringLiteral("mcq2")), options_C),
        new QuHeading(tr("Another:")),
        new QuMcq(fieldRef(QStringLiteral("mcq3")), options_B),
        new QuHeading(tr("The previous MCQ, reconfigured:")),
        (new QuMcq(fieldRef(QStringLiteral("mcq3")), options_B))
                            ->setHorizontal(true),
        new QuHeading(tr("A fourth MCQ, as text:")),
        (new QuMcq(fieldRef(QStringLiteral("mcq4")), options_B))
                            ->setHorizontal(true)
                            ->setAsTextButton(true),
    })->setTitle(tr("Multiple-choice questions (MCQs)")));

    // ------------------------------------------------------------------------
    // MCQ variants
    // ------------------------------------------------------------------------

    const QString squestion(tr("Question"));
    QuPagePtr page_mcq_variants((new QuPage{
         new QuHeading(tr("MCQ grid:")),
         (new QuMcqGrid(
            {
                QuestionWithOneField(squestion + " A", fieldRef(QStringLiteral("mcq5"))),
                QuestionWithOneField(squestion + " B", fieldRef(QStringLiteral("mcq6"))),
                QuestionWithOneField(squestion + " C", fieldRef(QStringLiteral("mcq7"))),
                QuestionWithOneField(squestion + " D (= A)", fieldRef(QStringLiteral("mcq5"))),
                QuestionWithOneField(squestion + " E (= B)", fieldRef(QStringLiteral("mcq6"))),
            },
            options_D
        ))->setSubtitles({{3, tr("subtitle before D")}}),
        new QuHeading(tr("Another MCQ grid:")),
        (new QuMcqGrid(
            {
                QuestionWithOneField(squestion + " A", fieldRef(QStringLiteral("mcq8"))),
                QuestionWithOneField(squestion + " B; " + lipsum2, fieldRef(QStringLiteral("mcq9"))),
                QuestionWithOneField(squestion + " C", fieldRef(QStringLiteral("mcq10"))),
            },
        options_A
        ))->setTitle(tr("MCQ 2 title; ") + lipsum2),
        new QuHeading(tr("Double MCQ grid:")),
        (new QuMcqGridDouble(
            {
                QuestionWithTwoFields(squestion + " A",
                                         fieldRef(QStringLiteral("mcqtext_1a")),
                                         fieldRef(QStringLiteral("mcqtext_1b"))),
                QuestionWithTwoFields(squestion + " B; " + lipsum2,
                                         fieldRef(QStringLiteral("mcqtext_2a")),
                                         fieldRef(QStringLiteral("mcqtext_2b"))),
                QuestionWithTwoFields(squestion + " C",
                                         fieldRef(QStringLiteral("mcqtext_3a")),
                                         fieldRef(QStringLiteral("mcqtext_3b"))),
            },
        options_E, options_F
        ))  ->setTitle(tr("Double-MCQ title"))
            ->setSubtitles({{2, tr("subtitle before C")}}),
        new QuHeading(tr("MCQ grid with single Boolean (right):")),
        (new QuMcqGridSingleBoolean(
            {
                QuestionWithTwoFields(squestion + " A",
                                         fieldRef(QStringLiteral("mcq5")), fieldRef(QStringLiteral("mcqbool1"))),
                QuestionWithTwoFields(squestion + " B; " + lipsum2,
                                         fieldRef(QStringLiteral("mcq6")), fieldRef(QStringLiteral("mcqbool2"))),
                QuestionWithTwoFields(squestion + " C",
                                         fieldRef(QStringLiteral("mcq7")), fieldRef(QStringLiteral("mcqbool3"))),
            },
            options_D,
            tr("Happy?")
        ))  ->setTitle(tr("Title for MCQ grid with single boolean"))
            ->setSubtitles({{2, tr("subtitle before C")}}),
        new QuHeading(tr("MCQ grid with single Boolean (left):")),
        (new QuMcqGridSingleBoolean(
            {
                QuestionWithTwoFields(squestion + " A",
                                         fieldRef(QStringLiteral("mcq5")), fieldRef(QStringLiteral("mcqbool1"))),
                QuestionWithTwoFields(squestion + " B; " + lipsum2,
                                         fieldRef(QStringLiteral("mcq6")), fieldRef(QStringLiteral("mcqbool2"))),
                QuestionWithTwoFields(squestion + " C",
                                         fieldRef(QStringLiteral("mcq7")), fieldRef(QStringLiteral("mcqbool3"))),
            },
            options_D,
            tr("Happy?")
        ))  ->setTitle(tr("Title for MCQ grid with single boolean"))
            ->setBooleanLeft(true),
    })->setTitle(tr("MCQ variants")));

    // ------------------------------------------------------------------------
    // Multiple responses
    // ------------------------------------------------------------------------

    QuPagePtr page_multiple_response((new QuPage{
        new QuHeading(tr("Standard n-from-many format:")),
        (new QuMultipleResponse({
            QuestionWithOneField(fieldRef(QStringLiteral("multipleresponse1")), tr("(a) First stem")),
            QuestionWithOneField(fieldRef(QStringLiteral("multipleresponse2")), tr("(b) Second stem")),
            QuestionWithOneField(fieldRef(QStringLiteral("multipleresponse3")), tr("(c) Third stem")),
            QuestionWithOneField(fieldRef(QStringLiteral("multipleresponse4")), tr("(d) Fourth stem")),
            QuestionWithOneField(fieldRef(QStringLiteral("multipleresponse5")), tr("(e) Fifth stem")),
            QuestionWithOneField(fieldRef(QStringLiteral("multipleresponse6")), tr("(f) Sixth stem")),
        }))->setMinimumAnswers(2)->setMaximumAnswers(3),
        new QuHeading(tr("With instructions off, horizontally, and text-button style:")),
        (new QuMultipleResponse({
            QuestionWithOneField(fieldRef(QStringLiteral("multipleresponse1")), tr("(a) First stem")),
            QuestionWithOneField(fieldRef(QStringLiteral("multipleresponse2")), tr("(b) Second stem")),
            QuestionWithOneField(fieldRef(QStringLiteral("multipleresponse3")), tr("(c) Third stem")),
            QuestionWithOneField(fieldRef(QStringLiteral("multipleresponse4")), tr("(d) Fourth stem")),
            QuestionWithOneField(fieldRef(QStringLiteral("multipleresponse5")), tr("(e) Fifth stem")),
            QuestionWithOneField(fieldRef(QStringLiteral("multipleresponse6")), tr("(f) Sixth stem")),
        }))->setMinimumAnswers(2)
            ->setMaximumAnswers(3)
            ->setShowInstruction(false)
            ->setHorizontal(true)
            ->setAsTextButton(true),
    })->setTitle(tr("Multiple-response questions")));

    // ------------------------------------------------------------------------
    // Pickers
    // ------------------------------------------------------------------------

    FieldRefPtr fr_picker1 = fieldRef(QStringLiteral("picker1"));
    FieldRefPtr fr_picker2 = fieldRef(QStringLiteral("picker2"));
    QuPagePtr page_pickers((new QuPage{
        new QuHeading(tr("Inline picker:")),
        new QuPickerInline(fr_picker1 , options_A),
        new QuHeading(tr("Its clone:")),
        new QuPickerInline(fr_picker1 , options_A),
        new QuHeading(tr("Its clone, randomized:")),
        (new QuPickerInline(fr_picker1 , options_A))->setRandomize(true),
        new QuHeading(tr("Popup picker:")),
        (new QuPickerPopup(fr_picker2, options_A))
                                ->setPopupTitle(tr("Pickers; question 5")),
        new QuHeading(tr("Its clone, randomized:")),
        (new QuPickerPopup(fr_picker2, options_A))->setRandomize(true)
    })->setTitle(tr("Pickers")));

    // ------------------------------------------------------------------------
    // Sliders, thermometer
    // ------------------------------------------------------------------------

    QVector<QuThermometerItem> thermometer_items;
    for (int i = 0; i <= 10; ++i) {
        QString text = QString::number(i);
        if (i == 10) {
            text += tr(" - very distressed");
        } else if (i == 0) {
            text += tr(" - chilled out");
        }
        QuThermometerItem item(
            uifunc::resourceFilename(
                QString(QStringLiteral("distressthermometer/dt_sel_%1.png"))
                        .arg(i)
            ),
            uifunc::resourceFilename(
                QString(QStringLiteral("distressthermometer/dt_unsel_%1.png"))
                        .arg(i)
            ),
            text,
            i
        );
        thermometer_items.append(item);
    }

    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    // Likert-style slider
    // See example in creating_tasks.rst
    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    const QString ROSES_FIELDNAME(QStringLiteral("thermometer"));
    const int STRONGLY_DISAGREE = 1;
    const int DISAGREE = 2;
    const int NEUTRAL = 3;
    const int AGREE = 4;
    const int STRONGLY_AGREE = 5;

    // --------------------------------------------------------------------
    // Question
    // --------------------------------------------------------------------

#ifndef DEBUG_DISABLE_MOST_SLIDERS
    auto rose_q = new QuText(tr("Roses are best when red."));
#endif

    // --------------------------------------------------------------------
    // Likert-style slider
    // --------------------------------------------------------------------
    // Create the horizontal slider
    QuSlider* likert_slider = new QuSlider(
        fieldRef(ROSES_FIELDNAME), STRONGLY_DISAGREE, STRONGLY_AGREE, 1);
    likert_slider->setHorizontal(true);
    likert_slider->setBigStep(1);

    // Ticks for every interval, above and below
    likert_slider->setTickInterval(1);
    likert_slider->setTickPosition(QSlider::TicksBothSides);

    // Labels
    likert_slider->setTickLabels({
        {STRONGLY_DISAGREE, tr("Strongly\ndisagree")},  // or an xstring()
        {DISAGREE, tr("Disagree")},
        {NEUTRAL, tr("Neutral")},
        {AGREE, tr("Agree")},
        {STRONGLY_AGREE, tr("Strongly\nagree")},
    });
    likert_slider->setTickLabelPosition(QSlider::TicksAbove);

    // Don't show the numerical value
    likert_slider->setShowValue(false);

    // Symmetry
    likert_slider->setNullApparentValue(NEUTRAL);
    likert_slider->setSymmetric(true);

    // --------------------------------------------------------------------
    // Grid to improve layout
    // --------------------------------------------------------------------
    // Prevent the edge labels overflowing the screen, without having to
    // use setEdgeInExtremeLabels(true) (which might distort the scale).

    const int MARGIN_WIDTH = 15;  // each side
    const int LIKERT_WIDTH = 70;
    auto likert_slider_grid = new QuGridContainer();
    likert_slider_grid->setColumnStretch(0, MARGIN_WIDTH);
    likert_slider_grid->setColumnStretch(1, LIKERT_WIDTH);
    likert_slider_grid->setColumnStretch(2, MARGIN_WIDTH);
    likert_slider_grid->addCell(QuGridCell(likert_slider, 0, 1));

    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    // VAS-style slider
    // See example in creating_tasks.rst
    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    const QString VAS_FIELDNAME(QStringLiteral("slider1"));
    const int VAS_MIN_INT = 0;  // the internal integer minimum
    const int VAS_CENTRAL_INT = 500;  // centre, for initial display
    const int VAS_MAX_INT = 1000;  // the internal integer maximum
    const double VAS_MIN = 0.0;  // the database/display minimum
    const double VAS_MAX = 1.0;  // the database/display maximum
    const int VAS_DISPLAY_DP = 3;
    const int VAS_ABSOLUTE_SIZE_CM = 10.0;
    const bool VAS_CAN_SHRINK = true;

    // --------------------------------------------------------------------
    // VAS-style slider
    // --------------------------------------------------------------------

    // Create the horizontal slider
    QuSlider* vas_slider = new QuSlider(
        fieldRef(VAS_FIELDNAME), VAS_MIN_INT, VAS_MAX_INT, 1);
    vas_slider->setConvertForRealField(true, VAS_MIN, VAS_MAX, VAS_DISPLAY_DP);
    vas_slider->setHorizontal(true);
    vas_slider->setBigStep(1);

    // Ticks just at the extremes
    vas_slider->setTickInterval(VAS_MAX_INT);
    vas_slider->setTickPosition(QSlider::TickPosition::TicksBothSides);

    // Labels
    vas_slider->setTickLabels({
        {VAS_MIN_INT, QString::number(VAS_MIN)},  // or whatever
        {VAS_MAX_INT, QString::number(VAS_MAX)},
    });
    vas_slider->setTickLabelPosition(QSlider::TickPosition::TicksAbove);

    // Show the numerical value
    vas_slider->setShowValue(true);

    // Symmetry
    vas_slider->setNullApparentValue(VAS_CENTRAL_INT);
    vas_slider->setSymmetric(true);
    vas_slider->setEdgeInExtremeLabels(false);

    // Absolute size, if absolutely required (beware small screens -- you
    // may want the can_shrink parameter to be true for those; if the
    // screen is too small, the slider goes below the specified absolute
    // size).
    vas_slider->setAbsoluteLengthCm(VAS_ABSOLUTE_SIZE_CM, VAS_CAN_SHRINK);

    // --------------------------------------------------------------------
    // VAS-style slider -- a vertical version
    // --------------------------------------------------------------------

    QuSlider* vas_slider2 = new QuSlider(
        fieldRef(VAS_FIELDNAME), VAS_MIN_INT, VAS_MAX_INT, 1);
    vas_slider2->setConvertForRealField(true, VAS_MIN, VAS_MAX, VAS_DISPLAY_DP);
    vas_slider2->setHorizontal(false);
    vas_slider2->setBigStep(1);

    vas_slider2->setTickInterval(VAS_MAX_INT);
    vas_slider2->setTickPosition(QSlider::TickPosition::TicksBothSides);

    vas_slider2->setTickLabels({
        {VAS_MIN_INT, QString::number(VAS_MIN)},  // or whatever
        {VAS_MAX_INT, QString::number(VAS_MAX)},
    });
    vas_slider2->setTickLabelPosition(QSlider::TickPosition::TicksAbove);

    vas_slider2->setShowValue(true);

    vas_slider2->setNullApparentValue(VAS_CENTRAL_INT);
    vas_slider2->setSymmetric(true);
    vas_slider2->setEdgeInExtremeLabels(false);

    vas_slider2->setAbsoluteLengthCm(VAS_ABSOLUTE_SIZE_CM, VAS_CAN_SHRINK);

    const QString vas_description = tr(
        "Slider is set to %1 cm; can_shrink = %2"
    ).arg(QString::number(VAS_ABSOLUTE_SIZE_CM),
          uifunc::trueFalse(VAS_CAN_SHRINK));

    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    // End of those examples. On to the page...
    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    FieldRefPtr fr_thermometer = fieldRef(QStringLiteral("thermometer"));
    FieldRefPtr fr_slider1 = fieldRef(QStringLiteral("slider1"));
    FieldRefPtr fr_slider2 = fieldRef(QStringLiteral("slider2"));
    QuPagePtr page_sliders((new QuPage{
#ifndef DEBUG_DISABLE_MOST_SLIDERS
        new QuHeading(tr("Integer slider:")),
        (new QuSlider(fr_thermometer, 0, 10, 1))
                                ->setTickInterval(1)
                                ->setTickPosition(QSlider::TicksBothSides)
                                ->setShowValue(true),
#endif
        new QuHeading(tr("Integer slider (same field as above), vertical")),
        (new QuSlider(fr_thermometer, 0, 10, 1))
                                ->setShowValue(true)
                                ->setTickInterval(2)
                                ->setTickPosition(QSlider::TicksBothSides)
                                ->setUseDefaultTickLabels(true)
                                ->setTickLabelPosition(QSlider::TicksBothSides)
                                ->setHorizontal(false),
#ifndef DEBUG_DISABLE_MOST_SLIDERS
        new QuHeading(tr("Real number/floating-point slider:")),
        (new QuSlider(fr_slider1, 0, 10, 1))
                                ->setShowValue(true)
                                ->setTickInterval(1)
                                ->setTickPosition(QSlider::TicksBelow)
                                ->setConvertForRealField(true, 0, 1),
        new QuHeading(tr("Real number slider with custom labels (edging in extreme labels):")),
        (new QuSlider(fr_slider2, 100, 500, 1))
                                ->setConvertForRealField(true, 1, 5)
                                ->setShowValue(false)
                                ->setTickInterval(1)
                                ->setTickPosition(QSlider::TicksAbove)
                                ->setTickLabelPosition(QSlider::TicksBelow)
                                ->setTickLabels({
                                    {100, "one: low"},
                                    {300, "three: medium"},
                                    {500, "five: maximum!"},
                                })
                                ->setShowValue(true)
                                ->setEdgeInExtremeLabels(true),
        new QuHeading(tr("Real number slider with custom labels (standard labels):")),
        (new QuSlider(fr_slider2, 100, 500, 1))
                                ->setConvertForRealField(true, 1, 5)
                                ->setShowValue(false)
                                ->setTickInterval(1)
                                // ->setTickPosition(QSlider::NoTicks)
                                // ->setTickPosition(QSlider::TicksAbove)
                                // ->setTickPosition(QSlider::TicksBelow)
                                ->setTickPosition(QSlider::TicksBothSides)
                                // ->setTickLabelPosition(QSlider::NoTicks)
                                // ->setTickLabelPosition(QSlider::TicksAbove)
                                // ->setTickLabelPosition(QSlider::TicksBelow)
                                ->setTickLabelPosition(QSlider::TicksBothSides)
                                ->setTickLabels({
                                    {100, tr("one: low")},
                                    {300, tr("three: medium")},
                                    {500, tr("five: maximum!")},
                                })
                                ->setShowValue(true),
        new QuHeading(tr("Thermometer:")),
        (new QuThermometer(fr_thermometer, thermometer_items))
                                ->setRescale(true, 0.4),
        new QuHeading(tr("Likert-style (discrete) slider, in grid (70% of window width)")),
        rose_q,
        likert_slider_grid,
        new QuHeading(tr("Visual analogue scale-style slider (approximating continuous)")),
        new QuText(vas_description),
        vas_slider,
        new QuHeading(tr("Visual analogue scale-style slider (vertical version)")),
        new QuText(vas_description),
        vas_slider2,
#endif
    })
        ->setTitle(tr("Sliders and thermometers"))
        ->setType(QuPage::PageType::ClinicianWithPatient));

    // ------------------------------------------------------------------------
    // Editable variables inc. datetime
    // ------------------------------------------------------------------------

    FieldRefPtr fr_date_time = fieldRef(QStringLiteral("date_time"));
    FieldRefPtr fr_date_only = fieldRef(QStringLiteral("date_only"));
    FieldRefPtr fr_time_only = fieldRef(QStringLiteral("time_only"));
    FieldRefPtr fr_typedvar_text = fieldRef(QStringLiteral("typedvar_text"));
    FieldRefPtr fr_typedvar_int = fieldRef(QStringLiteral("typedvar_int"));
    FieldRefPtr fr_typedvar_text_multiline = fieldRef(QStringLiteral("typedvar_text_multiline"));
    QuPagePtr page_vars((new QuPage{
        new QuText(tr("Pages for clinicians have a different background colour.")),
        new QuHeading(tr("Date/time:")),
        new QuDateTime(fr_date_time),
        new QuHeading(tr("Date/time (with ‘now’ and ‘nullify’ buttons):")),
        (new QuDateTime(fr_date_time))
                             ->setOfferNowButton(true)
                             ->setOfferNullButton(true),
        new QuHeading(tr("Date/time (custom format):")),
        (new QuDateTime(fr_date_time))
                             ->setMode(QuDateTime::CustomDateTime)
                             ->setCustomFormat(QStringLiteral("yyyy MM dd HH:mm:ss:zzz")),
        new QuHeading(tr("Date:")),
        (new QuDateTime(fr_date_only))
                             ->setMode(QuDateTime::DefaultDate)
                             ->setOfferNowButton(true),
        new QuHeading(tr("Date (custom format):")),
        (new QuDateTime(fr_date_only))
                             ->setMode(QuDateTime::CustomDate)
                             ->setCustomFormat(QStringLiteral("yyyy MM dd"))
                             ->setOfferNowButton(true),
        new QuHeading(tr("Time:")),
        (new QuDateTime(fr_time_only))
                             ->setMode(QuDateTime::DefaultTime)
                             ->setOfferNowButton(true)
                             ->setOfferNullButton(true),
        new QuHeading(tr("Time (custom format):")),
        (new QuDateTime(fr_time_only))
                             ->setMode(QuDateTime::CustomTime)
                             ->setCustomFormat(QStringLiteral("HH:mm:ss"))
                             ->setOfferNowButton(true),
        new QuHeading(tr("Integer spinbox (range 5–10):")),
        new QuSpinBoxInteger(fieldRef(QStringLiteral("spinbox_int")), 5, 10),
        new QuHeading(tr("Double spinbox (range 7.1–7.9):")),
        new QuSpinBoxDouble(fieldRef(QStringLiteral("spinbox_real")), 7.1, 7.9),
        new QuHeading(tr("Text editor (plain text):")),
        new QuTextEdit(fr_typedvar_text_multiline, false),
        new QuHeading(tr("Text editor (clone of previous):")),
        new QuTextEdit(fr_typedvar_text_multiline, false),
        new QuHeading(tr("Text editor (rich text):")),
        (new QuTextEdit(fieldRef(QStringLiteral("typedvar_text_rich")), true))
                             ->setHint(tr("This one has a hint (placeholder text)")),
        new QuHeading(tr("Line editor (plain):")),
        (new QuLineEdit(fr_typedvar_text))
                             ->setHint(tr("hint: plain text")),
        new QuHeading(tr("Line editor (integer, range 13–19):")),
        new QuLineEditInteger(fr_typedvar_int, 13, 19),
        new QuHeading(tr("Line editor (double, range -0.05 to -0.09, 2dp):")),
        new QuLineEditDouble(fieldRef(QStringLiteral("typedvar_real")), -0.05, -0.09, 2),
        new QuHeading(tr("Variables in a grid:")),
        questionnairefunc::defaultGridRawPointer({
            {tr("label 1 (text)"), new QuLineEdit(fr_typedvar_text)},
            {tr("label 2 (int 13-19)"), new QuLineEditInteger(fr_typedvar_int, 13, 19)},
            {tr("label 3"), new QuHeading("Just a heading: " + lipsum2)},
            {tr("label 4"), new QuDateTime(fr_date_time)},
            {tr("label 5 (multiline text)"), new QuTextEdit(fr_typedvar_text)},
        }, uiconst::DEFAULT_COLSPAN_Q, uiconst::DEFAULT_COLSPAN_A),
    })
        ->setTitle(tr("Editable variable including dates/times"))
        ->setType(QuPage::PageType::Clinician));

    // ------------------------------------------------------------------------
    // Diagnostic codes
    // ------------------------------------------------------------------------

#ifdef DEBUG_DIAGNOSTIC_SET_CREATION_SPEED
    qDebug() << Q_FUNC_INFO << "Making ICD-10 data set...";
#endif
    QSharedPointer<Icd10> icd10 = QSharedPointer<Icd10>(new Icd10(m_app));
#ifdef DEBUG_DIAGNOSTIC_SET_CREATION_SPEED
    qDebug() << Q_FUNC_INFO << "... done";
    qDebug() << Q_FUNC_INFO << "Making ICD-9-CM data set...";
#endif
    QSharedPointer<Icd9cm> icd9cm = QSharedPointer<Icd9cm>(new Icd9cm(m_app));
#ifdef DEBUG_DIAGNOSTIC_SET_CREATION_SPEED
    qDebug() << Q_FUNC_INFO << "... done";
#endif
    FieldRefPtr fr_dx_code = fieldRef(QStringLiteral("diagnosticcode_code"));
    FieldRefPtr fr_dx_desc = fieldRef(QStringLiteral("diagnosticcode_description"));
    FieldRefPtr fr_dx_code2 = fieldRef(QStringLiteral("diagnosticcode2_code"));
    FieldRefPtr fr_dx_desc2 = fieldRef(QStringLiteral("diagnosticcode2_description"));
    QuPagePtr page_diag((new QuPage{
        new QuHeading(tr("Diagnostic code, ICD-10:")),
        new QuDiagnosticCode(icd10, fr_dx_code, fr_dx_desc),
        new QuHeading(tr("Diagnostic code, clone of the preceding:")),
        new QuDiagnosticCode(icd10, fr_dx_code, fr_dx_desc),
        new QuHeading(tr("Diagnostic code, ICD-9-CM:")),
        new QuDiagnosticCode(icd9cm, fr_dx_code2, fr_dx_desc2),
    })->setTitle(tr("Diagnostic codes")));

    // ------------------------------------------------------------------------
    // Canvas
    // ------------------------------------------------------------------------

    BlobFieldRefPtr fr_canvas_blobid = blobFieldRef(QStringLiteral("canvas_blobid"), true);
    BlobFieldRefPtr fr_canvas2_blobid = blobFieldRef(QStringLiteral("canvas2_blobid"), true);
    QuPagePtr page_canvas_1((new QuPage{
        (new QuText(tr("Page style: ClinicianWithPatient")))->setItalic(true),
        (new QuText(tr("WATCH OUT: scrolling enabled for this page; may "
                       "conflict with canvas; see next page too")))->setWarning(true),
        new QuHeading(tr("Canvas, blank start:")),
        new QuCanvas(fr_canvas2_blobid),
        new QuHeading(tr("Canvas, using files:")),
        new QuCanvas(
            fr_canvas_blobid,
            uifunc::resourceFilename(QStringLiteral("ace3/rhinoceros.png"))
        ),
        new QuHeading(tr("Canvas, clone of the first one:")),
        new QuCanvas(fr_canvas2_blobid),
    })
        ->setTitle(tr("Canvas (allowing scrolling)"))
        ->setType(QuPage::PageType::ClinicianWithPatient));

    QuPagePtr page_canvas_2((new QuPage{
        new QuHeading(tr("As before, but with scrolling disabled:")),
        new QuCanvas(
            fr_canvas_blobid,
            uifunc::resourceFilename(QStringLiteral("ace3/rhinoceros.png"))
        ),
    })
        ->setTitle(tr("Canvas (disabling scrolling)"))
        ->allowScroll(false)
        ->setType(QuPage::PageType::ClinicianWithPatient));

    // ------------------------------------------------------------------------
    // Buttons
    // ------------------------------------------------------------------------

    // Safe object lifespan signal: can use std::bind
    QuPagePtr page_buttons((new QuPage{
        new QuButton(
            tr("Say hello"),
            std::bind(&DemoQuestionnaire::callbackHello, this)),
        (new QuButton(
            tr("Say hello [disabled]"),
            std::bind(&DemoQuestionnaire::callbackHello, this)))
                                ->setActive(false),
        new QuButton(
            tr("Button with args ('foo')"),
            std::bind(&DemoQuestionnaire::callbackArg, this, "foo")),
        new QuButton(
            tr("Button with args ('bar')"),
            std::bind(&DemoQuestionnaire::callbackArg, this, "bar")),
        new QuButton(
            uiconst::CBS_ADD, true, true,
            std::bind(&DemoQuestionnaire::callbackHello, this)),
    })->setTitle(tr("Buttons")));

    // ------------------------------------------------------------------------
    // Photo (for a mandatory photo: last page in case we have no camera)
    // ... make it non-mandatory...
    // ------------------------------------------------------------------------

    QuPagePtr page_photo((new QuPage{
        new QuHeading(tr("Photo:")),
        new QuPhoto(blobFieldRef(QStringLiteral("photo_blobid"), false)),
    })->setTitle(tr("Photo")));

    // ------------------------------------------------------------------------
    // Layout test: cf. WidgetTestMenu::testQuestionnaire()
    // ------------------------------------------------------------------------

    QuPagePtr page_minimal_layout((new QuPage{
        new QuText(TextConst::LOREM_IPSUM_1),
    })->setTitle(tr("Reasonably long title with several words")));

    // ------------------------------------------------------------------------
    // Questionnaire
    // ------------------------------------------------------------------------

    auto questionnaire = new Questionnaire(m_app, {
        page_text,
#ifdef DEBUG_BIG_HEADER_ONLY_PAGE
        page_text_header_only,
#endif
        page_image, page_headings_layout,
        page_audio_countdown, page_boolean,
        page_mcq, page_mcq_variants, page_multiple_response, page_pickers,
        page_sliders, page_vars, page_diag,
        page_canvas_1, page_canvas_2,
        page_buttons,
        page_photo, page_minimal_layout,
    });
    questionnaire->setReadOnly(read_only);
    return questionnaire;
}


// ============================================================================
// Extra
// ============================================================================

void DemoQuestionnaire::callbackHello()
{
    uifunc::alert(tr("Hello!"));
}


void DemoQuestionnaire::callbackArg(const QString& arg)
{
    uifunc::alert(tr("Function argument was:") + " " + arg);
}


QuBoolean* DemoQuestionnaire::aceBoolean(const QString& stringname,
                                         const QString& fieldname)
{
    return (new QuBoolean(
        m_app.xstring(QStringLiteral("ace3"), stringname),
        fieldRef(fieldname, false)
    ))->setBigIndicator(false);
}