Sideband/sbapp/plyer/platforms/win/libs/balloontip.py

207 lines
6.3 KiB
Python

# -- coding: utf-8 --
'''
Module of Windows API for creating taskbar balloon tip
notification in the taskbar's tray notification area.
'''
__all__ = ('WindowsBalloonTip', 'balloon_tip')
import time
import ctypes
import atexit
from threading import RLock
from sbapp.plyer.platforms.win.libs import win_api_defs
WS_OVERLAPPED = 0x00000000
WS_SYSMENU = 0x00080000
WM_DESTROY = 2
CW_USEDEFAULT = 8
LR_LOADFROMFILE = 16
LR_DEFAULTSIZE = 0x0040
IDI_APPLICATION = 32512
IMAGE_ICON = 1
NOTIFYICON_VERSION_4 = 4
NIM_ADD = 0
NIM_MODIFY = 1
NIM_DELETE = 2
NIM_SETVERSION = 4
NIF_MESSAGE = 1
NIF_ICON = 2
NIF_TIP = 4
NIF_INFO = 0x10
NIIF_USER = 4
NIIF_LARGE_ICON = 0x20
class WindowsBalloonTip:
'''
Implementation of balloon tip notifications through Windows API.
* Register Window class name:
https://msdn.microsoft.com/en-us/library/windows/desktop/ms632596.aspx
* Create an overlapped window using the registered class.
- It's hidden everywhere in GUI unless ShowWindow(handle, SW_SHOW)
function is called.
* Show/remove a tray icon and a balloon tip notification.
Each instance is a separate notification with different parameters.
Can be used with Threads.
'''
_class_atom = 0
_wnd_class_ex = None
_hwnd = None
_hicon = None
_balloon_icon = None
_notify_data = None
_count = 0
_lock = RLock()
@staticmethod
def _get_unique_id():
'''
Keep track of each created balloon tip notification names,
so that they can be easily identified even from outside.
Make sure the count is shared between all the instances
i.e. use a lock, so that _count class variable is incremented
safely when using balloon tip notifications with Threads.
'''
WindowsBalloonTip._lock.acquire()
val = WindowsBalloonTip._count
WindowsBalloonTip._count += 1
WindowsBalloonTip._lock.release()
return val
def __init__(self, title, message, app_name, app_icon='',
timeout=10, **kwargs):
'''
The app_icon parameter, if given, is an .ICO file.
'''
atexit.register(self.__del__)
wnd_class_ex = win_api_defs.get_WNDCLASSEXW()
class_name = 'PlyerTaskbar' + str(WindowsBalloonTip._get_unique_id())
wnd_class_ex.lpszClassName = class_name
# keep ref to it as long as window is alive
wnd_class_ex.lpfnWndProc = win_api_defs.WindowProc(
win_api_defs.DefWindowProcW
)
wnd_class_ex.hInstance = win_api_defs.GetModuleHandleW(None)
if wnd_class_ex.hInstance is None:
raise Exception('Could not get windows module instance.')
class_atom = win_api_defs.RegisterClassExW(wnd_class_ex)
if class_atom == 0:
raise Exception('Could not register the PlyerTaskbar class.')
self._class_atom = class_atom
self._wnd_class_ex = wnd_class_ex
# create window
self._hwnd = win_api_defs.CreateWindowExW(
# dwExStyle, lpClassName, lpWindowName, dwStyle
0, class_atom, '', WS_OVERLAPPED,
# x, y, nWidth, nHeight
0, 0, CW_USEDEFAULT, CW_USEDEFAULT,
# hWndParent, hMenu, hInstance, lpParam
None, None, wnd_class_ex.hInstance, None
)
if self._hwnd is None:
raise Exception('Could not get create window.')
win_api_defs.UpdateWindow(self._hwnd)
# load .ICO file for as balloon tip and tray icon
if app_icon:
icon_flags = LR_LOADFROMFILE | LR_DEFAULTSIZE
hicon = win_api_defs.LoadImageW(
None, app_icon, IMAGE_ICON, 0, 0, icon_flags
)
if hicon is None:
raise Exception('Could not load icon {}'.format(app_icon))
self._balloon_icon = self._hicon = hicon
else:
self._hicon = win_api_defs.LoadIconW(
None,
ctypes.cast(IDI_APPLICATION, win_api_defs.LPCWSTR)
)
# show the notification
self.notify(title, message, app_name)
if timeout:
time.sleep(timeout)
def __del__(self):
'''
Clean visible parts of the notification object, then free all resources
allocated for creating the nofitication Window and icon.
'''
self.remove_notify()
if self._hicon is not None:
win_api_defs.DestroyIcon(self._hicon)
if self._wnd_class_ex is not None:
win_api_defs.UnregisterClassW(
self._class_atom,
self._wnd_class_ex.hInstance
)
if self._hwnd is not None:
win_api_defs.DestroyWindow(self._hwnd)
def notify(self, title, message, app_name):
'''
Displays a balloon in the systray. Can be called multiple times
with different parameter values.
'''
# remove previous visible balloon tip nofitication if available
self.remove_notify()
# add icon and messages to window
hicon = self._hicon
flags = NIF_TIP | NIF_INFO
icon_flag = 0
if hicon is not None:
flags |= NIF_ICON
# if icon is default app's one, don't display it in message
if self._balloon_icon is not None:
icon_flag = NIIF_USER | NIIF_LARGE_ICON
notify_data = win_api_defs.get_NOTIFYICONDATAW(
0, self._hwnd,
id(self), flags, 0, hicon, app_name, 0, 0, message,
NOTIFYICON_VERSION_4, title, icon_flag, win_api_defs.GUID(),
self._balloon_icon
)
self._notify_data = notify_data
if not win_api_defs.Shell_NotifyIconW(NIM_ADD, notify_data):
raise Exception('Shell_NotifyIconW failed.')
if not win_api_defs.Shell_NotifyIconW(NIM_SETVERSION,
notify_data):
raise Exception('Shell_NotifyIconW failed.')
def remove_notify(self):
'''
Removes the notify balloon, if displayed.
'''
if self._notify_data is not None:
win_api_defs.Shell_NotifyIconW(NIM_DELETE, self._notify_data)
self._notify_data = None
def balloon_tip(**kwargs):
'''
Instance for balloon tip notification implementation.
'''
WindowsBalloonTip(**kwargs)