15.1.529. tablet_qt/questionnairelib/qutextedit.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 "qutextedit.h"

#include <QTimer>

#include "common/textconst.h"
#include "lib/timerfunc.h"
#include "lib/widgetfunc.h"
#include "qobjects/focuswatcher.h"
#include "questionnairelib/questionnaire.h"
#include "widgets/growingtextedit.h"


const int WRITE_DELAY_MS = 400;


QuTextEdit::QuTextEdit(
    FieldRefPtr fieldref, const bool accept_rich_text, QObject* parent
) :
    QuElement(parent),
    m_fieldref(fieldref),
    m_accept_rich_text(accept_rich_text),
    m_allow_tabs_in_content(false),
    m_hint(TextConst::defaultHintText()),
#ifdef QUTEXTEDIT_USE_PLAIN_TEXT_EDITOR
    m_plain_editor(nullptr),
#endif
    m_rich_editor(nullptr),
    m_ignore_widget_signal(false),
    m_focus_watcher(nullptr)
{
    Q_ASSERT(m_fieldref);
    timerfunc::makeSingleShotTimer(m_timer);
    connect(m_timer.data(), &QTimer::timeout, this, &QuTextEdit::textChanged);
    connect(
        m_fieldref.data(),
        &FieldRef::valueChanged,
        this,
        &QuTextEdit::fieldValueChanged
    );
    connect(
        m_fieldref.data(),
        &FieldRef::mandatoryChanged,
        this,
        &QuTextEdit::fieldValueChanged
    );
}

QuTextEdit* QuTextEdit::setAllowTabsInContent(const bool allow_tabs_in_content)
{
    m_allow_tabs_in_content = allow_tabs_in_content;
    return this;
}

QuTextEdit* QuTextEdit::setHint(const QString& hint)
{
    m_hint = hint;
    return this;
}

void QuTextEdit::setFromField()
{
    fieldValueChanged(m_fieldref.data(), nullptr);
    // special; pretend "it didn't come from us" to disable the efficiency
    // check in fieldValueChanged
}

QPointer<QWidget> QuTextEdit::makeWidget(Questionnaire* questionnaire)
{
    const bool read_only = questionnaire->readOnly();
    // A bit ugly, but since QPlainTextEdit and QTextEdit share no useful
    // parent class...
#ifdef QUTEXTEDIT_USE_PLAIN_TEXT_EDITOR
    if (m_accept_rich_text) {
#endif
        m_rich_editor = new GrowingTextEdit();
        m_rich_editor->setEnabled(!read_only);
        m_rich_editor->setAcceptRichText(m_accept_rich_text);
        m_rich_editor->setPlaceholderText(m_hint);
        m_rich_editor->setTabChangesFocus(!m_allow_tabs_in_content);
        if (!read_only) {
            connect(
                m_rich_editor.data(),
                &GrowingTextEdit::textChanged,
                this,
                &QuTextEdit::widgetTextChanged
            );
            // QTextEdit::textChanged - Called *whenever* contents changed.
            // https://doc.qt.io/qt-6.5/qtextedit.html#textChanged
            // Note: no data sent along with the signal

            m_focus_watcher = new FocusWatcher(m_rich_editor.data());
            connect(
                m_focus_watcher.data(),
                &FocusWatcher::focusChanged,
                this,
                &QuTextEdit::widgetFocusChanged
            );
        }
        setFromField();
        return QPointer<QWidget>(m_rich_editor);

#ifdef QUTEXTEDIT_USE_PLAIN_TEXT_EDITOR
    } else {
        m_plain_editor = new GrowingPlainTextEdit();
        m_plain_editor->setEnabled(!read_only);
        m_plain_editor->setPlaceholderText(m_hint);
        m_plain_editor->setTabChangesFocus(!m_allow_tabs_in_content);
        if (!read_only) {
            connect(
                m_plain_editor.data(),
                &GrowingPlainTextEdit::textChanged,
                this,
                &QuTextEdit::widgetTextChanged
            );
            // QPlainTextEdit::textChanged - Called *whenever* contents
            // changed.
            // https://doc.qt.io/qt-6.5/qplaintextedit.html#textChanged

            m_focus_watcher = new FocusWatcher(m_plain_editor.data());
            connect(
                m_focus_watcher.data(),
                &FocusWatcher::focusChanged,
                this,
                &QuTextEdit::widgetFocusChanged
            );
        }
        setFromField();
        return QPointer<QWidget>(m_plain_editor);
    }
#endif
}

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

void QuTextEdit::widgetTextChanged()
{
    if (m_ignore_widget_signal) {
        // Note: ignore it now, not after the timer! Otherwise impossible
        // (well, harder) to synchronize flag distinguishing "real" and
        // "internally generated" changes with events at the far end of the
        // timer.
        return;
    }
    m_timer->start(WRITE_DELAY_MS);  // will restart if already timing
    // ... goes to textChanged()
}

void QuTextEdit::textChanged()
{
#ifdef QUTEXTEDIT_USE_PLAIN_TEXT_EDITOR
    if (!m_plain_editor && !m_rich_editor) {
        return;
    }
#else
    if (!m_rich_editor) {
        return;
    }
#endif
    QString text;
    if (m_accept_rich_text) {
        text = m_rich_editor->toPlainText();
        if (!text.isEmpty()) {
            text = m_rich_editor->toHtml();
        }
        // ... That forces the text to empty (rather than a bunch of HTML
        // representing nothing) if there is no real text.
    } else {
#ifdef QUTEXTEDIT_USE_PLAIN_TEXT_EDITOR
        text = m_plain_editor->toPlainText();
#else
        text = m_rich_editor->toPlainText();
#endif
    }
    const bool changed = m_fieldref->setValue(text, this);
    // ... Will trigger valueChanged.
    if (changed) {
        emit elementValueChanged();
    }
}

void QuTextEdit::fieldValueChanged(
    const FieldRef* fieldref, const QObject* originator
)
{
#ifdef QUTEXTEDIT_USE_PLAIN_TEXT_EDITOR
    QWidget* pwidget = m_accept_rich_text
        ? static_cast<QWidget*>(m_rich_editor.data())
        : static_cast<QWidget*>(m_plain_editor.data());
#else
    auto pwidget = static_cast<QWidget*>(m_rich_editor.data());
#endif
    if (!pwidget) {
        return;
    }
    widgetfunc::setPropertyMissing(pwidget, fieldref->missingInput());
    if (originator != this) {
        // In this case we don't want to block all signals, because the
        // GrowingPlainTextEdit/GrowingTextEdit widget needs internal signals.
        // However, we want to stop signal receipt by our own textChanged()
        // slot. So we can set a flag:
        m_ignore_widget_signal = true;
        if (m_accept_rich_text) {
            m_rich_editor->setHtml(fieldref->valueString());
        } else {
#ifdef QUTEXTEDIT_USE_PLAIN_TEXT_EDITOR
            m_plain_editor->setPlainText(fieldref->valueString());
#else
            m_rich_editor->setPlainText(fieldref->valueString());
#endif
        }
        m_ignore_widget_signal = false;
    }
}

void QuTextEdit::widgetFocusChanged(const bool in)
{
    // If focus is leaving the widget, save the field value.
#ifdef QUTEXTEDIT_USE_PLAIN_TEXT_EDITOR
    if (in || (!m_plain_editor && !m_rich_editor)) {
        return;
    }
#else
    if (in || !m_rich_editor) {
        return;
    }
#endif
    const bool change_pending = m_timer->isActive();
    m_timer->stop();  // just in case it's running
    if (change_pending) {
        textChanged();  // maybe
    }
}