15.1.501. tablet_qt/questionnairelib/qumcqgridsingleboolean.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 "qumcqgridsingleboolean.h"

#include "common/cssconst.h"
#include "db/fieldref.h"
#include "questionnairelib/mcqfunc.h"
#include "questionnairelib/questionnaire.h"
#include "questionnairelib/qumcqgridsinglebooleansignaller.h"
#include "widgets/basewidget.h"
#include "widgets/booleanwidget.h"

QuMcqGridSingleBoolean::QuMcqGridSingleBoolean(
    const QVector<QuestionWithTwoFields>& questions_with_fields,
    const NameValueOptions& mcq_options,
    const QString& boolean_text,
    QObject* parent
) :
    QuElement(parent),
    m_boolean_left(false),
    m_questions_with_fields(questions_with_fields),
    m_mcq_options(mcq_options),
    m_boolean_text(boolean_text),
    m_question_width(-1),
    m_boolean_width(-1),
    m_expand(false),
    m_stripy(true)
{
    m_mcq_options.validateOrDie();
    // each QuestionWithTwoFields will have asserted on construction

    for (int qi = 0; qi < m_questions_with_fields.size(); ++qi) {
        FieldRefPtr mcq_fieldref
            = m_questions_with_fields.at(qi).firstFieldRef();
        // DANGEROUS OBJECT LIFESPAN SIGNAL: do not use std::bind
        auto sig = new QuMcqGridSingleBooleanSignaller(this, qi);
        m_signallers.append(sig);

        connect(
            mcq_fieldref.data(),
            &FieldRef::valueChanged,
            sig,
            &QuMcqGridSingleBooleanSignaller::mcqFieldValueOrMandatoryChanged
        );
        connect(
            mcq_fieldref.data(),
            &FieldRef::mandatoryChanged,
            sig,
            &QuMcqGridSingleBooleanSignaller::mcqFieldValueOrMandatoryChanged
        );

        FieldRefPtr bool_fieldref
            = m_questions_with_fields.at(qi).secondFieldRef();
        connect(
            bool_fieldref.data(),
            &FieldRef::valueChanged,
            sig,
            &QuMcqGridSingleBooleanSignaller::
                booleanFieldValueOrMandatoryChanged
        );
        connect(
            bool_fieldref.data(),
            &FieldRef::mandatoryChanged,
            sig,
            &QuMcqGridSingleBooleanSignaller::
                booleanFieldValueOrMandatoryChanged
        );
    }
}

QuMcqGridSingleBoolean::~QuMcqGridSingleBoolean()
{
    while (!m_signallers.isEmpty()) {
        delete m_signallers.takeAt(0);
    }
}

QuMcqGridSingleBoolean*
    QuMcqGridSingleBoolean::setBooleanLeft(const bool boolean_left)
{
    m_boolean_left = boolean_left;
    return this;
}

QuMcqGridSingleBoolean* QuMcqGridSingleBoolean::setWidth(
    const int question_width,
    const QVector<int>& mcq_option_widths,
    const int boolean_width
)
{
    if (mcq_option_widths.size() != m_mcq_options.size()) {
        qWarning() << Q_FUNC_INFO << "Bad mcq_option_widths; command ignored";
        return this;
    }
    m_question_width = question_width;
    m_mcq_option_widths = mcq_option_widths;
    m_boolean_width = boolean_width;
    return this;
}

QuMcqGridSingleBoolean* QuMcqGridSingleBoolean::setTitle(const QString& title)
{
    m_title = title;
    return this;
}

QuMcqGridSingleBoolean* QuMcqGridSingleBoolean::setSubtitles(
    const QVector<McqGridSubtitle>& subtitles
)
{
    m_subtitles = subtitles;
    return this;
}

QuMcqGridSingleBoolean* QuMcqGridSingleBoolean::setExpand(const bool expand)
{
    m_expand = expand;
    return this;
}

QuMcqGridSingleBoolean* QuMcqGridSingleBoolean::setStripy(const bool stripy)
{
    m_stripy = stripy;
    return this;
}

void QuMcqGridSingleBoolean::setFromFields()
{
    for (int qi = 0; qi < m_questions_with_fields.size(); ++qi) {
        mcqFieldValueOrMandatoryChanged(
            qi, m_questions_with_fields.at(qi).firstFieldRef().data()
        );
        booleanFieldValueOrMandatoryChanged(
            qi, m_questions_with_fields.at(qi).secondFieldRef().data()
        );
    }
}

int QuMcqGridSingleBoolean::mcqColnum(const int value_index) const
{
    return (m_boolean_left ? 4 : 2) + value_index;
}

int QuMcqGridSingleBoolean::booleanColnum() const
{
    return m_boolean_left ? 2 : (3 + m_mcq_options.size());
}

int QuMcqGridSingleBoolean::spacercol(const bool first) const
{
    return first ? 1 : ((m_boolean_left ? mcqColnum(0) : booleanColnum()) - 1);
}

void QuMcqGridSingleBoolean::addOptions(GridLayout* grid, const int row)
{
    for (int i = 0; i < m_mcq_options.size(); ++i) {
        mcqfunc::addOption(
            grid, row, mcqColnum(i), m_mcq_options.atPosition(i).name()
        );
    }
    mcqfunc::addOption(grid, row, booleanColnum(), m_boolean_text);
}

QPointer<QWidget>
    QuMcqGridSingleBoolean::makeWidget(Questionnaire* questionnaire)
{
    const bool read_only = questionnaire->readOnly();
    m_mcq_widgets.clear();
    m_boolean_widgets.clear();

    auto grid = new GridLayout();
    grid->setContentsMargins(uiconst::NO_MARGINS);
    grid->setHorizontalSpacing(uiconst::MCQGRID_HSPACING);
    grid->setVerticalSpacing(uiconst::MCQGRID_VSPACING);

    const int n_subtitles = m_subtitles.size();
    const int n_rows = 1 + n_subtitles + m_questions_with_fields.size();
    const int n_cols = m_mcq_options.size() + 4;
    const Qt::Alignment response_align = mcqfunc::response_widget_align;
    const int n_options = m_mcq_options.size();
    int row = 0;

    // Title row
    mcqfunc::addOptionBackground(grid, row, 0, n_cols);
    mcqfunc::addTitle(grid, row, m_title);
    addOptions(grid, row);
    ++row;  // new row after title/option text

    // Main question rows (with any preceding subtitles)
    for (int qi = 0; qi < m_questions_with_fields.size(); ++qi) {

        // Any preceding subtitles?
        for (int s = 0; s < n_subtitles; ++s) {
            const McqGridSubtitle& sub = m_subtitles.at(s);
            if (sub.pos() == qi) {
                // Yes. Add a subtitle row.
                mcqfunc::addOptionBackground(grid, row, 0, n_cols);
                mcqfunc::addSubtitle(grid, row, sub.string());
                if (sub.repeatOptions()) {
                    addOptions(grid, row);
                }
                ++row;  // new row after subtitle
            }
        }

        if (m_stripy) {
            mcqfunc::addStripeBackground(grid, row, 0, n_cols);
        }

        // The question
        mcqfunc::addQuestion(
            grid, row, m_questions_with_fields.at(qi).question()
        );

        // The response widgets
        QVector<QPointer<BooleanWidget>> question_widgets;
        for (int vi = 0; vi < n_options; ++vi) {
            QPointer<BooleanWidget> w = new BooleanWidget();
            w->setAppearance(BooleanWidget::Appearance::Radio);
            w->setReadOnly(read_only);
            if (!read_only) {
                // Safe object lifespan signal: can use std::bind
                connect(
                    w,
                    &BooleanWidget::clicked,
                    std::bind(
                        &QuMcqGridSingleBoolean::mcqClicked, this, qi, vi
                    )
                );
            }
            grid->addWidget(w, row, mcqColnum(vi), response_align);
            question_widgets.append(w);
        }
        m_mcq_widgets.append(question_widgets);

        QPointer<BooleanWidget> bw = new BooleanWidget();
        bw->setAppearance(BooleanWidget::Appearance::CheckRed);
        bw->setReadOnly(read_only);
        if (!read_only) {
            // Safe object lifespan signal: can use std::bind
            connect(
                bw,
                &BooleanWidget::clicked,
                std::bind(&QuMcqGridSingleBoolean::booleanClicked, this, qi)
            );
        }
        grid->addWidget(bw, row, booleanColnum(), response_align);
        m_boolean_widgets.append(bw);

        ++row;  // new row after question/response widgets
    }

    // Set widths, if asked
    if (m_question_width > 0
        && m_mcq_option_widths.size() == m_mcq_options.size()) {
        grid->setColumnStretch(0, m_question_width);
        for (int i = 0; i < m_mcq_option_widths.size(); ++i) {
            grid->setColumnStretch(mcqColnum(i), m_mcq_option_widths.at(i));
        }
        grid->setColumnStretch(booleanColnum(), m_boolean_width);
    }

    // Vertical lines
    mcqfunc::addVerticalLine(grid, spacercol(true), n_rows);
    mcqfunc::addVerticalLine(grid, spacercol(false), n_rows);

    QPointer<QWidget> widget = new BaseWidget();
    widget->setLayout(grid);
    widget->setObjectName(cssconst::MCQ_GRID_SINGLE_BOOLEAN);
    if (m_expand) {
        widget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);
    } else {
        widget->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
    }

    setFromFields();

    return widget;
}

FieldRefPtrList QuMcqGridSingleBoolean::fieldrefs() const
{
    FieldRefPtrList refs;
    for (const QuestionWithTwoFields& q : m_questions_with_fields) {
        refs.append(q.firstFieldRef());
        refs.append(q.secondFieldRef());
    }
    return refs;
}

void QuMcqGridSingleBoolean::mcqClicked(
    const int question_index, const int value_index
)
{
    if (question_index < 0
        || question_index >= m_questions_with_fields.size()) {
        qWarning() << Q_FUNC_INFO << "Bad question_index:" << question_index;
        return;
    }
    if (!m_mcq_options.validIndex(value_index)) {
        qWarning() << Q_FUNC_INFO << "- out of range";
        return;
    }
    const QVariant newvalue = m_mcq_options.valueFromIndex(value_index);
    FieldRefPtr fieldref
        = m_questions_with_fields.at(question_index).firstFieldRef();
    const bool changed = fieldref->setValue(newvalue);
    // ... Will trigger valueChanged
    if (changed) {
        emit elementValueChanged();
    }
}

void QuMcqGridSingleBoolean::booleanClicked(const int question_index)
{
    if (question_index < 0
        || question_index >= m_questions_with_fields.size()) {
        qWarning() << Q_FUNC_INFO << "Bad question_index:" << question_index;
        return;
    }
    FieldRefPtr fieldref
        = m_questions_with_fields.at(question_index).secondFieldRef();
    mcqfunc::toggleBooleanField(fieldref.data());
    emit elementValueChanged();
}

void QuMcqGridSingleBoolean::mcqFieldValueOrMandatoryChanged(
    const int question_index, const FieldRef* fieldref
)
{
    if (question_index < 0
        || question_index >= m_questions_with_fields.size()) {
        qWarning() << Q_FUNC_INFO << "Bad question_index:" << question_index;
        return;
    }
    if (question_index >= m_mcq_widgets.size()) {
        return;
    }
    const QVector<QPointer<BooleanWidget>>& question_widgets
        = m_mcq_widgets.at(question_index);

    mcqfunc::setResponseWidgets(m_mcq_options, question_widgets, fieldref);
}

void QuMcqGridSingleBoolean::booleanFieldValueOrMandatoryChanged(
    const int question_index, const FieldRef* fieldref
)
{
    if (question_index < 0
        || question_index >= m_questions_with_fields.size()) {
        qWarning() << Q_FUNC_INFO << "Bad question_index:" << question_index;
        return;
    }
    if (question_index >= m_boolean_widgets.size()) {
        return;
    }
    QPointer<BooleanWidget> bw = m_boolean_widgets.at(question_index);

    bw->setValue(fieldref->value(), fieldref->mandatory());
}