14.1.506. tablet_qt/tasks/ace3.cpp

/*
    Copyright (C) 2012-2019 Rudolf Cardinal (rudolf@pobox.com).

    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 <http://www.gnu.org/licenses/>.
*/

#include "ace3.h"
#include <QDebug>
#include "common/textconst.h"
#include "common/uiconst.h"
#include "lib/datetime.h"
#include "maths/mathfunc.h"
#include "lib/stringfunc.h"
#include "lib/uifunc.h"
#include "questionnairelib/namevalueoptions.h"
#include "questionnairelib/quboolean.h"
#include "questionnairelib/quflowcontainer.h"
#include "questionnairelib/qugridcontainer.h"
#include "questionnairelib/quverticalcontainer.h"
#include "questionnairelib/qucountdown.h"
#include "questionnairelib/questionnaire.h"
#include "questionnairelib/questionnairefunc.h"
#include "questionnairelib/quheading.h"
#include "questionnairelib/quimage.h"
#include "questionnairelib/qulineedit.h"
#include "questionnairelib/qulineeditinteger.h"
#include "questionnairelib/qumcq.h"
#include "questionnairelib/quphoto.h"
#include "questionnairelib/quspacer.h"
#include "questionnairelib/qutext.h"
#include "tasklib/taskfactory.h"
using mathfunc::eq;
using mathfunc::noneNull;
using mathfunc::sumInt;
using mathfunc::scoreStringWithPercent;
using mathfunc::totalScorePhrase;
using stringfunc::strnum;
using stringfunc::strseq;

const QString Ace3::ACE3_TABLENAME("ace3");

const QString IMAGE_SPOON("ace3/spoon.png");
const QString IMAGE_BOOK("ace3/book.png");
const QString IMAGE_KANGAROO("ace3/kangaroo.png");
const QString IMAGE_PENGUIN("ace3/penguin.png");
const QString IMAGE_ANCHOR("ace3/anchor.png");
const QString IMAGE_CAMEL("ace3/camel.png");
const QString IMAGE_HARP("ace3/harp.png");
const QString IMAGE_RHINOCEROS("ace3/rhinoceros.png");
const QString IMAGE_BARREL("ace3/barrel.png");
const QString IMAGE_CROWN("ace3/crown.png");
const QString IMAGE_CROCODILE("ace3/crocodile.png");
const QString IMAGE_ACCORDION("ace3/accordion.png");
const QString IMAGE_INFINITY("ace3/infinity.png");
const QString IMAGE_CUBE("ace3/cube.png");
const QString IMAGE_DOTS8("ace3/dots8.png");
const QString IMAGE_DOTS10("ace3/dots10.png");
const QString IMAGE_DOTS7("ace3/dots7.png");
const QString IMAGE_DOTS9("ace3/dots9.png");
const QString IMAGE_K("ace3/k.png");
const QString IMAGE_M("ace3/m.png");
const QString IMAGE_A("ace3/a.png");
const QString IMAGE_T("ace3/t.png");

const QString TAG_MEM_RECOGNIZE("mem_recognize");
const QString TAG_PG_LANG_COMMANDS_SENTENCES("pg_lang_commands_sentences");
const QString TAG_PG_MEM_PROMPTED_RECALL("pg_mem_prompted_recall");
const QString TAG_EL_LANG_OPTIONAL_COMMAND("lang_optional_command");
const QString TAG_EL_LANG_NOT_SHOWN("lang_not_shown");
const QString TAG_RECOG_REQUIRED("recog_required");
const QString TAG_RECOG_SUPERFLUOUS("recog_superfluous");
const QString TAG_RECOG_NAME("recog_name");
const QString TAG_RECOG_NUMBER("recog_number");
const QString TAG_RECOG_STREET("recog_street");
const QString TAG_RECOG_TOWN("recog_town");
const QString TAG_RECOG_COUNTY("recog_county");

// Field names, field prefixes, and field counts
const QString FN_AGE_FT_EDUCATION("age_at_leaving_full_time_education");
const QString FN_OCCUPATION("occupation");
const QString FN_HANDEDNESS("handedness");
const QString FP_ATTN_TIME("attn_time");
const int N_ATTN_TIME = 5;
const QString FP_ATTN_PLACE("attn_place");
const int N_ATTN_PLACE = 5;
const QString FP_ATTN_REPEAT_WORD("attn_repeat_word");
const int N_ATTN_REPEAT_WORD = 3;
const QString FN_ATTN_NUM_REGISTRATION_TRIALS("attn_num_registration_trials");
const QString FP_ATTN_SERIAL7("attn_serial7_subtraction");
const int N_ATTN_SERIAL7 = 5;
const QString FP_MEM_RECALL_WORD("mem_recall_word");
const int N_MEM_RECALL_WORD = 3;
const QString FN_FLUENCY_LETTERS_SCORE("fluency_letters_score");
const QString FN_FLUENCY_ANIMALS_SCORE("fluency_animals_score");
const QString FP_MEM_REPEAT_ADDR_TRIAL1("mem_repeat_address_trial1_");
const QString FP_MEM_REPEAT_ADDR_TRIAL2("mem_repeat_address_trial2_");
const QString FP_MEM_REPEAT_ADDR_TRIAL3("mem_repeat_address_trial3_");
const int N_MEM_REPEAT_ADDR = 7;
const QString FP_MEM_FAMOUS("mem_famous");
const int N_MEM_FAMOUS = 4;
const QString FN_LANG_FOLLOW_CMD_PRACTICE("lang_follow_command_practice");
const QString FP_LANG_FOLLOW_CMD("lang_follow_command");
const int N_LANG_FOLLOW_CMD = 3;
const QString FP_LANG_WRITE_SENTENCES_POINT("lang_write_sentences_point");
const int N_LANG_WRITE_SENTENCES_POINT = 2;
const QString FP_LANG_REPEAT_WORD("lang_repeat_word");
const int N_LANG_REPEAT_WORD = 4;
const QString FP_LANG_REPEAT_SENTENCE("lang_repeat_sentence");
const int N_LANG_REPEAT_SENTENCE = 2;
const QString FP_LANG_NAME_PICTURE("lang_name_picture");
const int N_LANG_NAME_PICTURE = 12;
const QString FP_LANG_IDENTIFY_CONCEPT("lang_identify_concept");
const int N_LANG_IDENTIFY_CONCEPT = 4;
const QString FN_LANG_READ_WORDS_ALOUD("lang_read_words_aloud");
const QString FN_VSP_COPY_INFINITY("vsp_copy_infinity");
const QString FN_VSP_COPY_CUBE("vsp_copy_cube");
const QString FN_VSP_DRAW_CLOCK("vsp_draw_clock");
const QString FP_VSP_COUNT_DOTS("vsp_count_dots");
const int N_VSP_COUNT_DOTS = 4;
const QString FP_VSP_IDENTIFY_LETTER("vsp_identify_letter");
const int N_VSP_IDENTIFY_LETTER = 4;
const QString FP_MEM_RECALL_ADDRESS("mem_recall_address");
const int N_MEM_RECALL_ADDRESS = 7;
const QString FP_MEM_RECOGNIZE_ADDRESS_SCORE("mem_recognize_address");  // SCORE; matches versions before 2.0.0
const QString FP_MEM_RECOGNIZE_ADDRESS_CHOICE("mem_recognize_address_choice");  // CHOICE; v2.0.0 onwards
// ... storing raw choices is new in v2.0.0, but the score field is preserved
//     for backwards compatibility
const int N_MEM_RECOGNIZE_ADDRESS = 5;
const QString FN_PICTURE1_BLOBID("picture1_blobid");
// defunct: picture1_rotation
const QString FN_PICTURE2_BLOBID("picture2_blobid");
// defunct: picture2_rotation
const QString FN_COMMENTS("comments");

// Subtotals. No magic numbers...
const int TOTAL_OVERALL = 100;
const int TOTAL_ATTN = 18;
const int TOTAL_MEM = 26;
const int TOTAL_FLUENCY = 14;
const int TOTAL_LANG = 26;
const int TOTAL_VSP = 16;

const int MIN_AGE = 0;
const int MAX_AGE_Y = 120;
const int FLUENCY_TIME_SEC = 60;

// We can't store a char in a variant, so an alternative would be QChar,
// but QString is just as simple.
const QString CHOICE_A("A");
const QString CHOICE_B("B");
const QString CHOICE_C("C");


void initializeAce3(TaskFactory& factory)
{
    static TaskRegistrar<Ace3> registered(factory);
}


Ace3::Ace3(CamcopsApp& app, DatabaseManager& db, const int load_pk) :
    Task(app, db, ACE3_TABLENAME, false, true, false),  // ... anon, clin, resp
    m_questionnaire(nullptr)
{
    addField(FN_AGE_FT_EDUCATION, QVariant::Int);
    addField(FN_OCCUPATION, QVariant::String);
    addField(FN_HANDEDNESS, QVariant::String);
    addFields(strseq(FP_ATTN_TIME, 1, N_ATTN_TIME), QVariant::Int);
    addFields(strseq(FP_ATTN_PLACE, 1, N_ATTN_PLACE), QVariant::Int);
    addFields(strseq(FP_ATTN_REPEAT_WORD, 1, N_ATTN_REPEAT_WORD), QVariant::Int);
    addField(FN_ATTN_NUM_REGISTRATION_TRIALS, QVariant::Int);
    addFields(strseq(FP_ATTN_SERIAL7, 1, N_ATTN_SERIAL7), QVariant::Int);
    addFields(strseq(FP_MEM_RECALL_WORD, 1, N_MEM_RECALL_WORD), QVariant::Int);
    addField(FN_FLUENCY_LETTERS_SCORE, QVariant::Int);
    addField(FN_FLUENCY_ANIMALS_SCORE, QVariant::Int);
    addFields(strseq(FP_MEM_REPEAT_ADDR_TRIAL1, 1, N_MEM_REPEAT_ADDR), QVariant::Int);
    addFields(strseq(FP_MEM_REPEAT_ADDR_TRIAL2, 1, N_MEM_REPEAT_ADDR), QVariant::Int);
    addFields(strseq(FP_MEM_REPEAT_ADDR_TRIAL3, 1, N_MEM_REPEAT_ADDR), QVariant::Int);
    addFields(strseq(FP_MEM_FAMOUS, 1, N_MEM_FAMOUS), QVariant::Int);
    addField(FN_LANG_FOLLOW_CMD_PRACTICE, QVariant::Int);
    addFields(strseq(FP_LANG_FOLLOW_CMD, 1, N_LANG_FOLLOW_CMD), QVariant::Int);
    addFields(strseq(FP_LANG_WRITE_SENTENCES_POINT, 1, N_LANG_WRITE_SENTENCES_POINT), QVariant::Int);
    addFields(strseq(FP_LANG_REPEAT_WORD, 1, N_LANG_REPEAT_WORD), QVariant::Int);
    addFields(strseq(FP_LANG_REPEAT_SENTENCE, 1, N_LANG_REPEAT_SENTENCE), QVariant::Int);
    addFields(strseq(FP_LANG_NAME_PICTURE, 1, N_LANG_NAME_PICTURE), QVariant::Int);
    addFields(strseq(FP_LANG_IDENTIFY_CONCEPT, 1, N_LANG_IDENTIFY_CONCEPT), QVariant::Int);
    addField(FN_LANG_READ_WORDS_ALOUD, QVariant::Int);
    addField(FN_VSP_COPY_INFINITY, QVariant::Int);
    addField(FN_VSP_COPY_CUBE, QVariant::Int);
    addField(FN_VSP_DRAW_CLOCK, QVariant::Int);
    addFields(strseq(FP_VSP_COUNT_DOTS, 1, N_VSP_COUNT_DOTS), QVariant::Int);
    addFields(strseq(FP_VSP_IDENTIFY_LETTER, 1, N_VSP_IDENTIFY_LETTER), QVariant::Int);
    addFields(strseq(FP_MEM_RECALL_ADDRESS, 1, N_MEM_RECALL_ADDRESS), QVariant::Int);
    addFields(strseq(FP_MEM_RECOGNIZE_ADDRESS_SCORE, 1, N_MEM_RECOGNIZE_ADDRESS), QVariant::Int);
    addFields(strseq(FP_MEM_RECOGNIZE_ADDRESS_CHOICE, 1, N_MEM_RECOGNIZE_ADDRESS), QVariant::Char);
    addField(FN_PICTURE1_BLOBID, QVariant::Int);  // FK to BLOB table
    addField(FN_PICTURE2_BLOBID, QVariant::Int);  // FK to BLOB table
    addField(FN_COMMENTS, QVariant::String);

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


// ============================================================================
// Class info
// ============================================================================

QString Ace3::shortname() const
{
    return "ACE-III";
}


QString Ace3::longname() const
{
    return tr("Addenbrooke’s Cognitive Examination, revision 3");
}


QString Ace3::description() const
{
    return tr("100-point clinician-administered assessment of attention/"
              "orientation, memory, fluency, language, and visuospatial "
              "domains.");
}


// ============================================================================
// Instance info
// ============================================================================

bool Ace3::isComplete() const
{
    return noneNull(values(strseq(FP_ATTN_TIME, 1, N_ATTN_TIME))) &&
        noneNull(values(strseq(FP_ATTN_PLACE, 1, N_ATTN_PLACE))) &&
        noneNull(values(strseq(FP_ATTN_REPEAT_WORD, 1, N_ATTN_REPEAT_WORD))) &&
        noneNull(values(strseq(FP_ATTN_SERIAL7, 1, N_ATTN_SERIAL7))) &&
        noneNull(values(strseq(FP_MEM_RECALL_WORD, 1, N_MEM_RECALL_WORD))) &&
        !valueIsNull(FN_FLUENCY_LETTERS_SCORE) &&
        !valueIsNull(FN_FLUENCY_ANIMALS_SCORE) &&
        noneNull(values(strseq(FP_MEM_REPEAT_ADDR_TRIAL3, 1, N_MEM_REPEAT_ADDR))) &&
        noneNull(values(strseq(FP_MEM_FAMOUS, 1, N_MEM_FAMOUS))) &&
        !valueIsNull(FN_LANG_FOLLOW_CMD_PRACTICE) &&
        (eq(value(FN_LANG_FOLLOW_CMD_PRACTICE), 0) ||
            noneNull(values(strseq(FP_LANG_FOLLOW_CMD, 1, N_LANG_FOLLOW_CMD)))) &&
        // ... failed practice, or completed all three actual tests
        noneNull(values(strseq(FP_LANG_WRITE_SENTENCES_POINT, 1, N_LANG_WRITE_SENTENCES_POINT))) &&
        noneNull(values(strseq(FP_LANG_REPEAT_WORD, 1, N_LANG_REPEAT_WORD))) &&
        noneNull(values(strseq(FP_LANG_REPEAT_SENTENCE, 1, N_LANG_REPEAT_SENTENCE))) &&
        noneNull(values(strseq(FP_LANG_NAME_PICTURE, 1, N_LANG_NAME_PICTURE))) &&
        noneNull(values(strseq(FP_LANG_IDENTIFY_CONCEPT, 1, N_LANG_IDENTIFY_CONCEPT))) &&
        !valueIsNull(FN_LANG_READ_WORDS_ALOUD) &&
        !valueIsNull(FN_VSP_COPY_INFINITY) &&
        !valueIsNull(FN_VSP_COPY_CUBE) &&
        !valueIsNull(FN_VSP_DRAW_CLOCK) &&
        noneNull(values(strseq(FP_VSP_COUNT_DOTS, 1, N_VSP_COUNT_DOTS))) &&
        noneNull(values(strseq(FP_VSP_IDENTIFY_LETTER, 1, N_VSP_IDENTIFY_LETTER))) &&
        noneNull(values(strseq(FP_MEM_RECALL_ADDRESS, 1, N_MEM_RECALL_ADDRESS))) &&
        isRecognitionComplete();
}


QStringList Ace3::summary() const
{
    const int a = getAttnScore();
    const int m = getMemScore();
    const int f = getFluencyScore();
    const int l = getLangScore();
    const int v = getVisuospatialScore();
    const int t = a + m + f + l + v;
    auto scorelambda = [](int score, int out_of) -> QString {
        return ": " + scoreStringWithPercent(score, out_of) + ".";
    };
    QStringList lines;
    lines.append(totalScorePhrase(t, TOTAL_OVERALL));
    lines.append(xstring("cat_attn") + scorelambda(a, TOTAL_ATTN));
    lines.append(xstring("cat_mem") + scorelambda(m, TOTAL_MEM));
    lines.append(xstring("cat_fluency") + scorelambda(f, TOTAL_FLUENCY));
    lines.append(xstring("cat_lang") + scorelambda(l, TOTAL_LANG));
    lines.append(xstring("cat_vsp") + scorelambda(v, TOTAL_VSP));
    return lines;
}


OpenableWidget* Ace3::editor(const bool read_only)
{
    int pagenum = 1;
    auto makeTitle = [this, &pagenum](const QString& title) -> QString {
        return xstring("title_prefix") + QString(" %1").arg(pagenum++) + ": "
                + title;
    };
    auto textRaw = [](const QString& string) -> QuElement* {
        return new QuText(string);
    };
    auto text = [this, textRaw](const QString& stringname) -> QuElement* {
        return textRaw(xstring(stringname));
    };
    auto explanation = [this](const QString& stringname) -> QuElement* {
        return (new QuText(xstring(stringname)))->setItalic();
    };
    auto heading = [this](const QString& stringname) -> QuElement* {
        return new QuHeading(xstring(stringname));
    };
    auto subheading = [this](const QString& stringname) -> QuElement* {
        return (new QuText(xstring(stringname)))->setBold()->setBig();
    };
    auto instructionRaw = [](const QString& string) -> QuElement* {
        return (new QuText(string))->setBold();
    };
    auto instruction = [this, instructionRaw](const QString& stringname) -> QuElement* {
        return instructionRaw(xstring(stringname));
    };
    auto boolean = [this](const QString& stringname, const QString& fieldname,
                          bool mandatory = true,
                          bool bold = false) -> QuElement* {
        return (new QuBoolean(xstring(stringname),
                              fieldRef(fieldname, mandatory)))->setBold(bold);
    };
    auto boolimg = [this](const QString& filenamestem, const QString& fieldname,
                          bool mandatory = true) -> QuElement* {
        return new QuBoolean(uifunc::resourceFilename(filenamestem), QSize(),
                             fieldRef(fieldname, mandatory));
    };
    auto warning = [](const QString& string) -> QuElement* {
        return (new QuText(string))->setWarning();
    };

    // ------------------------------------------------------------------------
    // Preamble; age-leaving-full-time-education; handedness
    // ------------------------------------------------------------------------

    const NameValueOptions options_handedness{
        {xstring("left_handed"), "L"},
        {xstring("right_handed"), "R"},
    };
    QuPagePtr page_preamble((new QuPage{
        instruction("instruction_need_paper"),
        getClinicianQuestionnaireBlockRawPointer(),
        instruction("preamble_instruction"),
        questionnairefunc::defaultGridRawPointer({
            {xstring("q_age_leaving_fte"),
             new QuLineEditInteger(fieldRef(FN_AGE_FT_EDUCATION), MIN_AGE, MAX_AGE_Y)},
            {xstring("q_occupation"),
             new QuLineEdit(fieldRef(FN_OCCUPATION))},
            {xstring("q_handedness"),
             (new QuMcq(fieldRef(FN_HANDEDNESS), options_handedness))->setHorizontal(true)},
        }, uiconst::DEFAULT_COLSPAN_Q, uiconst::DEFAULT_COLSPAN_A),
    })
        ->setTitle(makeTitle(tr("Preamble")))
        ->setType(QuPage::PageType::Clinician));

    // ------------------------------------------------------------------------
    // Attention/orientation/three word recall
    // ------------------------------------------------------------------------

    const QDateTime now = datetime::now();
    QString season;
    switch (datetime::now().date().month()) {
        // 1 = Jan, 12 = Dec
    case 12:
    case 1:
    case 2:
        season = xstring("season_winter");
        break;
    case 3:
    case 4:
    case 5:
        season = xstring("season_spring");
        break;
    case 6:
    case 7:
    case 8:
        season = xstring("season_summer");
        break;
    case 9:
    case 10:
    case 11:
        season = xstring("season_autumn");
        break;
    default:
        season = "?(season_bug)";
        break;
    }
    const QString correct_date = "     " + now.toString("dddd d MMMM yyyy") +
            "; " + season;
    // ... e.g. "Monday 2 January 2016; winter";
    // http://doc.qt.io/qt-5/qdate.html#toString

    const NameValueOptions options_registration{
        {"1", 1},
        {"2", 2},
        {"3", 3},
        {"4", 4},
        {">4", 0},
    };
    QuPagePtr page_attn((new QuPage{
        heading("cat_attn"),
        // Orientation
        instruction("attn_q_time"),
        new QuFlowContainer{
            boolean("attn_time1", strnum(FP_ATTN_TIME, 1)),
            boolean("attn_time2", strnum(FP_ATTN_TIME, 2)),
            boolean("attn_time3", strnum(FP_ATTN_TIME, 3)),
            boolean("attn_time4", strnum(FP_ATTN_TIME, 4)),
            boolean("attn_time5", strnum(FP_ATTN_TIME, 5)),
        },
        explanation("instruction_time"),
        (new QuText(correct_date))->setItalic(),
        instruction("attn_q_place"),
        new QuFlowContainer{
            boolean("attn_place1", strnum(FP_ATTN_PLACE, 1)),
            boolean("attn_place2", strnum(FP_ATTN_PLACE, 2)),
            boolean("attn_place3", strnum(FP_ATTN_PLACE, 3)),
            boolean("attn_place4", strnum(FP_ATTN_PLACE, 4)),
            boolean("attn_place5", strnum(FP_ATTN_PLACE, 5)),
        },
        explanation("instruction_place"),
        // Lemon, key, ball (registration)
        heading("cat_attn"),
        instruction("attn_q_words"),
        explanation("attn_instruction_words"),
        new QuFlowContainer{
            boolean("mem_word1", strnum(FP_ATTN_REPEAT_WORD, 1)),
            boolean("mem_word2", strnum(FP_ATTN_REPEAT_WORD, 2)),
            boolean("mem_word3", strnum(FP_ATTN_REPEAT_WORD, 3)),
        },
        new QuFlowContainer{
            text("attn_q_register_n_trials"),
            (new QuMcq(fieldRef(FN_ATTN_NUM_REGISTRATION_TRIALS, false),  // not mandatory
                                 options_registration))->setHorizontal(true),
        },
        // Serial 7s
        heading("cat_attn"),
        instruction("attn_q_serial_sevens"),
        explanation("attn_instruction_sevens"),
        new QuFlowContainer{
            boolean("attn_subtraction1", strnum(FP_ATTN_SERIAL7, 1)),
            boolean("attn_subtraction2", strnum(FP_ATTN_SERIAL7, 2)),
            boolean("attn_subtraction3", strnum(FP_ATTN_SERIAL7, 3)),
            boolean("attn_subtraction4", strnum(FP_ATTN_SERIAL7, 4)),
            boolean("attn_subtraction5", strnum(FP_ATTN_SERIAL7, 5)),
        },
        // Lemon, key, ball (recall)
        heading("cat_mem"),
        instruction("mem_q_recall_words"),
        explanation("mem_instruction_recall"),
        new QuFlowContainer{
            boolean("mem_word1", strnum(FP_MEM_RECALL_WORD, 1)),
            boolean("mem_word2", strnum(FP_MEM_RECALL_WORD, 2)),
            boolean("mem_word3", strnum(FP_MEM_RECALL_WORD, 3)),
        },
    })->setTitle(makeTitle(tr("Attention")))->setType(QuPage::PageType::Clinician));

    // ------------------------------------------------------------------------
    // Fluency
    // ------------------------------------------------------------------------

    const NameValueOptions options_fluency_letters{
        {"0–1", 0},
        {"2–3", 1},
        {"4–5", 2},
        {"6–7", 3},
        {"8–10", 4},
        {"11–13", 5},
        {"14–17", 6},
        {"≥18", 7}

    };
    const NameValueOptions options_fluency_animals{
        {"0–4", 0},
        {"5–6", 1},
        {"7–8", 2},
        {"9–10", 3},
        {"11–13", 4},
        {"14–16", 5},
        {"17–21", 6},
        {"≥22", 7}
    };
    QuPagePtr page_fluency((new QuPage{
        heading("cat_fluency"),
        // Letters
        subheading("fluency_subhead_letters"),
        instruction("fluency_q_letters"),
        new QuCountdown(FLUENCY_TIME_SEC),
        explanation("fluency_instruction_letters"),
        text("fluency_prompt_letters_cor"),
        (new QuMcq(fieldRef(FN_FLUENCY_LETTERS_SCORE),
                             options_fluency_letters))->setHorizontal(true),
        new QuSpacer(),
        // Animals
        subheading("fluency_subheading_animals"),
        instruction("fluency_q_animals"),
        new QuCountdown(FLUENCY_TIME_SEC),
        explanation("fluency_instruction_animals"),
        text("fluency_prompt_animals_cor"),
        (new QuMcq(fieldRef(FN_FLUENCY_ANIMALS_SCORE),
                             options_fluency_animals))->setHorizontal(true),
    })->setTitle(makeTitle(tr("Fluency")))->setType(QuPage::PageType::Clinician));

    // ------------------------------------------------------------------------
    // Learning the address; famous people
    // ------------------------------------------------------------------------

    QuPagePtr page_repeat_addr_famous((new QuPage{
        heading("cat_mem"),
        instruction("memory_q_address"),
        explanation("memory_instruction_address_1"),
        explanation("memory_instruction_address_2"),
        // Address 1
        new QuVerticalContainer{
            instructionRaw(xstring("trial") + " 1"),
            new QuFlowContainer{
                boolean("address_1", strnum(FP_MEM_REPEAT_ADDR_TRIAL1, 1), false),
                boolean("address_2", strnum(FP_MEM_REPEAT_ADDR_TRIAL1, 2), false),
            },
            new QuFlowContainer{
                boolean("address_3", strnum(FP_MEM_REPEAT_ADDR_TRIAL1, 3), false),
                boolean("address_4", strnum(FP_MEM_REPEAT_ADDR_TRIAL1, 4), false),
                boolean("address_5", strnum(FP_MEM_REPEAT_ADDR_TRIAL1, 5), false),
            },
            boolean("address_6", strnum(FP_MEM_REPEAT_ADDR_TRIAL1, 6), false),
            boolean("address_7", strnum(FP_MEM_REPEAT_ADDR_TRIAL1, 7), false),
        },
        // Address 2
        new QuVerticalContainer{
            instructionRaw(xstring("trial") + " 2"),
            new QuFlowContainer{
                boolean("address_1", strnum(FP_MEM_REPEAT_ADDR_TRIAL2, 1), false),
                boolean("address_2", strnum(FP_MEM_REPEAT_ADDR_TRIAL2, 2), false),
            },
            new QuFlowContainer{
                boolean("address_3", strnum(FP_MEM_REPEAT_ADDR_TRIAL2, 3), false),
                boolean("address_4", strnum(FP_MEM_REPEAT_ADDR_TRIAL2, 4), false),
                boolean("address_5", strnum(FP_MEM_REPEAT_ADDR_TRIAL2, 5), false),
            },
            boolean("address_6", strnum(FP_MEM_REPEAT_ADDR_TRIAL2, 6), false),
            boolean("address_7", strnum(FP_MEM_REPEAT_ADDR_TRIAL2, 7), false),
        },
        // Address 3
        new QuVerticalContainer{
            instructionRaw(xstring("trial") + " 3"),
            new QuFlowContainer{
                boolean("address_1", strnum(FP_MEM_REPEAT_ADDR_TRIAL3, 1), true),
                boolean("address_2", strnum(FP_MEM_REPEAT_ADDR_TRIAL3, 2), true),
            },
            new QuFlowContainer{
                boolean("address_3", strnum(FP_MEM_REPEAT_ADDR_TRIAL3, 3), true),
                boolean("address_4", strnum(FP_MEM_REPEAT_ADDR_TRIAL3, 4), true),
                boolean("address_5", strnum(FP_MEM_REPEAT_ADDR_TRIAL3, 5), true),
            },
            boolean("address_6", strnum(FP_MEM_REPEAT_ADDR_TRIAL3, 6), true),
            boolean("address_7", strnum(FP_MEM_REPEAT_ADDR_TRIAL3, 7), true),
        },
        heading("cat_mem"),
        boolean("famous_1", strnum(FP_MEM_FAMOUS, 1), true, true),
        boolean("famous_2", strnum(FP_MEM_FAMOUS, 2), true, true),
        boolean("famous_3", strnum(FP_MEM_FAMOUS, 3), true, true),
        boolean("famous_4", strnum(FP_MEM_FAMOUS, 4), true, true),
        explanation("instruction_famous"),
    })
        ->setTitle(makeTitle(tr("Address learning; famous people")))
        ->setType(QuPage::PageType::Clinician));

    // ------------------------------------------------------------------------
    // Commands; writing sentences
    // ------------------------------------------------------------------------

    QuPagePtr page_commands_sentences(( new QuPage{
        heading("cat_lang"),
        explanation("lang_q_command_1"),
        boolean("lang_command_practice", FN_LANG_FOLLOW_CMD_PRACTICE, true, true),
        explanation("lang_q_command_2"),
        boolean("lang_command1", strnum(FP_LANG_FOLLOW_CMD, 1), true, true)->addTag(TAG_EL_LANG_OPTIONAL_COMMAND),
        boolean("lang_command2", strnum(FP_LANG_FOLLOW_CMD, 2), true, true)->addTag(TAG_EL_LANG_OPTIONAL_COMMAND),
        boolean("lang_command3", strnum(FP_LANG_FOLLOW_CMD, 3), true, true)->addTag(TAG_EL_LANG_OPTIONAL_COMMAND),
        warning(tr("Other commands not shown; subject failed practice trial"))
                                            ->addTag(TAG_EL_LANG_NOT_SHOWN),
        heading("cat_lang"),
        instruction("lang_q_sentences"),
        boolean("lang_sentences_point1", strnum(FP_LANG_WRITE_SENTENCES_POINT, 1)),
        boolean("lang_sentences_point2", strnum(FP_LANG_WRITE_SENTENCES_POINT, 2)),
    })
        ->setTitle(makeTitle(tr("Commands; writing sentences")))
        ->addTag(TAG_PG_LANG_COMMANDS_SENTENCES)
        ->setType(QuPage::PageType::Clinician));

    // ------------------------------------------------------------------------
    // Repetition; preparing clinician for pictures
    // ------------------------------------------------------------------------

    QuPagePtr page_repetition(( new QuPage{
        // Repeating words
        heading("cat_lang"),
        instruction("lang_q_repeat"),
        boolean("lang_repeat_word1", strnum(FP_LANG_REPEAT_WORD, 1)),
        boolean("lang_repeat_word2", strnum(FP_LANG_REPEAT_WORD, 2)),
        boolean("lang_repeat_word3", strnum(FP_LANG_REPEAT_WORD, 3)),
        boolean("lang_repeat_word4", strnum(FP_LANG_REPEAT_WORD, 4)),
        explanation("lang_instruction_repeat"),
        // Repeating sentences
        heading("cat_lang"),
        instruction("lang_q_repeat"),
        boolean("lang_sentence1", strnum(FP_LANG_REPEAT_SENTENCE, 1)),
        boolean("lang_sentence2", strnum(FP_LANG_REPEAT_SENTENCE, 2)),
        explanation("lang_instruction_sentences_1"),
        explanation("lang_instruction_sentences_2"),
        new QuSpacer(),
        // Preparation for clinician for pictures
        instruction("advance_warning_1"),
        explanation("advance_warning_2"),
        explanation("advance_warning_3"),
        explanation("advance_warning_4"),
        explanation("advance_warning_5"),
        explanation("advance_warning_6"),
        explanation("advance_warning_7"),
        explanation("advance_warning_8"),
    })
        ->setTitle(makeTitle(tr("Repetition")))
        ->setType(QuPage::PageType::Clinician));

    // ------------------------------------------------------------------------
    // Naming pictures
    // ------------------------------------------------------------------------

    QuPagePtr page_name_pictures((new QuPage{
        // Naming pictures
        heading("cat_lang"),
        instruction("lang_q_identify_pic"),
        new QuGridContainer(3, {
            boolimg(IMAGE_SPOON, strnum(FP_LANG_NAME_PICTURE, 1)),
            boolimg(IMAGE_BOOK, strnum(FP_LANG_NAME_PICTURE, 2)),
            boolimg(IMAGE_KANGAROO, strnum(FP_LANG_NAME_PICTURE, 3)),
            boolimg(IMAGE_PENGUIN, strnum(FP_LANG_NAME_PICTURE, 4)),
            boolimg(IMAGE_ANCHOR, strnum(FP_LANG_NAME_PICTURE, 5)),
            boolimg(IMAGE_CAMEL, strnum(FP_LANG_NAME_PICTURE, 6)),
            boolimg(IMAGE_HARP, strnum(FP_LANG_NAME_PICTURE, 7)),
            boolimg(IMAGE_RHINOCEROS, strnum(FP_LANG_NAME_PICTURE, 8)),
            boolimg(IMAGE_BARREL, strnum(FP_LANG_NAME_PICTURE, 9)),
            boolimg(IMAGE_CROWN, strnum(FP_LANG_NAME_PICTURE, 10)),
            boolimg(IMAGE_CROCODILE, strnum(FP_LANG_NAME_PICTURE, 11)),
            boolimg(IMAGE_ACCORDION, strnum(FP_LANG_NAME_PICTURE, 12)),
        }),
        // Choosing pictures by concept
        heading("cat_lang"),
        instruction("lang_q_identify_concept"),
        boolean("lang_concept1", strnum(FP_LANG_IDENTIFY_CONCEPT, 1)),
        boolean("lang_concept2", strnum(FP_LANG_IDENTIFY_CONCEPT, 2)),
        boolean("lang_concept3", strnum(FP_LANG_IDENTIFY_CONCEPT, 3)),
        boolean("lang_concept4", strnum(FP_LANG_IDENTIFY_CONCEPT, 4)),
        explanation("lang_instruction_identify_concept"),
    })
        ->setTitle(makeTitle(tr("Naming pictures")))
        ->setType(QuPage::PageType::ClinicianWithPatient));

    // ------------------------------------------------------------------------
    // Reading irregular words
    // ------------------------------------------------------------------------

    QuPagePtr page_read_words_aloud((new QuPage{
        // Reading irregular words aloud
        heading("cat_lang"),
        instruction("lang_q_read_aloud"),
        new QuSpacer(),
        subheading("lang_read_aloud_words"),  // the words
        new QuSpacer(),
        boolean("lang_read_aloud_all_correct", FN_LANG_READ_WORDS_ALOUD),
        explanation("lang_instruction_read_aloud"),
    })
        ->setTitle(makeTitle(tr("Reading irregular words")))
        ->setType(QuPage::PageType::ClinicianWithPatient));

    // ------------------------------------------------------------------------
    // Infinity
    // ------------------------------------------------------------------------

    QuPagePtr page_infinity((new QuPage{
        heading("cat_vsp"),
        instruction("vsp_q_infinity"),
        new QuImage(uifunc::resourceFilename(IMAGE_INFINITY)),
        boolean("vsp_infinity_correct", FN_VSP_COPY_INFINITY),
    })
        ->setTitle(makeTitle(tr("Infinity")))
        ->setType(QuPage::PageType::ClinicianWithPatient));

    // ------------------------------------------------------------------------
    // Cube
    // ------------------------------------------------------------------------

    const NameValueOptions options_cube = NameValueOptions::makeNumbers(0, 2);
    QuPagePtr page_cube((new QuPage{
        instruction("vsp_q_cube"),
        new QuImage(uifunc::resourceFilename(IMAGE_CUBE)),
        text("vsp_score_cube"),
        (new QuMcq(fieldRef(FN_VSP_COPY_CUBE), options_cube))->setHorizontal(true),
    })
        ->setTitle(makeTitle(tr("Cube")))
        ->setType(QuPage::PageType::ClinicianWithPatient));

    // ------------------------------------------------------------------------
    // Clock
    // ------------------------------------------------------------------------

    const NameValueOptions options_clock = NameValueOptions::makeNumbers(0, 5);
    QuPagePtr page_clock((new QuPage{
                              instruction("vsp_q_clock"),
                              explanation("vsp_instruction_clock"),
                              text("vsp_score_clock"),
                              (new QuMcq(fieldRef(FN_VSP_DRAW_CLOCK), options_clock))->setHorizontal(true),
    })
        ->setTitle(makeTitle(tr("Clock")))
        ->setType(QuPage::PageType::Clinician));

    // ------------------------------------------------------------------------
    // Dots
    // ------------------------------------------------------------------------

    QuPagePtr page_dots((new QuPage{
        heading("cat_vsp"),
        instruction("vsp_q_dots"),
        new QuGridContainer(2, {
            boolimg(IMAGE_DOTS8, strnum(FP_VSP_COUNT_DOTS, 1)),
            boolimg(IMAGE_DOTS10, strnum(FP_VSP_COUNT_DOTS, 2)),
            boolimg(IMAGE_DOTS7, strnum(FP_VSP_COUNT_DOTS, 3)),
            boolimg(IMAGE_DOTS9, strnum(FP_VSP_COUNT_DOTS, 4)),
        }),
    })
        ->setTitle(makeTitle(tr("Dot counting")))
        ->setType(QuPage::PageType::ClinicianWithPatient));

    // ------------------------------------------------------------------------
    // Letters
    // ------------------------------------------------------------------------

    QuPagePtr page_letters((new QuPage{
        heading("cat_vsp"),
        instruction("vsp_q_letters"),
        new QuGridContainer(2, {
            boolimg(IMAGE_K, strnum(FP_VSP_IDENTIFY_LETTER, 1)),
            boolimg(IMAGE_M, strnum(FP_VSP_IDENTIFY_LETTER, 2)),
            boolimg(IMAGE_A, strnum(FP_VSP_IDENTIFY_LETTER, 3)),
            boolimg(IMAGE_T, strnum(FP_VSP_IDENTIFY_LETTER, 4)),
        }),
    })
        ->setTitle(makeTitle(tr("Noisy letters")))
        ->setType(QuPage::PageType::ClinicianWithPatient));

    // ------------------------------------------------------------------------
    // Back to clinician
    // ------------------------------------------------------------------------

    QuPagePtr page_back_to_clinician((new QuPage{
        textRaw(tr("Please make sure the subject can’t see the screen "
                   "before you proceed. (Memory prompts coming up.)")),
    })
        ->setTitle(makeTitle(tr("[reminder to clinician]")))
        ->setType(QuPage::PageType::Clinician));

    // ------------------------------------------------------------------------
    // Address recall: free
    // ------------------------------------------------------------------------

    QuPagePtr page_recall_address_free((new QuPage{
        heading("cat_mem"),
        instruction("mem_q_recall_address"),
        new QuVerticalContainer{
            new QuFlowContainer{
                boolean("address_1", strnum(FP_MEM_RECALL_ADDRESS, 1)),
                boolean("address_2", strnum(FP_MEM_RECALL_ADDRESS, 2)),
            },
            new QuFlowContainer{
                boolean("address_3", strnum(FP_MEM_RECALL_ADDRESS, 3)),
                boolean("address_4", strnum(FP_MEM_RECALL_ADDRESS, 4)),
                boolean("address_5", strnum(FP_MEM_RECALL_ADDRESS, 5)),
            },
            boolean("address_6", strnum(FP_MEM_RECALL_ADDRESS, 6)),
            boolean("address_7", strnum(FP_MEM_RECALL_ADDRESS, 7)),
        },
    })
        ->setTitle(makeTitle(tr("Free recall")))
        ->setType(QuPage::PageType::Clinician));

    // ------------------------------------------------------------------------
    // Address recall: cued
    // ------------------------------------------------------------------------

    const NameValueOptions options_recall_name({
        {xstring("mem_recall_option1_line1"), CHOICE_A},
        {xstring("mem_recall_option2_line1"), CHOICE_B},  // correct
        {xstring("mem_recall_option3_line1"), CHOICE_C},
    });
    const NameValueOptions options_recall_number({
        {xstring("mem_recall_option1_line2"), CHOICE_A},
        {xstring("mem_recall_option2_line2"), CHOICE_B},  // correct
        {xstring("mem_recall_option3_line2"), CHOICE_C},
    });
    const NameValueOptions options_recall_street({
        {xstring("mem_recall_option1_line3"), CHOICE_A},
        {xstring("mem_recall_option2_line3"), CHOICE_B},
        {xstring("mem_recall_option3_line3"), CHOICE_C},  // correct
    });
    const NameValueOptions options_recall_town({
        {xstring("mem_recall_option1_line4"), CHOICE_A},
        {xstring("mem_recall_option2_line4"), CHOICE_B},  // correct
        {xstring("mem_recall_option3_line4"), CHOICE_C},
    });
    const NameValueOptions options_recall_county({
        {xstring("mem_recall_option1_line5"), CHOICE_A},  // correct
        {xstring("mem_recall_option2_line5"), CHOICE_B},
        {xstring("mem_recall_option3_line5"), CHOICE_C},
    });
    FieldRefPtr fr_recallprompted_name = fieldRef(strnum(FP_MEM_RECOGNIZE_ADDRESS_CHOICE, 1));
    FieldRefPtr fr_recallprompted_number = fieldRef(strnum(FP_MEM_RECOGNIZE_ADDRESS_CHOICE, 2));
    FieldRefPtr fr_recallprompted_street = fieldRef(strnum(FP_MEM_RECOGNIZE_ADDRESS_CHOICE, 3));
    FieldRefPtr fr_recallprompted_town = fieldRef(strnum(FP_MEM_RECOGNIZE_ADDRESS_CHOICE, 4));
    FieldRefPtr fr_recallprompted_county = fieldRef(strnum(FP_MEM_RECOGNIZE_ADDRESS_CHOICE, 5));
    connect(fr_recallprompted_name.data(), &FieldRef::valueChanged,
            this, &Ace3::updateAddressRecognition);
    connect(fr_recallprompted_number.data(), &FieldRef::valueChanged,
            this, &Ace3::updateAddressRecognition);
    connect(fr_recallprompted_street.data(), &FieldRef::valueChanged,
            this, &Ace3::updateAddressRecognition);
    connect(fr_recallprompted_town.data(), &FieldRef::valueChanged,
            this, &Ace3::updateAddressRecognition);
    connect(fr_recallprompted_county.data(), &FieldRef::valueChanged,
            this, &Ace3::updateAddressRecognition);

    QuPagePtr page_recall_address_prompted((new QuPage{
        instruction("no_need_for_extra_recall")->addTag(TAG_RECOG_SUPERFLUOUS),
        instruction("mem_q_recognize_address")->addTag(TAG_RECOG_REQUIRED),
        textRaw(tr("Name:"))->addTag(TAG_RECOG_NAME),
        (new QuMcq(fr_recallprompted_name, options_recall_name))
                    ->setHorizontal(true)
                    ->addTag(TAG_RECOG_NAME),
        textRaw(tr("Number:"))->addTag(TAG_RECOG_NUMBER),
        (new QuMcq(fr_recallprompted_number, options_recall_number))
                    ->setHorizontal(true)
                    ->addTag(TAG_RECOG_NUMBER),
        textRaw(tr("Street:"))->addTag(TAG_RECOG_STREET),
        (new QuMcq(fr_recallprompted_street, options_recall_street))
                    ->setHorizontal(true)
                    ->addTag(TAG_RECOG_STREET),
        textRaw(tr("Town:"))->addTag(TAG_RECOG_TOWN),
        (new QuMcq(fr_recallprompted_town, options_recall_town))
                    ->setHorizontal(true)
                    ->addTag(TAG_RECOG_TOWN),
        textRaw(tr("County:"))->addTag(TAG_RECOG_COUNTY),
        (new QuMcq(fr_recallprompted_county, options_recall_county))
                    ->setHorizontal(true)
                    ->addTag(TAG_RECOG_COUNTY),

    })
        ->setTitle(makeTitle(tr("Cued recall")))
        ->addTag(TAG_PG_MEM_PROMPTED_RECALL)
        ->setType(QuPage::PageType::Clinician));

    // ------------------------------------------------------------------------
    // Comments
    // ------------------------------------------------------------------------

    QuPagePtr page_comments((new QuPage{
        instructionRaw(TextConst::examinerCommentsPrompt()),
        (new QuLineEdit(fieldRef(FN_COMMENTS, false)))
            ->setHint(TextConst::examinerComments()),
    })
        ->setTitle(makeTitle(tr("Comments")))
        ->setType(QuPage::PageType::Clinician));

    // ------------------------------------------------------------------------
    // Photo 1
    // ------------------------------------------------------------------------

    QuPagePtr page_photo_1((new QuPage{
        instruction("picture1_q"),
        explanation("picture_instruction1"),
        explanation("picture_instruction2"),
        new QuPhoto(blobFieldRef(FN_PICTURE1_BLOBID, false)),
    })
        ->setTitle(makeTitle(tr("Photo 1")))
        ->setType(QuPage::PageType::Clinician));

    // ------------------------------------------------------------------------
    // Photo 2
    // ------------------------------------------------------------------------

    QuPagePtr page_photo_2((new QuPage{
        instruction("picture2_q"),
        explanation("picture_instruction1"),
        explanation("picture_instruction2"),
        new QuPhoto(blobFieldRef(FN_PICTURE2_BLOBID, false)),
    })
        ->setTitle(makeTitle(tr("Photo 2")))
        ->setType(QuPage::PageType::Clinician));

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

    m_questionnaire = new Questionnaire(m_app, {
        page_preamble, page_attn, page_fluency, page_repeat_addr_famous,
        page_commands_sentences, page_repetition,
        page_name_pictures, page_read_words_aloud,
        page_infinity, page_cube, page_clock,
        page_dots, page_letters, page_back_to_clinician,
        page_recall_address_free, page_recall_address_prompted,
        page_comments, page_photo_1, page_photo_2,
    });
    m_questionnaire->setReadOnly(read_only);

    // ------------------------------------------------------------------------
    // Signals and initial dynamic state
    // ------------------------------------------------------------------------

    FieldRefPtr fr_lang_practice = fieldRef(FN_LANG_FOLLOW_CMD_PRACTICE);
    connect(fr_lang_practice.data(), &FieldRef::valueChanged,
            this, &Ace3::langPracticeChanged);
    langPracticeChanged(fr_lang_practice.data());

    for (int i = 1; i <= N_MEM_RECALL_ADDRESS; ++i) {
        FieldRefPtr fr = fieldRef(strnum(FP_MEM_RECALL_ADDRESS, i));
        connect(fr.data(), &FieldRef::valueChanged,
                this, &Ace3::updateAddressRecognition);
    }

    updateAddressRecognition();

    // ------------------------------------------------------------------------
    // Done
    // ------------------------------------------------------------------------

    return m_questionnaire;
}


// ============================================================================
// Task-specific calculations
// ============================================================================

int Ace3::getAttnScore() const
{
    return sumInt(values(strseq(FP_ATTN_TIME, 1, N_ATTN_TIME) +
                         strseq(FP_ATTN_PLACE, 1, N_ATTN_PLACE) +
                         strseq(FP_ATTN_REPEAT_WORD, 1, N_ATTN_REPEAT_WORD) +
                         strseq(FP_ATTN_SERIAL7, 1, N_ATTN_SERIAL7)));
}


int Ace3::getMemRecognitionScore() const
{
    const int recall1 = valueInt(strnum(FP_MEM_RECALL_ADDRESS, 1));
    const int recall2 = valueInt(strnum(FP_MEM_RECALL_ADDRESS, 2));
    const int recall3 = valueInt(strnum(FP_MEM_RECALL_ADDRESS, 3));
    const int recall4 = valueInt(strnum(FP_MEM_RECALL_ADDRESS, 4));
    const int recall5 = valueInt(strnum(FP_MEM_RECALL_ADDRESS, 5));
    const int recall6 = valueInt(strnum(FP_MEM_RECALL_ADDRESS, 6));
    const int recall7 = valueInt(strnum(FP_MEM_RECALL_ADDRESS, 7));
    const int recog1 = valueInt(strnum(FP_MEM_RECOGNIZE_ADDRESS_SCORE, 1));
    const int recog2 = valueInt(strnum(FP_MEM_RECOGNIZE_ADDRESS_SCORE, 2));
    const int recog3 = valueInt(strnum(FP_MEM_RECOGNIZE_ADDRESS_SCORE, 3));
    const int recog4 = valueInt(strnum(FP_MEM_RECOGNIZE_ADDRESS_SCORE, 4));
    const int recog5 = valueInt(strnum(FP_MEM_RECOGNIZE_ADDRESS_SCORE, 5));
    int score = 0;
    score += (recall1 && recall2) ? 1 : recog1;  // forename, surname
    score += recall3 ? 1 : recog2;  // number
    score += (recall4 && recall5) ? 1 : recog3;  // streetname, streettype
    score += recall6 ? 1 : recog4;  // city
    score += recall7 ? 1 : recog5; // county
    return score;
}


int Ace3::getMemScore() const
{
    return sumInt(values(strseq(FP_MEM_RECALL_WORD, 1, N_MEM_RECALL_WORD) +
                         strseq(FP_MEM_REPEAT_ADDR_TRIAL3, 1, N_MEM_REPEAT_ADDR) +
                         strseq(FP_MEM_FAMOUS, 1, N_MEM_FAMOUS) +
                         strseq(FP_MEM_RECALL_ADDRESS, 1, N_MEM_RECALL_ADDRESS))) +
            getMemRecognitionScore();
}


int Ace3::getFluencyScore() const
{
    return valueInt(FN_FLUENCY_LETTERS_SCORE) +
            valueInt(FN_FLUENCY_ANIMALS_SCORE);
}


int Ace3::getFollowCommandScore() const
{
    if (!valueInt(FN_LANG_FOLLOW_CMD_PRACTICE)) {
        return 0;
    }
    return sumInt(values(strseq(FP_LANG_FOLLOW_CMD, 1, N_LANG_FOLLOW_CMD)));
}


int Ace3::getRepeatWordScore() const
{
    const int n = sumInt(values(strseq(FP_LANG_REPEAT_WORD, 1, 4)));
    return n >= 4 ? 2 : (n == 3 ? 1 : 0);
}


int Ace3::getLangScore() const
{
    return getFollowCommandScore() +  // 3 points
            sumInt(values(strseq(FP_LANG_WRITE_SENTENCES_POINT, 1, N_LANG_WRITE_SENTENCES_POINT))) +  // 2 points
            getRepeatWordScore() +  // 2 points
            sumInt(values(strseq(FP_LANG_REPEAT_SENTENCE, 1, N_LANG_REPEAT_SENTENCE))) +  // 2 points
            sumInt(values(strseq(FP_LANG_NAME_PICTURE, 1, N_LANG_NAME_PICTURE))) +  // 12 points
            sumInt(values(strseq(FP_LANG_IDENTIFY_CONCEPT, 1, N_LANG_IDENTIFY_CONCEPT))) +  // 4 points
            valueInt(FN_LANG_READ_WORDS_ALOUD);  // 1 point
}


int Ace3::getVisuospatialScore() const
{
    return valueInt(FN_VSP_COPY_INFINITY) +  // 1 point
            valueInt(FN_VSP_COPY_CUBE) +  // 2 points
            valueInt(FN_VSP_DRAW_CLOCK) +  // 5 points
            sumInt(values(strseq(FP_VSP_COUNT_DOTS, 1, N_VSP_COUNT_DOTS))) +
            sumInt(values(strseq(FP_VSP_IDENTIFY_LETTER, 1, N_VSP_IDENTIFY_LETTER)));
}


int Ace3::totalScore() const
{
    return getAttnScore() +
            getMemScore() +
            getFluencyScore() +
            getLangScore() +
            getVisuospatialScore();
}


bool Ace3::isRecognitionComplete() const
{
    const int recall1 = valueInt(strnum(FP_MEM_RECALL_ADDRESS, 1));
    const int recall2 = valueInt(strnum(FP_MEM_RECALL_ADDRESS, 2));
    const int recall3 = valueInt(strnum(FP_MEM_RECALL_ADDRESS, 3));
    const int recall4 = valueInt(strnum(FP_MEM_RECALL_ADDRESS, 4));
    const int recall5 = valueInt(strnum(FP_MEM_RECALL_ADDRESS, 5));
    const int recall6 = valueInt(strnum(FP_MEM_RECALL_ADDRESS, 6));
    const int recall7 = valueInt(strnum(FP_MEM_RECALL_ADDRESS, 7));
    const bool recog1present = !valueIsNull(strnum(FP_MEM_RECOGNIZE_ADDRESS_CHOICE, 1));
    const bool recog2present = !valueIsNull(strnum(FP_MEM_RECOGNIZE_ADDRESS_CHOICE, 2));
    const bool recog3present = !valueIsNull(strnum(FP_MEM_RECOGNIZE_ADDRESS_CHOICE, 3));
    const bool recog4present = !valueIsNull(strnum(FP_MEM_RECOGNIZE_ADDRESS_CHOICE, 4));
    const bool recog5present = !valueIsNull(strnum(FP_MEM_RECOGNIZE_ADDRESS_CHOICE, 5));
    return (
        ((recall1 && recall2) || recog1present) &&  // forename, surname
        (recall3 || recog2present) &&  // number
        ((recall4 && recall5) || recog3present) &&  // streetname, streettype
        (recall6 || recog4present) &&  // city
        (recall7 || recog5present)  // county
    );
}


// ============================================================================
// Signal handlers
// ============================================================================

void Ace3::langPracticeChanged(const FieldRef* fieldref)
{
    // qDebug() << Q_FUNC_INFO;
    if (!m_questionnaire) {
        return;
    }
    const QVariant value = fieldref->value();
    const bool visible = !eq(value, false);
    const bool mandatory = value.toBool();
    for (int i = 1; i <= N_LANG_FOLLOW_CMD; ++i) {
        fieldRef(strnum(FP_LANG_FOLLOW_CMD, i))->setMandatory(mandatory);
    }
    m_questionnaire->setVisibleByTag(TAG_EL_LANG_OPTIONAL_COMMAND, visible,
                                     false, TAG_PG_LANG_COMMANDS_SENTENCES);
    m_questionnaire->setVisibleByTag(TAG_EL_LANG_NOT_SHOWN, !visible,
                                     false, TAG_PG_LANG_COMMANDS_SENTENCES);
}


void Ace3::updateAddressRecognition()
{
    // Parameter "const FieldRef* fieldref" not needed;
    // http://doc.qt.io/qt-5/signalsandslots.html
    // "... a slot may have a shorter signature than the signal it receives"

    if (!m_questionnaire) {
        return;
    }

    // We show something if we failed to recall all parts of it.
    // This function also updates the scores, so this code is all in one place.
    const bool name_correct = valueInt(strnum(FP_MEM_RECALL_ADDRESS, 1)) &&
            valueInt(strnum(FP_MEM_RECALL_ADDRESS, 2));
    const bool number_correct = valueInt(strnum(FP_MEM_RECALL_ADDRESS, 3));
    const bool street_correct = valueInt(strnum(FP_MEM_RECALL_ADDRESS, 4)) &&
            valueInt(strnum(FP_MEM_RECALL_ADDRESS, 5));
    const bool town_correct = valueInt(strnum(FP_MEM_RECALL_ADDRESS, 6));
    const bool county_correct = valueInt(strnum(FP_MEM_RECALL_ADDRESS, 7));
    const bool recog_required = !name_correct || !number_correct ||
            !street_correct || !town_correct || !county_correct;
    const bool recog_superfluous = !recog_required;
    m_questionnaire->setVisibleByTag(TAG_RECOG_NAME, !name_correct,
                                     false, TAG_PG_MEM_PROMPTED_RECALL);
    m_questionnaire->setVisibleByTag(TAG_RECOG_NUMBER, !number_correct,
                                     false, TAG_PG_MEM_PROMPTED_RECALL);
    m_questionnaire->setVisibleByTag(TAG_RECOG_STREET, !street_correct,
                                     false, TAG_PG_MEM_PROMPTED_RECALL);
    m_questionnaire->setVisibleByTag(TAG_RECOG_TOWN, !town_correct,
                                     false, TAG_PG_MEM_PROMPTED_RECALL);
    m_questionnaire->setVisibleByTag(TAG_RECOG_COUNTY, !county_correct,
                                     false, TAG_PG_MEM_PROMPTED_RECALL);
    m_questionnaire->setVisibleByTag(TAG_RECOG_REQUIRED, recog_required,
                                     false, TAG_PG_MEM_PROMPTED_RECALL);
    m_questionnaire->setVisibleByTag(TAG_RECOG_SUPERFLUOUS, recog_superfluous,
                                     false, TAG_PG_MEM_PROMPTED_RECALL);

    // - bool to int 0/1 is guaranteed, so no need for " ? 1 : 0";
    //   http://stackoverflow.com/questions/5369770/bool-to-int-conversion
    const int recogscore1 = name_correct || valueString(
                strnum(FP_MEM_RECOGNIZE_ADDRESS_CHOICE, 1)) == CHOICE_B;
    const int recogscore2 = number_correct || valueString(
                strnum(FP_MEM_RECOGNIZE_ADDRESS_CHOICE, 2)) == CHOICE_B;
    const int recogscore3 = street_correct || valueString(
                strnum(FP_MEM_RECOGNIZE_ADDRESS_CHOICE, 3)) == CHOICE_C;
    const int recogscore4 = town_correct || valueString(
                strnum(FP_MEM_RECOGNIZE_ADDRESS_CHOICE, 4)) == CHOICE_B;
    const int recogscore5 = county_correct || valueString(
                strnum(FP_MEM_RECOGNIZE_ADDRESS_CHOICE, 5)) == CHOICE_A;
    setValue(strnum(FP_MEM_RECOGNIZE_ADDRESS_SCORE, 1), recogscore1);
    setValue(strnum(FP_MEM_RECOGNIZE_ADDRESS_SCORE, 2), recogscore2);
    setValue(strnum(FP_MEM_RECOGNIZE_ADDRESS_SCORE, 3), recogscore3);
    setValue(strnum(FP_MEM_RECOGNIZE_ADDRESS_SCORE, 4), recogscore4);
    setValue(strnum(FP_MEM_RECOGNIZE_ADDRESS_SCORE, 5), recogscore5);
}