nordvpntray/venv/lib/python3.12/site-packages/pystray/_darwin.py
2024-12-23 11:03:07 +01:00

269 lines
8.7 KiB
Python

# coding=utf-8
# pystray
# Copyright (C) 2016-2022 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 io
import signal
import subprocess
import AppKit
import Foundation
import objc
import PIL
import PyObjCTools.MachSignals
from . import _base
class Icon(_base.Icon):
#: The selector for the button action
_ACTION_SELECTOR = b'activate:sender'
#: The selector for the menu item actions
_MENU_ITEM_SELECTOR = b'activateMenuItem:sender'
# We support only a default action with an empty menu
HAS_DEFAULT_ACTION = False
# Mutually exclusive menu itema are not displayed distinctly
HAS_MENU_RADIO = False
def __init__(self, *args, **kwargs):
super(Icon, self).__init__(*args, **kwargs)
#: The NSImage version of the icon
self._icon_image = None
#: The NSApplication managing this icon
self._app = self._options['nsapplication'] \
if 'nsapplication' in self._options \
else AppKit.NSApplication.sharedApplication()
#: The icon delegate
self._delegate = IconDelegate.alloc().init()
self._delegate.icon = self
self._status_bar = AppKit.NSStatusBar.systemStatusBar()
self._status_item = self._status_bar.statusItemWithLength_(
AppKit.NSVariableStatusItemLength)
self._status_item.button().setTarget_(self._delegate)
self._status_item.button().setAction_(self._ACTION_SELECTOR)
def _show(self):
self._assert_image()
self._update_title()
self._status_item.button().setHidden_(False)
def _hide(self):
self._status_item.button().setHidden_(True)
def _update_icon(self):
self._icon_image = None
if self.visible:
self._assert_image()
self._icon_valid = True
def _update_title(self):
self._status_item.button().setToolTip_(self.title)
def _update_menu(self):
callbacks = []
nsmenu = self._create_menu(self.menu, callbacks)
if nsmenu:
self._status_item.setMenu_(nsmenu)
self._menu_handle = (nsmenu, callbacks)
else:
self._status_item.setMenu_(None)
self._menu_handle = None
def _run(self):
# Notify the setup callback
self._mark_ready()
def sigint(*args):
self._app.terminate_(None)
if previous_sigint:
previous_sigint(*args)
# Make sure that we do not inhibit ctrl+c
previous_sigint = PyObjCTools.MachSignals.signal(signal.SIGINT, sigint)
try:
self._app.run()
except:
self._log.error(
'An error occurred in the main loop', exc_info=True)
finally:
if PyObjCTools.MachSignals.getsignal(signal.SIGINT) == sigint:
PyObjCTools.MachSignals.signal(signal.SIGINT, previous_sigint)
self._status_bar.removeStatusItem_(self._status_item)
def _run_detached(self):
self._mark_ready()
def _notify(self, message, title=None):
subprocess.check_call([
'osascript',
'-e',
'display notification "{}" with title "{}"'.format(
message.replace('\\', '\\\\').replace('"', '\\"'),
title.replace('\\', '\\\\').replace('"', '\\"'))])
def _remove_notification(self):
pass
def _stop(self):
self._app.stop_(self._app)
# Post a dummy event; stop_ will only set a flag in NSApp, so it will
# not terminate until an event has been processed
event = getattr(
AppKit.NSEvent,
'otherEventWithType_'
'location_'
'modifierFlags_'
'timestamp_'
'windowNumber_'
'context_'
'subtype_'
'data1_'
'data2_')(
AppKit.NSApplicationDefined,
AppKit.NSPoint(0, 0),
0,
0.0,
0,
None,
0,
0,
0)
self._app.postEvent_atStart_(event, False)
def _assert_image(self):
"""Asserts that the cached icon image exists.
"""
thickness = self._status_bar.thickness()
size = (int(thickness), int(thickness))
if self._icon_image and self._icon_image.size() == size:
return
if self._icon.size == size:
source = self._icon
else:
source = PIL.Image.new(
'RGBA',
size)
source.paste(self._icon.resize(
size,
PIL.Image.LANCZOS))
# Convert the PIL image to an NSImage
b = io.BytesIO()
source.save(b, 'png')
data = Foundation.NSData(b.getvalue())
self._icon_image = AppKit.NSImage.alloc().initWithData_(data)
self._status_item.button().setImage_(self._icon_image)
def _create_menu(self, descriptors, callbacks):
"""Creates a :class:`AppKit.NSMenu` from a :class:`pystray.Menu`
instance.
If :meth:`_run` has not yet been called, ``None`` is returned.
: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 tags correspond to the items in this
list.
:return: a menu
"""
if not descriptors:
return None
else:
# Generate the menu
nsmenu = AppKit.NSMenu.alloc().initWithTitle_(self.name)
nsmenu.setAutoenablesItems_(False)
for descriptor in descriptors:
# Append the callback after creating the menu item to ensure
# that the first item gets the tag 0
nsmenu.addItem_(
self._create_menu_item(descriptor, callbacks))
callbacks.append(self._handler(descriptor))
return nsmenu
def _create_menu_item(self, descriptor, callbacks):
"""Creates a :class:`AppKit.NSMenuItem` 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 tags correspond to the items in this
list.
:return: a :class:`AppKit.NSMenuItem`
"""
if descriptor is _base.Menu.SEPARATOR:
return AppKit.NSMenuItem.separatorItem()
else:
menu_item = AppKit.NSMenuItem.alloc() \
.initWithTitle_action_keyEquivalent_(
descriptor.text, self._MENU_ITEM_SELECTOR, '')
if descriptor.submenu:
menu_item.setSubmenu_(self._create_menu(
descriptor.submenu, callbacks))
else:
menu_item.setAction_(self._MENU_ITEM_SELECTOR)
menu_item.setTarget_(self._delegate)
menu_item.setTag_(len(callbacks))
if descriptor.default:
menu_item.setAttributedTitle_(
Foundation.NSAttributedString.alloc()
.initWithString_attributes_(
descriptor.text,
Foundation.NSDictionary.alloc()
.initWithObjectsAndKeys_(
AppKit.NSFont.boldSystemFontOfSize_(
AppKit.NSFont.menuFontOfSize_(0)
.pointSize()),
AppKit.NSFontAttributeName)))
if descriptor.checked is not None:
menu_item.setState_(
AppKit.NSOnState if descriptor.checked
else AppKit.NSOffState)
menu_item.setEnabled_(descriptor.enabled)
return menu_item
class IconDelegate(Foundation.NSObject):
@objc.namedSelector(Icon._ACTION_SELECTOR)
def activate_button(self, sender):
self.icon()
@objc.namedSelector(Icon._MENU_ITEM_SELECTOR)
def activate_menu_item(self, sender):
nsmenu, callbacks = self.icon._menu_handle
callbacks[sender.tag()](self.icon)