123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408 |
- # coding=utf-8
- # pystray
- # Copyright (C) 2016-2020 Moses Palmér
- #
- # This program is free software: you can redistribute it and/or modify it under
- # the terms of the GNU Lesser General Public License as published by the Free
- # Software Foundation, either version 3 of the License, or (at your option) any
- # later version.
- #
- # This program is distributed in the hope that it will be useful, but WITHOUT
- # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
- # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
- # details.
- #
- # You should have received a copy of the GNU Lesser General Public License
- # along with this program. If not, see <http://www.gnu.org/licenses/>.
- import ctypes
- import threading
- from ctypes import wintypes
- from six.moves import queue
- from ._util import serialized_image, win32
- from . import _base
- class Icon(_base.Icon):
- _HWND_TO_ICON = {}
- def __init__(self, *args, **kwargs):
- super(Icon, self).__init__(*args, **kwargs)
- self._atom = self._register_class()
- self._icon_handle = None
- self._hwnd = None
- self._menu_hwnd = None
- self._hmenu = None
- # This is a mapping from win32 event codes to handlers used by the
- # mainloop
- self._message_handlers = {
- win32.WM_STOP: self._on_stop,
- win32.WM_NOTIFY: self._on_notify,
- win32.WM_TASKBARCREATED: self._on_taskbarcreated}
- self._queue = queue.Queue()
- def __del__(self):
- if self._running:
- self._stop()
- if self._thread.ident != threading.current_thread().ident:
- self._thread.join()
- self._release_icon()
- def _show(self):
- self._assert_icon_handle()
- self._message(
- win32.NIM_ADD,
- win32.NIF_MESSAGE | win32.NIF_ICON | win32.NIF_TIP,
- uCallbackMessage=win32.WM_NOTIFY,
- hIcon=self._icon_handle,
- szTip=self.title)
- def _hide(self):
- self._message(
- win32.NIM_DELETE,
- 0)
- def _update_icon(self):
- self._release_icon()
- self._assert_icon_handle()
- self._message(
- win32.NIM_MODIFY,
- win32.NIF_ICON,
- hIcon=self._icon_handle)
- self._icon_valid = True
- def _update_title(self):
- self._message(
- win32.NIM_MODIFY,
- win32.NIF_TIP,
- szTip=self.title)
- def _notify(self, message, title=None):
- self._message(
- win32.NIM_MODIFY,
- win32.NIF_INFO,
- szInfo=message,
- szInfoTitle=title or self.title or '')
- def _remove_notification(self):
- self._message(
- win32.NIM_MODIFY,
- win32.NIF_INFO,
- szInfo='')
- def _update_menu(self):
- try:
- hmenu, callbacks = self._menu_handle
- win32.DestroyMenu(hmenu)
- except:
- pass
- callbacks = []
- hmenu = self._create_menu(self.menu, callbacks)
- if hmenu:
- self._menu_handle = (hmenu, callbacks)
- else:
- self._menu_handle = None
- def _run(self):
- # Create the message loop
- msg = wintypes.MSG()
- lpmsg = ctypes.byref(msg)
- win32.PeekMessage(
- lpmsg, None, win32.WM_USER, win32.WM_USER, win32.PM_NOREMOVE)
- self._hwnd = self._create_window(self._atom)
- self._menu_hwnd = self._create_window(self._atom)
- self._HWND_TO_ICON[self._hwnd] = self
- self._mark_ready()
- # Run the event loop
- self._thread = threading.current_thread()
- self._mainloop()
- def _run_detached(self):
- threading.Thread(target=lambda: self._run()).start()
- def _stop(self):
- win32.PostMessage(self._hwnd, win32.WM_STOP, 0, 0)
- def _mainloop(self):
- """The body of the main loop thread.
- This method retrieves all events from *Windows* and makes sure to
- dispatch clicks.
- """
- # Pump messages
- try:
- msg = wintypes.MSG()
- lpmsg = ctypes.byref(msg)
- while True:
- r = win32.GetMessage(lpmsg, None, 0, 0)
- if not r:
- break
- elif r == -1:
- break
- else:
- win32.TranslateMessage(lpmsg)
- win32.DispatchMessage(lpmsg)
- except:
- self._log.error(
- 'An error occurred in the main loop', exc_info=True)
- finally:
- try:
- self._hide()
- del self._HWND_TO_ICON[self._hwnd]
- except:
- # Ignore
- pass
- win32.DestroyWindow(self._hwnd)
- win32.DestroyWindow(self._menu_hwnd)
- if self._menu_handle:
- hmenu, callbacks = self._menu_handle
- win32.DestroyMenu(hmenu)
- self._unregister_class(self._atom)
- def _on_stop(self, wparam, lparam):
- """Handles ``WM_STOP``.
- This method posts a quit message, causing the mainloop thread to
- terminate.
- """
- win32.PostQuitMessage(0)
- def _on_notify(self, wparam, lparam):
- """Handles ``WM_NOTIFY``.
- If this is a left button click, this icon will be activated. If a menu
- is registered and this is a right button click, the popup menu will be
- displayed.
- """
- if lparam == win32.WM_LBUTTONUP:
- self()
- elif self._menu_handle and lparam == win32.WM_RBUTTONUP:
- # TrackPopupMenuEx does not behave unless our systray window is the
- # foreground window
- win32.SetForegroundWindow(self._hwnd)
- # Get the cursor position to determine where to display the menu
- point = wintypes.POINT()
- win32.GetCursorPos(ctypes.byref(point))
- # Display the menu and get the menu item identifier; the identifier
- # is the menu item index
- hmenu, descriptors = self._menu_handle
- index = win32.TrackPopupMenuEx(
- hmenu,
- win32.TPM_RIGHTALIGN | win32.TPM_BOTTOMALIGN
- | win32.TPM_RETURNCMD,
- point.x,
- point.y,
- self._menu_hwnd,
- None)
- if index > 0:
- descriptors[index - 1](self)
- def _on_taskbarcreated(self, wparam, lparam):
- """Handles ``WM_TASKBARCREATED``.
- This message is broadcast when the notification area becomes available.
- Handling this message allows catching explorer restarts.
- """
- if self.visible:
- self._show()
- def _create_window(self, atom):
- """Creates the system tray icon window.
- :param atom: The window class atom.
- :return: a window
- """
- # Broadcast messages (including WM_TASKBARCREATED) can be caught
- # only by top-level windows, so we cannot create a message-only window
- hwnd = win32.CreateWindowEx(
- 0,
- atom,
- None,
- win32.WS_POPUP,
- 0, 0, 0, 0,
- 0,
- None,
- win32.GetModuleHandle(None),
- None)
- # On Vista+, we must explicitly opt-in to receive WM_TASKBARCREATED
- # when running with escalated privileges
- win32.ChangeWindowMessageFilterEx(
- hwnd, win32.WM_TASKBARCREATED, win32.MSGFLT_ALLOW, None)
- return hwnd
- def _create_menu(self, descriptors, callbacks):
- """Creates a :class:`ctypes.wintypes.HMENU` from a
- :class:`pystray.Menu` instance.
- :param descriptors: The menu descriptors. If this is falsy, ``None`` is
- returned.
- :param callbacks: A list to which a callback is appended for every menu
- item created. The menu item IDs correspond to the items in this
- list plus one.
- :return: a menu
- """
- if not descriptors:
- return None
- else:
- # Generate the menu
- hmenu = win32.CreatePopupMenu()
- for i, descriptor in enumerate(descriptors):
- # Append the callbacks before creating the menu items to ensure
- # that the first item gets the ID 1
- callbacks.append(self._handler(descriptor))
- menu_item = self._create_menu_item(descriptor, callbacks)
- win32.InsertMenuItem(hmenu, i, True, ctypes.byref(menu_item))
- return hmenu
- def _create_menu_item(self, descriptor, callbacks):
- """Creates a :class:`pystray._util.win32.MENUITEMINFO` from a
- :class:`pystray.MenuItem` instance.
- :param descriptor: The menu item descriptor.
- :param callbacks: A list to which a callback is appended for every menu
- item created. The menu item IDs correspond to the items in this
- list plus one.
- :return: a :class:`pystray._util.win32.MENUITEMINFO`
- """
- if descriptor is _base.Menu.SEPARATOR:
- return win32.MENUITEMINFO(
- cbSize=ctypes.sizeof(win32.MENUITEMINFO),
- fMask=win32.MIIM_FTYPE,
- fType=win32.MFT_SEPARATOR)
- else:
- return win32.MENUITEMINFO(
- cbSize=ctypes.sizeof(win32.MENUITEMINFO),
- fMask=win32.MIIM_ID | win32.MIIM_STRING | win32.MIIM_STATE
- | win32.MIIM_FTYPE | win32.MIIM_SUBMENU,
- wID=len(callbacks),
- dwTypeData=descriptor.text,
- fState=0
- | (win32.MFS_DEFAULT if descriptor.default else 0)
- | (win32.MFS_CHECKED if descriptor.checked else 0)
- | (win32.MFS_DISABLED if not descriptor.enabled else 0),
- fType=0
- | (win32.MFT_RADIOCHECK if descriptor.radio else 0),
- hSubMenu=self._create_menu(descriptor.submenu, callbacks)
- if descriptor.submenu
- else None)
- def _message(self, code, flags, **kwargs):
- """Sends a message the the systray icon.
- This method adds ``cbSize``, ``hWnd``, ``hId`` and ``uFlags`` to the
- message data.
- :param int message: The message to send. This should be one of the
- ``NIM_*`` constants.
- :param int flags: The value of ``NOTIFYICONDATAW::uFlags``.
- :param kwargs: Data for the :class:`NOTIFYICONDATAW` object.
- """
- win32.Shell_NotifyIcon(code, win32.NOTIFYICONDATAW(
- cbSize=ctypes.sizeof(win32.NOTIFYICONDATAW),
- hWnd=self._hwnd,
- hID=id(self),
- uFlags=flags,
- **kwargs))
- def _release_icon(self):
- """Releases the icon handle and sets it to ``None``.
- If not icon handle is set, no action is performed.
- """
- if self._icon_handle:
- win32.DestroyIcon(self._icon_handle)
- self._icon_handle = None
- def _assert_icon_handle(self):
- """Asserts that the cached icon handle exists.
- """
- if self._icon_handle:
- return
- with serialized_image(self.icon, 'ICO') as icon_path:
- self._icon_handle = win32.LoadImage(
- None,
- icon_path,
- win32.IMAGE_ICON,
- 0,
- 0,
- win32.LR_DEFAULTSIZE | win32.LR_LOADFROMFILE)
- def _register_class(self):
- """Registers the systray window class.
- :return: the class atom
- """
- return win32.RegisterClassEx(win32.WNDCLASSEX(
- cbSize=ctypes.sizeof(win32.WNDCLASSEX),
- style=0,
- lpfnWndProc=_dispatcher,
- cbClsExtra=0,
- cbWndExtra=0,
- hInstance=win32.GetModuleHandle(None),
- hIcon=None,
- hCursor=None,
- hbrBackground=win32.COLOR_WINDOW + 1,
- lpszMenuName=None,
- lpszClassName='%s%dSystemTrayIcon' % (self.name, id(self)),
- hIconSm=None))
- def _unregister_class(self, atom):
- """Unregisters the systray window class.
- :param atom: The class atom returned by :meth:`_register_class`.
- """
- win32.UnregisterClass(atom, win32.GetModuleHandle(None))
- @win32.WNDPROC
- def _dispatcher(hwnd, uMsg, wParam, lParam):
- """The function used as window procedure for the systray window.
- """
- # These messages are sent before Icon._HWND_TO_ICON[hwnd] has been set, so
- # we handle them explicitly
- if uMsg == win32.WM_NCCREATE:
- return True
- if uMsg == win32.WM_CREATE:
- return 0
- try:
- icon = Icon._HWND_TO_ICON[hwnd]
- except KeyError:
- return win32.DefWindowProc(hwnd, uMsg, wParam, lParam)
- try:
- return int(icon._message_handlers.get(
- uMsg, lambda w, l: 0)(wParam, lParam) or 0)
- except:
- icon._log.error(
- 'An error occurred when calling message handler', exc_info=True)
- return 0
|