15.1.501. tablet_qt/questionnairelib/qumultipleresponse.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/>.
*/

#include "qumultipleresponse.h"
#include "common/cssconst.h"
#include "db/fieldref.h"
#include "layouts/layouts.h"
#include "layouts/flowlayouthfw.h"
#include "lib/uifunc.h"
#include "maths/ccrandom.h"
#include "questionnairelib/questionnaire.h"
#include "widgets/basewidget.h"
#include "widgets/booleanwidget.h"
#include "widgets/clickablelabelwordwrapwide.h"
#include "widgets/labelwordwrapwide.h"


QuMultipleResponse::QuMultipleResponse(
        const QVector<QuestionWithOneField>& items, QObject* parent) :
    QuElement(parent),
    m_items(items),
    m_minimum_answers(0),
    m_maximum_answers(-1),
    m_randomize(false),
    m_show_instruction(true),
    m_horizontal(false),
    m_as_text_button(false),
    m_bold(false),
    m_instruction_label(nullptr)
{
    // Connect fieldrefs at widget build time, for simplicity.
}


QuMultipleResponse::QuMultipleResponse(
        std::initializer_list<QuestionWithOneField> items, QObject* parent) :
    QuMultipleResponse(QVector<QuestionWithOneField>(items), parent)  // delegating constructor
{
}


QuMultipleResponse::QuMultipleResponse(QObject* parent) :
    QuMultipleResponse(QVector<QuestionWithOneField>(), parent)  // delegating constructor
{
}


QuMultipleResponse* QuMultipleResponse::addItem(
        const QuestionWithOneField& item)
{
    m_items.append(item);
    return this;
}


QuMultipleResponse* QuMultipleResponse::setMinimumAnswers(int minimum_answers)
{
    if (minimum_answers < 0) {
        minimum_answers = 0;
    }
    const bool changed = minimum_answers != m_minimum_answers;
    if (changed) {
        m_minimum_answers = minimum_answers;
        minOrMaxChanged();
    }
    return this;
}


QuMultipleResponse* QuMultipleResponse::setMaximumAnswers(int maximum_answers)
{
    if (maximum_answers == 0) {  // dumb value, use -1 for don't care
        maximum_answers = -1;
    }
    const bool changed = maximum_answers != m_maximum_answers;
    if (changed) {
        m_maximum_answers = maximum_answers;
        minOrMaxChanged();
    }
    return this;
}


void QuMultipleResponse::minOrMaxChanged()
{
    if (!m_widgets.empty()) {
        // we're live
        if (m_show_instruction && m_instruction_label &&
                m_instruction.isEmpty()) {
            m_instruction_label->setText(defaultInstruction());
        }
        fieldValueChanged();  // may change mandatory colour
        emit elementValueChanged();  // may change page "next" status etc.
    }
}


QuMultipleResponse* QuMultipleResponse::setRandomize(const bool randomize)
{
    m_randomize = randomize;
    return this;
}


QuMultipleResponse* QuMultipleResponse::setShowInstruction(
        bool show_instruction)
{
    m_show_instruction = show_instruction;
    return this;
}


QuMultipleResponse* QuMultipleResponse::setInstruction(
        const QString& instruction)
{
    m_instruction = instruction;
    return this;
}


QuMultipleResponse* QuMultipleResponse::setHorizontal(const bool horizontal)
{
    m_horizontal = horizontal;
    return this;
}


QuMultipleResponse* QuMultipleResponse::setAsTextButton(
        const bool as_text_button)
{
    m_as_text_button = as_text_button;
    return this;
}


QuMultipleResponse* QuMultipleResponse::setBold(const bool bold)
{
    m_bold = bold;
    return this;
}


QPointer<QWidget> QuMultipleResponse::makeWidget(Questionnaire* questionnaire)
{
    // Clear old stuff
    m_widgets.clear();

    // Randomize?
    if (m_randomize) {
        ccrandom::shuffle(m_items);
    }

    const bool read_only = questionnaire->readOnly();

    QPointer<QWidget> mainwidget = new BaseWidget();
    QLayout* mainlayout;
    if (m_horizontal) {
        mainlayout = new FlowLayoutHfw();
    } else {
        mainlayout = new VBoxLayout();
    }
    mainlayout->setContentsMargins(uiconst::NO_MARGINS);
    mainwidget->setLayout(mainlayout);

    for (int i = 0; i < m_items.size(); ++i) {
        const QuestionWithOneField& item = m_items.at(i);

        // Widget
        QPointer<BooleanWidget> w = new BooleanWidget();
        w->setReadOnly(read_only);
        w->setAppearance(m_as_text_button
                         ? BooleanWidget::Appearance::Text
                         : BooleanWidget::Appearance::CheckRed);
        if (m_as_text_button) {
            w->setText(item.text());
            w->setBold(m_bold);
        }
        if (!read_only) {
            // Safe object lifespan signal: can use std::bind
            connect(w, &BooleanWidget::clicked,
                    std::bind(&QuMultipleResponse::clicked, this, i));
        }
        m_widgets.append(w);

        // Layout, +/- label
        if (m_as_text_button) {
            mainlayout->addWidget(w);
            mainlayout->setAlignment(w, Qt::AlignTop);
        } else {
            // cf. QuMCQ
            auto itemwidget = new QWidget();
            ClickableLabelWordWrapWide* namelabel = new ClickableLabelWordWrapWide(item.text());
            namelabel->setEnabled(!read_only);
            const int fontsize = questionnaire->fontSizePt(uiconst::FontSize::Normal);
            const bool italic = false;
            const QString css = uifunc::textCSS(fontsize, m_bold, italic);
            namelabel->setStyleSheet(css);
            if (!read_only) {
                // Safe object lifespan signal: can use std::bind
                connect(namelabel, &ClickableLabelWordWrapWide::clicked,
                        std::bind(&QuMultipleResponse::clicked, this, i));
            }
            auto itemlayout = new HBoxLayout();
            itemlayout->setContentsMargins(uiconst::NO_MARGINS);
            itemwidget->setLayout(itemlayout);
            itemlayout->addWidget(w, 0, Qt::AlignTop);
            itemlayout->addWidget(namelabel, 0, Qt::AlignVCenter);  // different
            itemlayout->addStretch();

            mainlayout->addWidget(itemwidget);
            mainlayout->setAlignment(itemwidget, Qt::AlignTop);
        }

        // Field-to-this connections
        // You can't use connect() with a std::bind and simultaneously
        // specify a connection type such as Qt::UniqueConnection (which
        // only works with QObject, I think).
        // However, you can use a QSignalMapper. Then you can wipe it before
        // connecting, removing the need for Qt::UniqueConnection.
        // Finally, you need to disambiguate the slot with e.g.
        //      void (QSignalMapper::*map_signal)() = &QSignalMapper::map;
        // ... But in the end, all widgets may need to be updated when a single
        // value changes (based on the number required), so all this was
        // pointless and we can use a single signal with no parameters.

        FieldRef* fr = item.fieldref().data();
        connect(fr, &FieldRef::valueChanged,
                this, &QuMultipleResponse::fieldValueChanged,
                Qt::UniqueConnection);
        connect(fr, &FieldRef::mandatoryChanged,
                this, &QuMultipleResponse::fieldValueChanged,
                Qt::UniqueConnection);
    }

    QPointer<QWidget> final_widget;
    if (m_show_instruction) {
        // Higher-level widget containing {instructions, actual MCQ}
        auto layout_w_instr = new VBoxLayout();
        layout_w_instr->setContentsMargins(uiconst::NO_MARGINS);
        QString instruction = m_instruction.isEmpty() ? defaultInstruction()
                                                      : m_instruction;
        m_instruction_label = new LabelWordWrapWide(instruction);
        m_instruction_label->setObjectName(cssconst::MCQ_INSTRUCTION);
        layout_w_instr->addWidget(m_instruction_label);
        layout_w_instr->addWidget(mainwidget);
        QPointer<QWidget> widget_w_instr = new QWidget();
        widget_w_instr->setLayout(layout_w_instr);
        final_widget = widget_w_instr;
    } else {
        final_widget = mainwidget;
    }

    setFromFields();

    return final_widget;
}


void QuMultipleResponse::clicked(const int index)
{
    if (!validIndex(index)) {
        qWarning() << Q_FUNC_INFO << "- out of range";
        return;
    }
    const bool at_max = nTrueAnswers() >= maximumAnswers();
    bool changed = false;
    const QuestionWithOneField item = m_items.at(index);
    FieldRefPtr fieldref = item.fieldref();
    const QVariant value = fieldref->value();
    QVariant newvalue;
    if (value.isNull()) {  // NULL -> true
        if (!at_max) {
            newvalue = true;
            changed = true;
        }
    } else if (value.toBool()) {  // true -> false
        newvalue = false;
        changed = true;
    } else {  // false -> true
        if (!at_max) {
            newvalue = true;
            changed = true;
        }
    }
    if (!changed) {
        return;
    }
    fieldref->setValue(newvalue);  // Will trigger valueChanged
    emit elementValueChanged();
}


void QuMultipleResponse::setFromFields()
{
    fieldValueChanged();
}


void QuMultipleResponse::fieldValueChanged()
{
    const bool need_more = nTrueAnswers() < minimumAnswers();
    for (int i = 0; i < m_items.size(); ++i) {
        FieldRefPtr fieldref = m_items.at(i).fieldref();
        const QVariant value = fieldref->value();
        QPointer<BooleanWidget> w = m_widgets.at(i);
        if (!w) {
            qCritical() << Q_FUNC_INFO << "- defunct pointer!";
            continue;
        }
        if (!value.isNull() && value.toBool()) {
            // true
            w->setState(BooleanWidget::State::True);
        } else {
            // null or false (both look like blanks)
            w->setState(need_more ? BooleanWidget::State::NullRequired
                                  : BooleanWidget::State::Null);
            // We ignore mandatory properties on the fieldref, since we have a
            // minimum/maximum specified for them collectively.
            // Then we override missingInput() so that the QuPage uses our
            // information, not the fieldref information.
        }
    }
}


FieldRefPtrList QuMultipleResponse::fieldrefs() const
{
    FieldRefPtrList fieldrefs;
    for (const QuestionWithOneField& item : m_items) {
        fieldrefs.append(item.fieldref());
    }
    return fieldrefs;
}


int QuMultipleResponse::minimumAnswers() const
{
    return m_minimum_answers;
}


int QuMultipleResponse::maximumAnswers() const
{
    if (m_maximum_answers < 0) {  // the "don't care" value is -1
        return m_items.size();
    }
    return qMin(m_items.size(), m_maximum_answers);
}


QString QuMultipleResponse::defaultInstruction() const
{
    const int minimum = minimumAnswers();
    const int maximum = maximumAnswers();
    if (minimum == maximum) {
        return QString("Choose %1:").arg(minimum);
    }
    if (m_minimum_answers <= 0) {
        return QString("Choose up to %1:").arg(maximum);
    }
    if (m_maximum_answers < 0) {
        return QString("Choose %1 or more:").arg(minimum);
    }
    return QString("Choose %1–%2:").arg(minimum).arg(maximum);
}


bool QuMultipleResponse::validIndex(const int index)
{
    return index >= 0 && index < m_items.size();
}


int QuMultipleResponse::nTrueAnswers() const
{
    int n = 0;
    for (const QuestionWithOneField& item : m_items) {
        const QVariant value = item.fieldref()->value();
        if (!value.isNull() && value.toBool()) {
            n += 1;
        }
    }
    return n;
}


bool QuMultipleResponse::missingInput() const
{
    return nTrueAnswers() < minimumAnswers();
}