mirror of
https://github.com/markqvist/Sideband.git
synced 2024-12-13 09:54:30 -05:00
207 lines
6.3 KiB
Python
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 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)
|