47e0ad87创建于 2025年4月1日历史提交
/*
    SPDX-FileCopyrightText: 2024 Kristen McWilliam <kmcwilliampublic@gmail.com>
    SPDX-FileCopyrightText: 2024 Jakob Petsovits <jpetso@petsovits.com>

    SPDX-License-Identifier: LGPL-2.1-or-later

    Originating from kcm_screenlocker. Upstream any changes there until it goes into Kirigami (Addons).
*/

import QtQuick
import QtQuick.Layouts
import QtQuick.Controls as QQC2
import org.kde.kirigami as Kirigami

/**
 * A dialog that prompts the user to enter a duration.
 *
 * The dialog contains a spin box for the user to enter a duration, and its corresponding time unit.
 *
 * The dialog emits the `accepted` signal when the user clicks the confirm button, and the `rejected` signal when the user clicks the cancel button.
 *
 * After the dialog has been accepted, the `value` property will contain the duration value entered
 * by the user. The `unit` property will contain the time unit of `value`.
 */
Kirigami.Dialog {
    id: root

    /**
     * @brief The possible time units of the value of the input field.
     */
    enum Unit {
        Milliseconds,
        Seconds,
        Minutes,
        Hours,
        Days,
        Weeks,
        Months,
        Years
    }

    /**
     * @brief An array of time units that can be associated with the duration value.
     *
     * All elements must be `DurationPromptDialog.Unit` enum values, with no duplicates.
     *
     * At least one element is required. If more than one accepted unit is specified, the dialog
     * allows the user to select the unit of the duration.
     */
    required property var acceptsUnits

    /**
     * @brief A text label in front of the input field.
     */
    property string label

    /**
     * @brief The value of the input field.
     *
     * The value is in seconds if `unit` is `DurationPromptDialog.ValueType.Seconds`,
     * in minutes if `valueType` is `DurationPromptDialog.ValueType.Minutes`, and so on.
     *
     * Assign before opening the dialog to set the initial value.
     *
     * The value will have been updated when the user accepts the dialog.
     */
    property alias value: durationValueSpinBox.value

    property alias from: durationValueSpinBox.from
    property alias to: durationValueSpinBox.to

    /**
     * @brief The unit of the value of the input field, of type `DurationPromptDialog.Unit`.
     *
     * This must be one of the elements from the specified `acceptsUnits` property.
     * The first element of that array is used as default unit.
     */
    property int unit: acceptsUnits[0]

    padding: Kirigami.Units.largeSpacing
    standardButtons: Kirigami.Dialog.Ok | Kirigami.Dialog.Cancel
    showCloseButton: false

    // FIXME: KF 6.3 and up doesn't need content.implicitWidth as part of preferredWidth, it will
    //        automatically expand to implicit content size. Remove this once we can rely on KF 6.3.
    preferredWidth: Math.max(content.implicitWidth,
                             Kirigami.Units.gridUnit * 10, implicitHeaderWidth, implicitFooterWidth)

    onOpened: {
        // `focus: true` is not enough, because the OK button wants focus and gets priority.
        durationValueSpinBox.forceActiveFocus()
        // Set SpinBox width once, don't resize it if `to` changes based on unit changes.
        durationValueSpinBox.Layout.preferredWidth = durationValueSpinBox.implicitWidth;
    }

    RowLayout {
        id: content
        anchors.centerIn: parent

        spacing: Kirigami.Units.largeSpacing

        QQC2.Label {
            id: labelItem
            visible: (root.label?.length ?? 0) > 0

            Kirigami.MnemonicData.enabled: visible && durationValueSpinBox.enabled
            Kirigami.MnemonicData.controlType: Kirigami.MnemonicData.FormLabel
            Kirigami.MnemonicData.label: root.label ?? ""
            text: Kirigami.MnemonicData.richTextLabel
        }

        RowLayout {
            spacing: Kirigami.Units.smallSpacing

            QQC2.SpinBox {
                id: durationValueSpinBox
                from: 0
                to: 9999

                Shortcut {
                    sequence: labelItem.Kirigami.MnemonicData.sequence
                    onActivated: { durationValueSpinBox.forceActiveFocus(); }
                }
                Keys.onReturnPressed: { root.accept(); }
            }

            function unitSuffixForValue(val, unit) {
                switch (unit) {
                case DurationPromptDialog.Unit.Milliseconds:
                    return i18ncp("The unit of the time input field", "millisecond", "milliseconds", val);
                case DurationPromptDialog.Unit.Seconds:
                    return i18ncp("The unit of the time input field", "second", "seconds", val);
                case DurationPromptDialog.Unit.Minutes:
                    return i18ncp("The unit of the time input field", "minute", "minutes", val);
                case DurationPromptDialog.Unit.Hours:
                    return i18ncp("The unit of the time input field", "hour", "hours", val);
                case DurationPromptDialog.Unit.Days:
                    return i18ncp("The unit of the time input field", "day", "days", val);
                case DurationPromptDialog.Unit.Weeks:
                    return i18ncp("The unit of the time input field", "week", "weeks", val);
                case DurationPromptDialog.Unit.Months:
                    return i18ncp("The unit of the time input field", "month", "months", val);
                case DurationPromptDialog.Unit.Years:
                    return i18ncp("The unit of the time input field", "year", "years", val);
                }
                console.warn("invalid unit in unitSuffixForValue()");
            }

            QQC2.Label {
                visible: acceptsUnits.length == 1
                text: parent.unitSuffixForValue(value, acceptsUnits[0])

                // Try not to shrink. The +1 is there because I couldn't figure out why actual
                // plural metrics were 49 but implicitWidth for the plural string 49.78125.
                // An extra pixel shouldn't hurt either way, and hopefully metrics are close enough.
                Layout.preferredWidth: Math.max(implicitWidth, pluralUnitLabelMetrics.width + leftPadding + rightPadding + 1)
                TextMetrics {
                    id: pluralUnitLabelMetrics
                    text: unitSelectionRadios.labelForUnit(acceptsUnits[0])
                }
            }

            ColumnLayout {
                id: unitSelectionRadios
                spacing: Kirigami.Units.smallSpacing
                visible: acceptsUnits.length > 1

                function labelForUnit(unit) {
                    switch (unit) {
                    case DurationPromptDialog.Unit.Milliseconds:
                        return i18nc("@text:radiobutton Unit of the time input field", "milliseconds");
                    case DurationPromptDialog.Unit.Seconds:
                        return i18nc("@text:radiobutton Unit of the time input field", "seconds");
                    case DurationPromptDialog.Unit.Minutes:
                        return i18nc("@text:radiobutton Unit of the time input field", "minutes");
                    case DurationPromptDialog.Unit.Hours:
                        return i18nc("@text:radiobutton Unit of the time input field", "hours");
                    case DurationPromptDialog.Unit.Days:
                        return i18nc("@text:radiobutton Unit of the time input field", "days");
                    case DurationPromptDialog.Unit.Weeks:
                        return i18nc("@text:radiobutton Unit of the time input field", "weeks");
                    case DurationPromptDialog.Unit.Months:
                        return i18nc("@text:radiobutton Unit of the time input field", "months");
                    case DurationPromptDialog.Unit.Years:
                        return i18nc("@text:radiobutton Unit of the time input field", "years");
                    }
                    console.warn("invalid unit in radioButtonLabelForValue()");
                }

                Repeater {
                    id: repeater
                    model: acceptsUnits

                    QQC2.RadioButton {
                        required property int index
                        required property int modelData

                        text: parent.labelForUnit(modelData)
                        checked: unit === modelData
                        onClicked: unit = modelData

                        Keys.onReturnPressed: { root.accept(); }
                        Keys.onUpPressed: { const prev = repeater.itemAt(index - 1); if (prev) prev.focus = true; }
                        Keys.onDownPressed: { const next = repeater.itemAt(index + 1); if (next) next.focus = true; }
                        Keys.onLeftPressed: { if (!LayoutMirroring.enabled) durationValueSpinBox.forceActiveFocus(); }
                        Keys.onRightPressed: { if (LayoutMirroring.enabled) durationValueSpinBox.forceActiveFocus(); }
                    }
                }
            }
        }
    }
}