// Copyright (C) 2020 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
// Qt-Security score:significant reason:default

#include "qquickactiongroup_p.h"

#include <QtCore/private/qobject_p.h>
#include <QtCore/qmetaobject.h>
#include <QtCore/qvariant.h>
#include <QtQml/qqmlinfo.h>

#include "qquickaction_p.h"
#include "qquickaction_p_p.h"

#include <QtCore/qpointer.h>

QT_BEGIN_NAMESPACE

/*!
    \qmltype ActionGroup
    \inherits QtObject
//!     \nativetype QQuickActionGroup
    \inqmlmodule QtQuick.Controls
    \since 5.10
    \ingroup utilities
    \brief Groups actions together.

    ActionGroup is a non-visual group of actions. A mutually \l exclusive
    action group is used with actions where only one of the options can be
    selected at a time.

    The most straight-forward way to use ActionGroup is to declare actions
    as children of the group.

    \code
    ActionGroup {
        id: alignmentGroup

        Action {
            checked: true
            checkable: true
            text: qsTr("Left")
        }

        Action {
            checkable: true
            text: qsTr("Center")
        }

        Action {
            checkable: true
            text: qsTr("Right")
        }
    }
    \endcode

    Alternatively, the \l group attached property allows declaring the actions
    elsewhere and assigning them to a specific group.

    \code
    ActionGroup { id: alignmentGroup }

    Action {
        checked: true
        checkable: true
        text: qsTr("Left")
        ActionGroup.group: alignmentGroup
    }

    Action {
        checkable: true
        text: qsTr("Center")
        ActionGroup.group: alignmentGroup
    }

    Action {
        checkable: true
        text: qsTr("Right")
        ActionGroup.group: alignmentGroup
    }
    \endcode

    More advanced use cases can be handled using the \c addAction() and
    \c removeAction() methods.

    \sa Action, ButtonGroup
*/

/*!
    \qmlsignal QtQuick.Controls::ActionGroup::triggered(Action action)

    This signal is emitted when an \a action in the group has been triggered.

    This signal is convenient for implementing a common signal handler for
    all actions in the same group.

    \code
    ActionGroup {
        onTriggered: console.log("triggered:", action.text)

        Action { text: "First" }
        Action { text: "Second" }
        Action { text: "Third" }
    }
    \endcode

    \sa Action::triggered()
*/

class QQuickActionGroupPrivate : public QObjectPrivate
{
public:
    Q_DECLARE_PUBLIC(QQuickActionGroup)

    void clear();
    void actionTriggered();
    void _q_updateCurrent();

    static bool changeEnabled(QQuickAction *action, bool enabled);

    static void actions_append(QQmlListProperty<QQuickAction> *prop, QQuickAction *obj);
    static qsizetype actions_count(QQmlListProperty<QQuickAction> *prop);
    static QQuickAction *actions_at(QQmlListProperty<QQuickAction> *prop, qsizetype index);
    static void actions_clear(QQmlListProperty<QQuickAction> *prop);

    bool enabled = true;
    bool exclusive = true;
    QPointer<QQuickAction> checkedAction;
    QList<QQuickAction*> actions;
};

void QQuickActionGroupPrivate::clear()
{
    for (QQuickAction *action : std::as_const(actions)) {
        QQuickActionPrivate::get(action)->group = nullptr;
        QObjectPrivate::disconnect(action, &QQuickAction::triggered, this, &QQuickActionGroupPrivate::actionTriggered);
        QObjectPrivate::disconnect(action, &QQuickAction::checkedChanged, this, &QQuickActionGroupPrivate::_q_updateCurrent);
    }
    actions.clear();
}

void QQuickActionGroupPrivate::actionTriggered()
{
    Q_Q(QQuickActionGroup);
    QQuickAction *action = qobject_cast<QQuickAction*>(q->sender());
    if (action)
        emit q->triggered(action);
}

void QQuickActionGroupPrivate::_q_updateCurrent()
{
    Q_Q(QQuickActionGroup);
    if (!exclusive)
        return;
    QQuickAction *action = qobject_cast<QQuickAction*>(q->sender());
    if (action && action->isChecked())
        q->setCheckedAction(action);
    else if (!actions.contains(checkedAction))
        q->setCheckedAction(nullptr);
}

bool QQuickActionGroupPrivate::changeEnabled(QQuickAction *action, bool enabled)
{
    return action->isEnabled() != enabled && (!enabled || !QQuickActionPrivate::get(action)->explicitEnabled);
}

void QQuickActionGroupPrivate::actions_append(QQmlListProperty<QQuickAction> *prop, QQuickAction *obj)
{
    QQuickActionGroup *q = static_cast<QQuickActionGroup *>(prop->object);
    q->addAction(obj);
}

qsizetype QQuickActionGroupPrivate::actions_count(QQmlListProperty<QQuickAction> *prop)
{
    QQuickActionGroupPrivate *p = static_cast<QQuickActionGroupPrivate *>(prop->data);
    return p->actions.size();
}

QQuickAction *QQuickActionGroupPrivate::actions_at(QQmlListProperty<QQuickAction> *prop, qsizetype index)
{
    QQuickActionGroupPrivate *p = static_cast<QQuickActionGroupPrivate *>(prop->data);
    return p->actions.value(index);
}

void QQuickActionGroupPrivate::actions_clear(QQmlListProperty<QQuickAction> *prop)
{
    QQuickActionGroupPrivate *p = static_cast<QQuickActionGroupPrivate *>(prop->data);
    if (!p->actions.isEmpty()) {
        p->clear();
        QQuickActionGroup *q = static_cast<QQuickActionGroup *>(prop->object);
        // QTBUG-52358: don't clear the checked action immediately
        QMetaObject::invokeMethod(q, "_q_updateCurrent", Qt::QueuedConnection);
        emit q->actionsChanged();
    }
}

QQuickActionGroup::QQuickActionGroup(QObject *parent)
    : QObject(*(new QQuickActionGroupPrivate), parent)
{
}

QQuickActionGroup::~QQuickActionGroup()
{
    Q_D(QQuickActionGroup);
    d->clear();
}

QQuickActionGroupAttached *QQuickActionGroup::qmlAttachedProperties(QObject *object)
{
    return new QQuickActionGroupAttached(object);
}

/*!
    \qmlproperty Action QtQuick.Controls::ActionGroup::checkedAction

    This property holds the currently selected action in an exclusive group,
    or \c null if there is none or the group is non-exclusive.

    By default, it is the first checked action added to an exclusive action group.

    \sa exclusive
*/
QQuickAction *QQuickActionGroup::checkedAction() const
{
    Q_D(const QQuickActionGroup);
    return d->checkedAction;
}

void QQuickActionGroup::setCheckedAction(QQuickAction *checkedAction)
{
    Q_D(QQuickActionGroup);
    if (d->checkedAction == checkedAction)
        return;

    if (d->checkedAction)
        d->checkedAction->setChecked(false);
    d->checkedAction = checkedAction;
    if (checkedAction)
        checkedAction->setChecked(true);
    emit checkedActionChanged();
}

/*!
    \qmlproperty list<Action> QtQuick.Controls::ActionGroup::actions
    \qmldefault

    This property holds the list of actions in the group.

    \sa group
*/
QQmlListProperty<QQuickAction> QQuickActionGroup::actions()
{
    Q_D(QQuickActionGroup);
    return QQmlListProperty<QQuickAction>(this, d,
        QQuickActionGroupPrivate::actions_append,
        QQuickActionGroupPrivate::actions_count,
        QQuickActionGroupPrivate::actions_at,
        QQuickActionGroupPrivate::actions_clear);
}

/*!
    \qmlproperty bool QtQuick.Controls::ActionGroup::exclusive

    This property holds whether the action group is exclusive. The default value is \c true.

    If this property is \c true, then only one action in the group can be checked at any given time.
    The user can trigger any action to check it, and that action will replace the existing one as
    the checked action in the group.

    In an exclusive group, the user cannot uncheck the currently checked action by triggering it;
    instead, another action in the group must be triggered to set the new checked action for that
    group.

    In a non-exclusive group, checking and unchecking actions does not affect the other actions in
    the group. Furthermore, the value of the \l checkedAction property is \c null.
*/
bool QQuickActionGroup::isExclusive() const
{
    Q_D(const QQuickActionGroup);
    return d->exclusive;
}

void QQuickActionGroup::setExclusive(bool exclusive)
{
    Q_D(QQuickActionGroup);
    if (d->exclusive == exclusive)
        return;

    d->exclusive = exclusive;
    emit exclusiveChanged();
}

/*!
    \qmlproperty bool QtQuick.Controls::ActionGroup::enabled

    This property holds whether the action group is enabled. The default value is \c true.

    If this property is \c false, then all actions in the group are disabled. If this property
    is \c true, all actions in the group are enabled, unless explicitly disabled.
*/
bool QQuickActionGroup::isEnabled() const
{
    Q_D(const QQuickActionGroup);
    return d->enabled;
}

void QQuickActionGroup::setEnabled(bool enabled)
{
    Q_D(QQuickActionGroup);
    if (d->enabled == enabled)
        return;

    for (QQuickAction *action : std::as_const(d->actions)) {
        if (d->changeEnabled(action, enabled))
            emit action->enabledChanged(enabled);
    }

    d->enabled = enabled;
    emit enabledChanged();
}

/*!
    \qmlmethod void QtQuick.Controls::ActionGroup::addAction(Action action)

    Adds an \a action to the action group.

    \note Manually adding objects to a action group is typically unnecessary.
          The \l actions property and the \l group attached property provide a
          convenient and declarative syntax.

    \sa actions, group
*/
void QQuickActionGroup::addAction(QQuickAction *action)
{
    Q_D(QQuickActionGroup);
    if (!action || d->actions.contains(action))
        return;

    const bool enabledChange = d->changeEnabled(action, d->enabled);

    QQuickActionPrivate::get(action)->group = this;
    QObjectPrivate::connect(action, &QQuickAction::triggered, d, &QQuickActionGroupPrivate::actionTriggered);
    QObjectPrivate::connect(action, &QQuickAction::checkedChanged, d, &QQuickActionGroupPrivate::_q_updateCurrent);

    if (d->exclusive && action->isChecked())
        setCheckedAction(action);
    if (enabledChange)
        emit action->enabledChanged(action->isEnabled());

    d->actions.append(action);
    emit actionsChanged();
}

/*!
    \qmlmethod void QtQuick.Controls::ActionGroup::removeAction(Action action)

    Removes an \a action from the action group.

    \note Manually removing objects from a action group is typically unnecessary.
          The \l actions property and the \l group attached property provide a
          convenient and declarative syntax.

    \sa actions, group
*/
void QQuickActionGroup::removeAction(QQuickAction *action)
{
    Q_D(QQuickActionGroup);
    if (!action || !d->actions.contains(action))
        return;

    const bool enabledChange = d->changeEnabled(action, d->enabled);

    QQuickActionPrivate::get(action)->group = nullptr;
    QObjectPrivate::disconnect(action, &QQuickAction::triggered, d, &QQuickActionGroupPrivate::actionTriggered);
    QObjectPrivate::disconnect(action, &QQuickAction::checkedChanged, d, &QQuickActionGroupPrivate::_q_updateCurrent);

    if (d->checkedAction == action)
        setCheckedAction(nullptr);
    if (enabledChange)
        emit action->enabledChanged(action->isEnabled());

    d->actions.removeOne(action);
    emit actionsChanged();
}

class QQuickActionGroupAttachedPrivate : public QObjectPrivate
{
public:
    QQuickActionGroup *group = nullptr;
};

QQuickActionGroupAttached::QQuickActionGroupAttached(QObject *parent)
    : QObject(*(new QQuickActionGroupAttachedPrivate), parent)
{
}

/*!
    \qmlattachedproperty ActionGroup QtQuick.Controls::ActionGroup::group

    This property attaches an action to an action group.

    \code
    ActionGroup { id: group }

    Action {
        checked: true
        text: qsTr("Option A")
        ActionGroup.group: group
    }

    Action {
        text: qsTr("Option B")
        ActionGroup.group: group
    }
    \endcode

    \sa actions
*/
QQuickActionGroup *QQuickActionGroupAttached::group() const
{
    Q_D(const QQuickActionGroupAttached);
    return d->group;
}

void QQuickActionGroupAttached::setGroup(QQuickActionGroup *group)
{
    Q_D(QQuickActionGroupAttached);
    if (d->group == group)
        return;

    if (d->group)
        d->group->removeAction(qobject_cast<QQuickAction*>(parent()));
    d->group = group;
    if (group)
        group->addAction(qobject_cast<QQuickAction*>(parent()));
    emit groupChanged();
}

QT_END_NAMESPACE

#include "moc_qquickactiongroup_p.cpp"
