Source: shellfish-ui/ui/NotificationArea.shui

shellfish-ui/ui/NotificationArea.shui

/*******************************************************************************
This file is part of the Shellfish UI toolkit.
Copyright (c) 2024 Martin Grimme <martin.grimme@gmail.com>

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*******************************************************************************/

require "shellfish/ui";

/**
 * Element for displaying popup notifications.
 *
 * This box is transparent to pointer events and may be placed above your
 * application contents. The notification popups are as wide as this
 * notification area.
 *
 * Example
 * ```
 * require "shellfish/ui";
 * require "/icons/ui-icons.css";
 *
 * Document {
 *
 *     onInitialization: () =>
 *     {
 *         notificationArea.createNotification("Welcome", "ui-info", "Welcome to Shellfish!");
 *     }
 *
 *     NotificationArea {
 *         id: notificationArea
 *
 *         position: "free"
 *         origin: "top-right"
 *         width: theme.itemWidthLarge * 2
 *         height: thisDocument.windowHeight
 *     }
 *
 * }
 * ```
 *
 * Notifications have a number of properties you can set to control their appearance.
 * See {@link ui.NotificationArea.Notification Notification} for details.
 *
 * @memberof ui
 * @name NotificationArea
 * @class
 * @extends html.Box
 *
 * @property {number} count - [readonly] The current amount of notifications.
 * @property {string} verticalAlignment - (default: `"top"`) The vertical alignment of the notifications. One of `top|bottom`
 */
Box {
    id: notificationArea

    property count: 0
    property verticalAlignment: "top"

    /**
     * Creates and returns a new custom notification. If `sourceTemplate` is specified,
     * the template is loaded into the notification window.
     *
     * @method
     * @name createCustomNotification
     * @memberof ui.NotificationArea.prototype
     *
     * @param {function} [sourceTemplate] - A Shui template.
     * @return {html.Box} The notification.
     */
    function createCustomNotification(sourceTemplate)
    {
        const item = notificationT();
        if (sourceTemplate)
        {
            item.sourceTemplate = sourceTemplate;
        }

        if (children().length > 0)
        {
            get().add(item.get(), children()[children().length - 1].get());
        }
        else
        {
            add(item);
        }
        item.open();
        return item;
    }

    /**
     * Creates and returns a standard notification with title, icon,
     * and message text. The notification disappears after a set time.
     *
     * @method
     * @name createNotification
     * @memberof ui.NotificationArea.prototype
     *
     * @param {string} title - The title text.
     * @param {string} icon - The icon to show. Pass `""` for no icon.
     * @param {string} message - The message text.
     * @param {number} [timeout] - The timeout value in ms after which the notification disappears. Defaults to 3000 ms.
     * @return {html.Box} The notification.
     */
    function createNotification(title, icon, message, timeout)
    {
        if (timeout === undefined)
        {
            timeout = 3000;
        }

        const item = createCustomNotification();
        item.title = title;
        item.icon = icon;
        item.message = message;
        wait(timeout).then(() =>
        {
            item.close();
        });

        return item;
    }

    /**
     * Is triggered when the notification area wants to draw attention, e.g.
     * when a new notification appears.
     * By default, nothing special happens on this event.
     *
     * @memberof ui.NotificationArea.prototype
     * @name drawAttention
     * @event
     */
    event drawAttention

    overflowBehavior: "scroll"

    onInitialization: () =>
    {
        css("pointer-events", "none");
    }

    onCountChanged: () =>
    {
        if (count > 0)
        {
            drawAttention();
        }
    }

    Box {
        fillHeight: notificationArea.verticalAlignment === "bottom"
    }

    OverflowScroller { }


    /**
     * A notification popup.
     *
     * @memberof ui.NotificationArea
     * @name Notification
     * @class
     * @extends html.Box
     *
     * @property {string} icon - (default: `""`) The name of the icon.
     * @property {ui.Object} item - (default: `null`) The item created from a custom template.
     * @property {string} message - (default: `""`) The message text.
     * @property {number} progress - (default: `-1.0`) A progress value between `0.0` and `1.0`, or a negative value for showing no progress bar.
     * @property {string} progressText - (default: `""`) The text to display next to the progress bar.
     * @property {string} title - (default: `""`) The title text.
     */
    property notificationT: template Box {
        id: notification

        property title: ""
        property icon: ""
        property message: ""
        property progress: -1.0
        property progressText: ""
        property sourceTemplate: null

        property item: customLoader.item

        property closed: false

        function open()
        {
            wait(100).then(() => { openAnimation.start(); });
        }

        function close()
        {
            if (! closed)
            {
                closed = true;
                closeAnimation.start().then(() => { parent = null; });
            }
        }

        fillWidth: true
        height: 0

        SequentialAction {
            id: openAnimation

            ScriptAction {
                script: () =>
                {
                    ++notificationArea.count;
                }
            }

            WaitAction {
                enabled: notification.sourceTemplate !== null && ! customLoader.item
            }

            NumberAnimation {
                from: 0
                to: contentBox.bboxHeight + 2 * theme.paddingSmall
                duration: 500

                onNext: value => { notification.height = value; }
            }

            NumberAnimation {
                from: notification.bboxWidth
                to: 0
                duration: 500

                onNext: value => { contentBox.marginLeft = value; }
            }
        }

        SequentialAction {
            id: closeAnimation

            ScriptAction {
                script: () =>
                {
                    --notificationArea.count;
                }
            }

            NumberAnimation {
                from: 0
                to: notification.bboxWidth
                duration: 500

                onNext: value => { contentBox.marginLeft = value; }
            }

            NumberAnimation {
                from: bboxHeight
                to: 0
                duration: 500

                onNext: value => { notification.height = value; }
            }
        }

        MouseBox {
            id: contentBox

            width: notification.bboxWidth - theme.paddingSmall

            marginLeft: notification.bboxWidth
            marginRight: theme.paddingSmall
            marginTop: theme.paddingSmall
            marginBottom: theme.paddingSmall

            color: theme.primaryBackgroundColor
            borderColor: theme.borderColor
            borderWidth: 1
            borderRadius: theme.borderRadius

            style: "sh-dropshadow"

            onInitialization: () =>
            {
                css("pointer-events", "auto");
            }

            onPointerMove: () =>
            {
                notificationArea.drawAttention();
            }

            onContainsMouseChanged: () =>
            {
                if (containsMouse)
                {
                    notificationArea.drawAttention();
                }
            }

            // title bar
            Box {
                fillWidth: true
                height: theme.itemHeightSmall
                color: theme.primaryColor
                layout: "center-row"

                Label {
                    marginTop: 2
                    marginBottom: 2
                    marginLeft: theme.paddingSmall
                    marginRight: theme.paddingSmall
                    fillWidth: true
                    overflowBehavior: "ellipsis"
                    bold: true
                    color: theme.primaryBackgroundColor
                    text: notification.title
                }

                MouseBox {
                    width: bboxHeight
                    fillHeight: true
                    color: containsMouse ? theme.highlightBackgroundColor : "transparent"
                    layout: "center"

                    onClick: ev =>
                    {
                        ev.accepted = true;
                        notification.close();
                    }

                    Label {
                        color: parent.containsMouse ? theme.highlightColor : theme.primaryBackgroundColor
                        text: "[icon:core-window-close]"
                    }
                }
            }

            Box {
                fillWidth: true
                layout: "center-row"
                
                Label {
                    visible: text !== ""
                    marginLeft: theme.paddingSmall
                    fontSize: theme.fontSizeLarge
                    text: notification.icon ? "[icon:" + notification.icon + "]" : ""
                }

                Label {
                    visible: text !== ""
                    margins: theme.paddingSmall
                    fillWidth: true
                    overflowBehavior: "wrap"
                    text: notification.message
                }
            }

            Loader {
                id: customLoader

                property notification: parent.parent

                visible: !! item
                fillWidth: true
                sourceTemplate: notification.sourceTemplate
            }

            Box {
                visible: notification.progress >= 0.0

                marginTop: theme.paddingSmall
                marginLeft: theme.paddingSmall
                marginRight: theme.paddingSmall
                fillWidth: true
                layout: "row"

                Label {
                    fillWidth: true
                    fontSize: theme.fontSizeSmall
                    text: notification.progressText
                }

                Label {
                    fontSize: theme.fontSizeSmall
                    text: Math.round(notification.progress * 100) + "%"
                }
            }

            Box {
                visible: notification.progress >= 0.0
                marginLeft: theme.paddingSmall
                marginRight: theme.paddingSmall
                marginBottom: theme.paddingSmall
                fillWidth: true
                height: theme.paddingSmall
                color: theme.secondaryBackgroundColor
                borderRadius: theme.borderRadius

                Box {
                    width: Math.max(0, Math.min(parent.bboxWidth, parent.bboxWidth * notification.progress))
                    fillHeight: true
                    color: theme.primaryColor
                }

            }
        }

    }

}