Source code for windows_toasts.toasters

import warnings
from datetime import datetime
from typing import Optional, TypeVar

from winrt.windows.ui.notifications import (
    NotificationData,
    NotificationUpdateResult,
    ScheduledToastNotification,
    ToastNotification,
    ToastNotificationHistory,
    ToastNotificationManager,
    ToastNotifier,
)

from .events import ToastActivatedEventArgs
from .exceptions import ToastNotFoundError
from .toast import Toast
from .toast_document import ToastDocument
from .wrappers import ToastDuration, ToastImagePosition, ToastScenario

ToastNotificationT = TypeVar("ToastNotificationT", ToastNotification, ScheduledToastNotification)


def _build_adaptable_data(toast: Toast) -> NotificationData:
    """
    Build the adaptable content from a toast

    :param toast: Toast that has adaptable content
    :type toast: Toast
    :return: A NotificationData object ready to be used in ToastNotifier.Update
    :rtype: NotificationData
    """
    notificationData = NotificationData()

    toast.updates += 1
    notificationData.sequence_number = toast.updates

    for i, fieldContent in enumerate(toast.text_fields):
        if fieldContent is not None:
            notificationData.values[f"text{i + 1}"] = fieldContent

    progressBar = toast.progress_bar
    if progressBar is not None:
        notificationData.values["status"] = progressBar.status
        notificationData.values.insert(
            "progress", "indeterminate" if progressBar.progress is None else str(progressBar.progress)
        )
        progressOverride = progressBar.progress_override
        if progressOverride is None and progressBar.progress is not None:
            # Recreate default Windows behaviour while still allowing it to be changed in the future
            progressOverride = f"{round(progressBar.progress * 100)}%"

        notificationData.values["progress_override"] = progressOverride
        notificationData.values["caption"] = progressBar.caption or ""

    return notificationData


def _build_toast_notification(toast: Toast, toastNotification: ToastNotificationT) -> ToastNotificationT:
    """
    Builds a ToastNotification appropriately
    :param toast: The toast with the bindings
    :type toastNotification: ToastNotificationGeneric
    :rtype: ToastNotificationT
    """
    toastNotification.tag = toast.tag
    # Group, a non-empty string, is required for some functionality. If one isn't provided, use the tag
    toastNotification.group = toast.group or toast.tag

    if toast.expiration_time is not None:
        toastNotification.expiration_time = toast.expiration_time

    toastNotification.suppress_popup = toast.suppress_popup

    return toastNotification


[docs] class BaseWindowsToaster: """ Wrapper to simplify WinRT's ToastNotificationManager :param applicationText: Text to display the application as """ applicationText: str notifierAUMID: Optional[str] toastNotifier: ToastNotifier def __init__(self, applicationText: str): self.applicationText = applicationText @property def _AUMID(self) -> str: return self.notifierAUMID or self.applicationText def _setup_toast(self, toast: Toast, dynamic: bool) -> ToastDocument: """ Setup toast to send. Should generally be used internally :return: XML built from the toast """ # Should this be done in ToastDocument? toastContent = ToastDocument(toast) for image in toast.images: toastContent.AddImage(image) if toast.duration != ToastDuration.Default: toastContent.SetDuration(toast.duration) if toast.timestamp is not None: toastContent.SetCustomTimestamp(toast.timestamp) if toast.audio is not None: toastContent.SetAudioAttributes(toast.audio) if toast.attribution_text is not None: toastContent.SetAttributionText(toast.attribution_text) if toast.scenario != ToastScenario.Default: toastContent.SetScenario(toast.scenario) if toast.launch_action is not None: toastContent.SetAttribute(toastContent.GetElementByTagName("toast"), "launch", toast.launch_action) toastContent.SetAttribute(toastContent.GetElementByTagName("toast"), "activationType", "protocol") else: toastContent.SetAttribute(toastContent.GetElementByTagName("toast"), "launch", toast.tag) return toastContent
[docs] def show_toast(self, toast: Toast) -> None: """ Displays the specified toast notification. If `toast` has already been shown, it will pop up again, but make no new sections in the action center :param toast: Toast to display """ toastNotification = ToastNotification(self._setup_toast(toast, True).xmlDocument) toastNotification.data = _build_adaptable_data(toast) if toast.on_activated is not None: # pragma: no cover # For some reason on_activated's type is generic, so cast it toastNotification.add_activated( lambda _, eventArgs: toast.on_activated(ToastActivatedEventArgs.fromWinRt(eventArgs)) ) if toast.on_dismissed is not None: # pragma: no cover toastNotification.add_dismissed(lambda _, eventArgs: toast.on_dismissed(eventArgs)) if toast.on_failed is not None: # pragma: no cover toastNotification.add_failed(lambda _, eventArgs: toast.on_failed(eventArgs)) notificationToSend = _build_toast_notification(toast, toastNotification) self.toastNotifier.show(notificationToSend)
[docs] def update_toast(self, toast: Toast) -> bool: """ Update the passed notification data with the new data in the clas :param toast: Toast to update :type toast: Toast :return: Whether the update succeeded """ newData = _build_adaptable_data(toast) updateResult = self.toastNotifier.update_with_tag_and_group(newData, toast.tag, toast.group or toast.tag) return updateResult == NotificationUpdateResult.SUCCEEDED
[docs] def schedule_toast(self, toast: Toast, displayTime: datetime) -> None: """ Schedule the passed notification toast. Warning: scheduled toasts cannot be updated or activated (i.e. on_X) :param toast: Toast to display :type toast: Toast :param displayTime: Time to display the toast on :type displayTime: datetime """ toastNotification = ScheduledToastNotification(self._setup_toast(toast, False).xmlDocument, displayTime) scheduledNotificationToSend = _build_toast_notification(toast, toastNotification) self.toastNotifier.add_to_schedule(scheduledNotificationToSend)
[docs] def unschedule_toast(self, toast: Toast) -> None: """ Unschedule the passed notification toast :raises: ToastNotFoundError: If the toast could not be found """ scheduledToasts = self.toastNotifier.get_scheduled_toast_notifications() targetNotification = next( (scheduledToast for scheduledToast in scheduledToasts if scheduledToast.tag == toast.tag), None ) if targetNotification is None: raise ToastNotFoundError(f"Toast unscheduling failed. Toast {toast} not found") self.toastNotifier.remove_from_schedule(targetNotification)
[docs] def clear_toasts(self) -> None: """ Clear toasts popped by this toaster """ toastHistory: ToastNotificationHistory = ToastNotificationManager.history toastHistory.clear_with_id(self._AUMID)
[docs] def clear_scheduled_toasts(self) -> None: """ Clear all scheduled toasts set for the toaster """ scheduledToasts = self.toastNotifier.get_scheduled_toast_notifications() for toast in scheduledToasts: self.toastNotifier.remove_from_schedule(toast)
[docs] def remove_toast(self, toast: Toast) -> None: """ Removes an individual popped toast """ # Is fetching toastHistory expensive? Should this be stored in an instance variable? toastHistory: ToastNotificationHistory = ToastNotificationManager.history toastHistory.remove_grouped_tag_with_id(toast.tag, toast.group or toast.tag, self._AUMID)
[docs] def remove_toast_group(self, toastGroup: str) -> None: """ Removes a group of toast notifications, identified by the specified group ID """ toastHistory: ToastNotificationHistory = ToastNotificationManager.history toastHistory.remove_group_with_id(toastGroup, self._AUMID)
[docs] class WindowsToaster(BaseWindowsToaster): """ Basic toaster, used to display toasts without actions and/or inputs. If you need to use them, see :class:`InteractableWindowsToaster` :param applicationText: Text to display the application as """ __InteractableWarningMessage = ( "{0} are not supported in WindowsToaster. If you'd like to use {0}, " "instantiate a InteractableWindowsToaster class instead" ) def __init__(self, applicationText: str): super().__init__(applicationText) self.notifierAUMID = None # .create_toast_notifier() fails with "Element not found" self.toastNotifier = ToastNotificationManager.create_toast_notifier_with_id(applicationText)
[docs] def show_toast(self, toast: Toast) -> None: # pragma: no cover if len(toast.inputs) > 0: warnings.warn(self.__InteractableWarningMessage.format("input fields")) if len(toast.actions) > 0: warnings.warn(self.__InteractableWarningMessage.format("actions")) if len(toast.images) > 2: warnings.warn(self.__InteractableWarningMessage.format("more than two images")) if toast.progress_bar is not None: warnings.warn(self.__InteractableWarningMessage.format("progress bars")) if any(toast_image.position == ToastImagePosition.Hero for toast_image in toast.images): warnings.warn(self.__InteractableWarningMessage.format("hero placements")) super().show_toast(toast)
def _setup_toast(self, toast, dynamic) -> ToastDocument: toastContent = super()._setup_toast(toast, dynamic) for i, fieldContent in enumerate(toast.text_fields): if fieldContent is None: fieldContent = "" if dynamic: toastContent.SetTextField(i) else: toastContent.SetTextFieldStatic(i, fieldContent) toastContent.SetAttribute(toastContent.bindingNode, "template", "ToastImageAndText04") return toastContent
[docs] class InteractableWindowsToaster(BaseWindowsToaster): """ :class:`WindowsToaster`, but uses a AUMID to support actions. Actions require a recognised AUMID to trigger on_activated, otherwise it triggers on_dismissed with no arguments :param applicationText: Text to display the application as :param notifierAUMID: AUMID to use. Defaults to Command Prompt. To use a custom AUMID, see one of the scripts """ def __init__(self, applicationText: str, notifierAUMID: Optional[str] = None): super().__init__(applicationText) if notifierAUMID is None: self.defaultAUMID = True self.notifierAUMID = "{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\\cmd.exe" else: self.defaultAUMID = False self.notifierAUMID = notifierAUMID self.toastNotifier = ToastNotificationManager.create_toast_notifier_with_id(self.notifierAUMID) def _setup_toast(self, toast, dynamic): toastContent = super()._setup_toast(toast, dynamic) for i, fieldContent in enumerate(toast.text_fields): if fieldContent is None: continue if dynamic: toastContent.SetTextField(i) else: toastContent.SetTextFieldStatic(i, fieldContent) toastContent.SetAttribute(toastContent.bindingNode, "template", "ToastGeneric") toastNode = toastContent.GetElementByTagName("toast") toastContent.SetAttribute(toastNode, "useButtonStyle", "true") # If we haven't set up our own AUMID, put our application text in the attribution field if self.defaultAUMID and toast.attribution_text is None: toastContent.SetAttributionText(self.applicationText) for toastInput in toast.inputs: toastContent.AddInput(toastInput) for customAction in toast.actions: toastContent.AddAction(customAction) if toast.progress_bar is not None: if dynamic: toastContent.AddProgressBar() else: toastContent.AddStaticProgressBar(toast.progress_bar) return toastContent