diff --git a/.gitignore b/.gitignore index b6e4761..872c981 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,5 @@ dmypy.json # Pyre type checker .pyre/ + +.idea diff --git a/README.md b/README.md index 4914828..524f831 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,163 @@ # winsdk_toast -A simple module for displaying Windows Toast Notification based on winsdk + +A simple package for displaying Windows Toast Notification based on [winsdk]. + +Sometimes, after starting my data processing python script, I may surf the Internet. +It chokes my happiness that to frequently check whether the script stops, +or suddenly realize the script has stopped for a long while. + +It'll be reassuring that the script can stop with a friendly gesture. + + +## Usage + +```python +from os.path import abspath +from winsdk_toast import Notifier, Toast + +path_pic = abspath('./example/resource/python.ico') + +notifier = Notifier('程序名 applicationId') + +# %% minimal example +toast = Toast() +toast.add_text('第一行 1st line') +notifier.show(toast) +# %% which is equivalent to +xml = """ + + + + 第一行 1st line + + + +""" +toast = Toast(xml) +notifier.show(toast) + + +# %% simple example +toast = Toast() +toast.add_text('第一行 1st line', hint_align='center', hint_style='caption') +toast.add_text('第二行 2nd line') +toast.add_text('第三行 3rd line', placement='attribution') +toast.add_image(path_pic, placement='appLogoOverride') +toast.add_action('关闭 Close') +toast.set_audio(silent='true') +notifier.show(toast) +# %% which is equivalent to +xml = f""" + + + + 第一行 1st line + 第二行 2nd line + 第三行 3rd line + + + + + + +""" +toast = Toast(xml) +notifier.show(toast) + +# %% example for control freak +toast = Toast() +element_toast = toast.set_toast( + launch='blah', duration='long', displayTimeStamp='2022-04-01T12:00:00Z', scenario='default', + useButtonStyle='false', activationType='background' +) +element_visual = toast.set_visual( + version='1', lang='zh-CN', baseUri='ms-appx:///', branding='none', addImageQuery='false' +) +element_binding = toast.set_binding( + template='ToastGeneric', fallback='2ndtemplate', lang='zh-CN', addImageQuery='false', + baseUri='ms-appx:///', branding='none' +) +element_text = toast.add_text( + text='第一行 1st line for control freak', id_='1', lang='zh-CN', placement=None, + hint_maxLines='1', hint_style='title', hint_align='center', hint_wrap='false', + element_parent=element_binding +) +element_group = toast.add_group() +element_subgroup_left = toast.add_subgroup(element_parent=element_group) +element_text = toast.add_text( + text='第二行 2nd line for control freak', id_='2', lang='zh-CN', placement=None, + hint_maxLines='1', hint_style='captionSubtle ', hint_align='left', hint_wrap='false', + element_parent=element_subgroup_left +) +element_subgroup_right = toast.add_subgroup(element_parent=element_group) +element_text = toast.add_text( + text='第三行 3rd line for control freak', id_='3', lang='zh-CN', placement='attribution', + hint_maxLines='1', hint_style='captionSubtle', hint_align='left', hint_wrap='false', + element_parent=element_subgroup_right +) +toast.add_image( + path_pic, id_=None, alt='', addImageQuery='false', + placement='appLogoOverride', hint_crop='circle' +) +toast.set_actions() +toast.add_action( + '关闭 Close', arguments='dismiss', activationType='system', placement=None, + imageUri=None, hint_inputId=None, hint_buttonStyle=None, hint_toolTip='tip close' +) +notifier.show(toast) +# %% which is equivalent to +xml = f""" + + + + 第一行 1st line for control freak + + + 第二行 2nd line for control freak + + + 第三行 3rd line for control freak + + + + + + + + + +""" +toast = Toast(xml) +notifier.show(toast) +``` +The corresponding effects are like: + +![minimal_example.gif](doc/pic/minimal_example.gif) + +![simple_example.gif](doc/pic/simple_example.gif) + +![example_for_control_freak.gif](doc/pic/example_for_control_freak.gif) + + +## Todo + +- Events and callbacks. +- Documentation. +- Costume audio. According to [Microsoft Docs], this might be tricky. +- ... + +[Microsoft Docs]: https://docs.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/custom-audio-on-toasts + +## else + +When I almost make it work, I found another package [windows_toast] +which has the same dependency and more features. +Luckily our 'styles' are quite different, and I'm on vacation, +so I decide to finish it any way. + +If you need more features now, please use [windows_toast] instead, +maybe give this one a try later. + +[winsdk]: https://pypi.org/project/winsdk +[windows_toast]: https://github.com/DatGuy1/Windows-Toasts diff --git a/doc/pic/example_for_control_freak.gif b/doc/pic/example_for_control_freak.gif new file mode 100644 index 0000000..0a4de46 Binary files /dev/null and b/doc/pic/example_for_control_freak.gif differ diff --git a/doc/pic/minimal_example.gif b/doc/pic/minimal_example.gif new file mode 100644 index 0000000..2dedefc Binary files /dev/null and b/doc/pic/minimal_example.gif differ diff --git a/doc/pic/simple_example.gif b/doc/pic/simple_example.gif new file mode 100644 index 0000000..19453b7 Binary files /dev/null and b/doc/pic/simple_example.gif differ diff --git a/example/complex.py b/example/complex.py new file mode 100644 index 0000000..6327039 --- /dev/null +++ b/example/complex.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- +""" +example for control freak + +@Author: modabao +@Time: 2022/5/3 10:33 +""" + +from os.path import abspath + +from winsdk_toast import Notifier, Toast + +path_pic = abspath('./resource/python.ico') + +notifier = Notifier('程序名 applicationId') + + +# %% example for control freak +toast = Toast() +element_toast = toast.set_toast( + launch='blah', duration='long', displayTimeStamp='2022-04-01T12:00:00Z', scenario='default', + useButtonStyle='false', activationType='background' +) +element_visual = toast.set_visual( + version='1', lang='zh-CN', baseUri='ms-appx:///', branding='none', addImageQuery='false' +) +element_binding = toast.set_binding( + template='ToastGeneric', fallback='2ndtemplate', lang='zh-CN', addImageQuery='false', + baseUri='ms-appx:///', branding='none' +) +element_text = toast.add_text( + text='第一行 1st line for control freak', id_='1', lang='zh-CN', placement=None, + hint_maxLines='1', hint_style='title', hint_align='center', hint_wrap='false', + element_parent=element_binding +) +element_group = toast.add_group() +element_subgroup_left = toast.add_subgroup(element_parent=element_group) +element_text = toast.add_text( + text='第二行 2nd line for control freak', id_='2', lang='zh-CN', placement=None, + hint_maxLines='1', hint_style='captionSubtle ', hint_align='left', hint_wrap='false', + element_parent=element_subgroup_left +) +element_subgroup_right = toast.add_subgroup(element_parent=element_group) +element_text = toast.add_text( + text='第三行 3rd line for control freak', id_='3', lang='zh-CN', placement='attribution', + hint_maxLines='1', hint_style='captionSubtle', hint_align='left', hint_wrap='false', + element_parent=element_subgroup_right +) +toast.add_image( + path_pic, id_=None, alt='', addImageQuery='false', + placement='appLogoOverride', hint_crop='circle' +) +toast.set_actions() +toast.add_action( + '关闭 Close', arguments='dismiss', activationType='system', placement=None, + imageUri=None, hint_inputId=None, hint_buttonStyle=None, hint_toolTip='tip close' +) +notifier.show(toast) + +# %% which is equivalent to +xml = f""" + + + + 第一行 1st line for control freak + + + 第二行 2nd line for control freak + + + 第三行 3rd line for control freak + + + + + + + + + +""" +toast = Toast(xml) +notifier.show(toast) diff --git a/example/minimal.py b/example/minimal.py new file mode 100644 index 0000000..da593ce --- /dev/null +++ b/example/minimal.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +""" +minimal example + +@Author: modabao +@Time: 2022/5/3 10:33 +""" + +from winsdk_toast import Notifier, Toast + + +notifier = Notifier('程序名 applicationId') + +# %% minimal example +toast = Toast() +toast.add_text('第一行 1st line') +notifier.show(toast) + +# %% which is equivalent to +xml = """ + + + + 第一行 1st line + + + +""" +toast = Toast(xml) +notifier.show(toast) diff --git a/example/resource/python.ico b/example/resource/python.ico new file mode 100644 index 0000000..3cf51c6 Binary files /dev/null and b/example/resource/python.ico differ diff --git a/example/simple.py b/example/simple.py new file mode 100644 index 0000000..a997feb --- /dev/null +++ b/example/simple.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +""" +simple example + +@Author: modabao +@Time: 2022/5/3 10:33 +""" + +from os.path import abspath + +from winsdk_toast import Notifier, Toast + +path_pic = abspath('./resource/python.ico') + +notifier = Notifier('程序名 applicationId') + +# %% simple example +toast = Toast() +toast.add_text('第一行 1st line', hint_align='center', hint_style='caption') +toast.add_text('第二行 2nd line') +toast.add_text('第三行 3rd line', placement='attribution') +toast.add_image(path_pic, placement='appLogoOverride') +toast.add_action('关闭 Close') +toast.set_audio(silent='true') +notifier.show(toast) + +# %% which is equivalent to +xml = f""" + + + + 第一行 1st line + 第二行 2nd line + 第三行 3rd line + + + + + + +""" +toast = Toast(xml) +notifier.show(toast) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..fed528d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..43badb0 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,23 @@ +[metadata] +name = winsdk_toast +version = attr: winsdk_toast.__version__ +description = A simple package for displaying Windows Toast Notification based on winsdk +author = modabao +author_email = mo_dabao@qq.com +long_description = file: README.md +long_description_content_type = text/markdown +url = https://github.com/Mo-Dabao/winsdk_toast +license = MIT + +[options] +package_dir = + = src +packages = find: +install_requires = winsdk +include_package_data = True + +[options.packages.find] +where = src + +[options.package_data] +* = LICENSE diff --git a/src/winsdk_toast/__init__.py b/src/winsdk_toast/__init__.py new file mode 100644 index 0000000..13e59b8 --- /dev/null +++ b/src/winsdk_toast/__init__.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +""" + +@Author: modabao +@Time: 2022/5/1 11:05 +""" + +from winsdk_toast.toast import Toast +from winsdk_toast.notifier import Notifier + +__all__ = ['Toast', 'Notifier'] +__version__ = '0.0.1' diff --git a/src/winsdk_toast/notifier.py b/src/winsdk_toast/notifier.py new file mode 100644 index 0000000..72e8d8a --- /dev/null +++ b/src/winsdk_toast/notifier.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +""" + +@Author: modabao +@Time: 2022/5/1 11:52 +""" + +from winsdk.windows.ui.notifications import ToastNotificationManager + +from winsdk_toast.toast import Toast + + +class Notifier(object): + def __init__(self, applicationId): + """ + https://docs.microsoft.com/en-us/uwp/api/windows.ui.notifications.toastnotificationmanager.createtoastnotifier + Args: + applicationId: + """ + self.toast_notifier = ToastNotificationManager.create_toast_notifier(applicationId) + + def show(self, toast: Toast): + self.toast_notifier.show(toast.suit_up()) diff --git a/src/winsdk_toast/toast.py b/src/winsdk_toast/toast.py new file mode 100644 index 0000000..85770ff --- /dev/null +++ b/src/winsdk_toast/toast.py @@ -0,0 +1,364 @@ +# -*- coding: utf-8 -*- +""" +Handle or create XmlDocument toast. + +@Author: modabao +@Time: 2022/5/1 11:09 +""" + +from winsdk.windows.data.xml.dom import XmlDocument +from winsdk.windows.ui.notifications import ToastNotification + + +def set_attributes(element, attributes: dict): + for name, value in attributes.items(): + if value is None: + continue + element.set_attribute(name, value) + + +class Toast(object): + def __init__(self, xml: [str | None] = None): + self.xml_document = XmlDocument() + if xml is not None: + self.xml_document.load_xml(xml) + + def suit_up(self): + """Suit up to face notifier + + Returns: + toast_notification + """ + return ToastNotification(self.xml_document) + + def set_toast( + self, launch=None, duration=None, displayTimeStamp=None, scenario=None, useButtonStyle=None, + activationType='background' + ): + """ + https://docs.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-toast + Args: + launch: + duration: 'long' | 'short' + displayTimeStamp: + scenario: 'default' | 'reminder' | 'alarm' | 'incomingCall' | 'urgent' + useButtonStyle: 'false' | 'true' + activationType: 'background' | 'protocol' + + Returns: + element_toast + """ + xml_document = self.xml_document + element_toast = xml_document.create_element('toast') + xml_document.append_child(element_toast) + attributes = { + 'launch': launch, + 'duration': duration, + 'displayTimeStamp': displayTimeStamp, + 'scenario': scenario, + 'useButtonStyle': useButtonStyle, + 'activationType': activationType + } + set_attributes(element_toast, attributes) + return element_toast + + def set_visual(self, version=None, lang=None, baseUri=None, branding=None, addImageQuery=None): + """ + https://docs.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-visual + Args: + version: '1' + lang: + baseUri: 'ms-appx:///' + branding: Not used. 'none' | 'logo' | 'name' + addImageQuery: 'false' | 'true' + + Returns: + None + """ + xml_document = self.xml_document + element_toast = xml_document.select_single_node('/toast') or self.set_toast() + element_visual = xml_document.create_element('visual') + element_toast.append_child(element_visual) + attributes = { + 'version': version, + 'lang': lang, + 'baseUri': baseUri, + 'branding': branding, + 'addImageQuery': addImageQuery + } + set_attributes(element_visual, attributes) + return element_visual + + def set_binding( + self, template='ToastGeneric', fallback=None, lang=None, addImageQuery=None, baseUri=None, branding=None + ): + """ + https://docs.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-binding + Args: + template: 'ToastGeneric' + fallback: + lang: + addImageQuery: 'false' | 'true' + baseUri: 'ms-appx:///' + branding: Not used. 'none' | 'logo' | 'name' + + Returns: + element_binding + """ + xml_document = self.xml_document + element_visual = xml_document.select_single_node('//visual') or self.set_visual() + element_binding = xml_document.create_element('binding') + element_visual.append_child(element_binding) + attributes = { + 'template': template, + 'fallback': fallback, + 'lang': lang, + 'addImageQuery': addImageQuery, + 'baseUri': baseUri, + 'branding': branding + } + set_attributes(element_binding, attributes) + return element_binding + + def add_text( + self, text, id_=None, lang=None, placement=None, hint_maxLines=None, hint_style=None, hint_align=None, + hint_wrap=None, element_parent=None + ): + """ + https://docs.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-text + https://docs.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/adaptive-interactive-toasts + https://docs.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/adaptive-tiles-schema + Args: + text: + id_: + lang: + placement: 'attribution' ... + hint_maxLines: '1'-'4' + hint_style: 'base' | 'captionSubtle' ... + hint_align: 'left' | 'center' | 'right' + hint_wrap: 'false' | 'true' + element_parent: + + Returns: + element_text + """ + xml_document = self.xml_document + element_parent = element_parent or xml_document.select_single_node('//binding') or self.set_binding() + element_text = xml_document.create_element('text') + element_parent.append_child(element_text) + element_text.inner_text = text + attributes = { + 'id': id_, + 'lang': lang, + 'placement': placement, + 'hint-maxLines': hint_maxLines, + 'hint-style': hint_style, + 'hint-align': hint_align, + 'hint-wrap': hint_wrap + } + set_attributes(element_text, attributes) + return element_text + + def add_image( + self, src, id_=None, alt=None, addImageQuery=None, placement=None, hint_crop=None, element_parent=None + ): + """ + https://docs.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-image + https://docs.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/adaptive-interactive-toasts + Args: + src: (<3MB) + id_: + alt: + addImageQuery: 'false' | 'true' + placement: 'appLogoOverride' | 'hero' + hint_crop: 'circle' | '' + element_parent: + + Returns: + None + """ + xml_document = self.xml_document + element_parent = element_parent or xml_document.select_single_node('//binding') or self.set_binding() + element_image = xml_document.create_element('image') + element_parent.append_child(element_image) + attributes = { + 'src': src, + 'id': id_, + 'alt': alt, + 'addImageQuery': addImageQuery, + 'placement': placement, + 'hint-crop': hint_crop + } + set_attributes(element_image, attributes) + return element_image + + def add_progress(self, title=None, status='Progress', value='0.5', valueStringOverride=None, element_parent=None): + """ + https://docs.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/toast-progress-bar + Args: + title: + status: + value: '0.0' - '1.0' + valueStringOverride: + element_parent: + + Returns: + + """ + xml_document = self.xml_document + element_parent = element_parent or xml_document.select_single_node('//binding') or self.set_binding() + element_progress = xml_document.create_element('progress') + element_parent.append_child(element_progress) + attributes = { + 'title': title, + 'status': status, + 'value': value, + 'valueStringOverride': valueStringOverride + } + set_attributes(element_parent, attributes) + return element_progress + + def add_group(self): + """ + https://docs.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/adaptive-interactive-toasts + Returns: + element_group + """ + xml_document = self.xml_document + element_binding = xml_document.select_single_node('//binding') or self.set_binding() + element_group = xml_document.create_element('group') + element_binding.append_child(element_group) + return element_group + + def add_subgroup(self, element_parent=None): + """ + https://docs.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/adaptive-interactive-toasts + Returns: + element_group + """ + xml_document = self.xml_document + element_parent = element_parent or xml_document.select_single_node('//group') or self.add_group() + element_subgroup = xml_document.create_element('subgroup') + element_parent.append_child(element_subgroup) + return element_subgroup + + def set_actions(self): + """ + https://docs.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-actions + Returns: + None + """ + xml_document = self.xml_document + element_toast = xml_document.select_single_node('/toast') or self.set_toast() + element_actions = xml_document.create_element('actions') + element_toast.append_child(element_actions) + return element_actions + + def add_action( + self, content, arguments='dismiss', activationType='system', placement=None, imageUri=None, + hint_inputId=None, hint_buttonStyle=None, hint_toolTip=None + ): + """ + https://docs.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-action + https://docs.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/adaptive-interactive-toasts + type? + Args: + content: + arguments: ('dismiss' | 'snooze') for activationType='system' + activationType: 'system' | 'foreground' | 'background' | 'protocol' + placement: 'contextMenu' | '' + imageUri: + hint_inputId: + hint_buttonStyle: 'success' | 'citical' + hint_toolTip: + + Returns: + + """ + xml_document = self.xml_document + element_actions = xml_document.select_single_node('//actions') or self.set_actions() + element_action = xml_document.create_element('action') + element_actions.append_child(element_action) + attributes = { + 'content': content, + 'arguments': arguments, + 'activationType': activationType, + 'placement': placement, + 'imageUri': imageUri, + 'hint-inputId': hint_inputId, + 'hint-buttonStyle': hint_buttonStyle, + 'hint-toolTip': hint_toolTip + } + set_attributes(element_action, attributes) + return element_action + + def add_input(self, type_, id_=None, placeHolderContent=None, element_parent=None): + """ + https://docs.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-input + https://docs.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/adaptive-interactive-toasts + Args: + id_: + type_: 'text' | 'selection' + placeHolderContent: + + Returns: + + """ + xml_document = self.xml_document + element_parent = element_parent or xml_document.select_single_node('//actions') or self.set_actions() + element_input = xml_document.create_element('input') + element_parent.append_child(element_input) + attributes = { + 'type': type_, + 'id': id_, + 'placeHolderContent': placeHolderContent + } + set_attributes(element_input, attributes) + return element_input + + def add_selection(self, id_=None, content=None, element_parent=None): + """ + https://docs.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/adaptive-interactive-toasts + Args: + id_: + content: + element_parent: + + Returns: + element_selection + """ + pass + xml_document = self.xml_document + element_parent = element_parent or xml_document.select_single_node('//input') or self.add_input() + element_selection = xml_document.create_element('selection') + element_parent.append_child(element_selection) + attributes = { + 'id': id_, + 'content': content + } + set_attributes(element_selection, attributes) + return element_selection + + def set_audio(self, src=None, loop='false', silent='false'): + """ + https://docs.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-audio + https://docs.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/custom-audio-on-toasts + Args: + src: + loop: + silent: + + Returns: + + """ + xml_document = self.xml_document + element_toast = xml_document.select_single_node('/toast') or self.set_toast() + element_audio = xml_document.create_element('audio') + element_toast.append_child(element_audio) + attributes = { + 'src': src, + 'loop': loop, + 'silent': silent + } + set_attributes(element_audio, attributes) + return element_audio