15.1.491. tablet_qt/questionnairelib/qumcq.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 "qumcq.h"

#include <QWidget>

#include "common/cssconst.h"
#include "layouts/flowlayouthfw.h"
#include "layouts/layouts.h"
#include "lib/uifunc.h"
#include "questionnairelib/mcqfunc.h"
#include "questionnairelib/questionnaire.h"
#include "questionnairelib/questionnairefunc.h"
#include "widgets/basewidget.h"
#include "widgets/booleanwidget.h"
#include "widgets/clickablelabelwordwrapwide.h"
#include "widgets/labelwordwrapwide.h"

QuMcq::QuMcq(
    FieldRefPtr fieldref,
    const NameValueOptions& options,
    const QStringList* label_styles,
    QObject* parent
) :
    QuElement(parent),
    m_fieldref(fieldref),
    m_options(options),
    m_randomize(false),
    m_show_instruction(false),
    m_horizontal(false),
    m_as_text_button(false),
    m_bold(false)
{
    m_options.validateOrDie();
    if (label_styles) {
        m_label_styles = *label_styles;
        Q_ASSERT(m_label_styles.size() == m_options.size());
    }

    Q_ASSERT(m_fieldref);
    connect(
        m_fieldref.data(),
        &FieldRef::valueChanged,
        this,
        &QuMcq::fieldValueChanged
    );
    connect(
        m_fieldref.data(),
        &FieldRef::mandatoryChanged,
        this,
        &QuMcq::fieldValueChanged
    );
}

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

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

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

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

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

bool QuMcq::setOptionNames(const NameValueOptions& options)
{
    if (m_randomize || !options.valuesMatch(m_options)) {
        qWarning() << Q_FUNC_INFO
                   << "Attempt to change to incompatible options; prohibited";
        return false;
    }
    m_options = options;

    // Dynamic changes, if required:
    const int s = m_options.size();
    if (s > m_boolean_widgets.size() || s > m_label_widgets.size()) {
        // Widgets not yet created.
        return true;
    }
    for (int i = 0; i < s; ++i) {
        const QString& text = m_options.nameFromIndex(i);
        QPointer<BooleanWidget> bw = m_boolean_widgets[i];
        if (bw) {
            bw->setText(text);
        }
        QPointer<ClickableLabelWordWrapWide> lw = m_label_widgets[i];
        if (lw) {
            lw->setText(text);
        }
    }

    return true;
}

QPointer<QWidget> QuMcq::makeWidget(Questionnaire* questionnaire)
{
    // Clear old stuff (BEWARE: "empty()" = "isEmpty()" != "clear()")
    m_boolean_widgets.clear();
    m_label_widgets.clear();

    // Randomize?
    if (m_randomize) {
        m_options.shuffle();
    }

    const bool read_only = questionnaire->readOnly();

    // Actual MCQ: widget containing {widget +/- label} for each option
    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);
    // QGridLayout, but not QVBoxLayout or QHBoxLayout, can use addChildLayout;
    // the latter use addWidget.
    // FlowLayout is better than QHBoxLayout.

    for (int position = 0; position < m_options.size(); ++position) {
        const NameValuePair& nvp = m_options.atPosition(position);

        // MCQ touch-me widget
        QPointer<BooleanWidget> w = new BooleanWidget();
        w->setReadOnly(read_only);
        w->setAppearance(
            m_as_text_button ? BooleanWidget::Appearance::Text
                             : BooleanWidget::Appearance::Radio
        );
        if (m_as_text_button) {
            w->setText(nvp.name());
            w->setBold(m_bold);
        }
        if (!read_only) {
            // Safe object lifespan signal: can use std::bind
            connect(
                w,
                &BooleanWidget::clicked,
                std::bind(&QuMcq::clicked, this, position)
            );
        }
        m_boolean_widgets.append(w);

        if (m_as_text_button) {
            mainlayout->addWidget(w);
            mainlayout->setAlignment(w, Qt::AlignTop);
            m_label_widgets.append(nullptr);
        } else {
            // MCQ option label
            // Even in a horizontal layout, encapsulating widget/label pairs
            // prevents them being split apart.
            auto itemwidget = new QWidget();
            auto namelabel = new ClickableLabelWordWrapWide(nvp.name());
            m_label_widgets.append(namelabel);
            namelabel->setEnabled(!read_only);
            const int fontsize
                = questionnaire->fontSizePt(uiconst::FontSize::Normal);
            const bool italic = false;
            QString css = uifunc::textCSS(fontsize, m_bold, italic);

            if (!m_label_styles.isEmpty()) {
                const int index = m_options.indexFromPosition(position);
                css += m_label_styles[index];
            }

            namelabel->setStyleSheet(css);

            if (!read_only) {
                // Safe object lifespan signal: can use std::bind
                connect(
                    namelabel,
                    &ClickableLabelWordWrapWide::clicked,
                    std::bind(&QuMcq::clicked, this, position)
                );
            }
            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);
        }
        // The FlowLayout seems to ignore vertical centring. This makes it look
        // slightly dumb when one label has much longer text than the others,
        // but overall this is the best compromise I've found.
    }

    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);
        auto instructions = new LabelWordWrapWide(tr("Pick one:"));
        instructions->setObjectName(cssconst::MCQ_INSTRUCTION);
        layout_w_instr->addWidget(instructions);
        layout_w_instr->addWidget(mainwidget);
        QPointer<QWidget> widget_w_instr = new BaseWidget();
        widget_w_instr->setLayout(layout_w_instr);
        widget_w_instr->setSizePolicy(
            QSizePolicy::Preferred, QSizePolicy::Maximum
        );
        final_widget = widget_w_instr;
    } else {
        final_widget = mainwidget;
    }

    setFromField();

    return final_widget;
}

void QuMcq::clicked(const int position)
{
    if (!m_options.validIndex(position)) {
        qWarning() << Q_FUNC_INFO << "- out of range";
        return;
    }
    const QVariant newvalue = m_options.valueFromPosition(position);
    const bool changed = m_fieldref->setValue(newvalue);
    // ... Will trigger valueChanged
    if (changed) {
        emit elementValueChanged();
    }
}

void QuMcq::setFromField()
{
    fieldValueChanged(m_fieldref.data());
}

void QuMcq::fieldValueChanged(const FieldRef* fieldref)
{
    mcqfunc::setResponseWidgets(m_options, m_boolean_widgets, fieldref);
}

FieldRefPtrList QuMcq::fieldrefs() const
{
    return FieldRefPtrList{m_fieldref};
}