Bump rumps from 0.3.0 to 0.4.0 (#1896)

* Bump rumps from 0.3.0 to 0.4.0

Bumps [rumps](https://github.com/jaredks/rumps) from 0.3.0 to 0.4.0.
- [Release notes](https://github.com/jaredks/rumps/releases)
- [Changelog](https://github.com/jaredks/rumps/blob/master/CHANGES.rst)
- [Commits](https://github.com/jaredks/rumps/commits)

---
updated-dependencies:
- dependency-name: rumps
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Update rumps==0.4.0

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>

[skip ci]
This commit is contained in:
dependabot[bot] 2022-11-14 11:27:16 -08:00 committed by GitHub
parent b31d75aeee
commit 79cf61c53e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 573 additions and 243 deletions

View file

@ -2,7 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# rumps: Ridiculously Uncomplicated macOS Python Statusbar apps. # rumps: Ridiculously Uncomplicated macOS Python Statusbar apps.
# Copyright: (c) 2017, Jared Suttles. All rights reserved. # Copyright: (c) 2020, Jared Suttles. All rights reserved.
# License: BSD, see LICENSE for details. # License: BSD, see LICENSE for details.
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@ -17,10 +17,14 @@ statusbar application.
""" """
__title__ = 'rumps' __title__ = 'rumps'
__version__ = '0.3.0' __version__ = '0.4.0'
__author__ = 'Jared Suttles' __author__ = 'Jared Suttles'
__license__ = 'Modified BSD' __license__ = 'Modified BSD'
__copyright__ = 'Copyright 2017 Jared Suttles' __copyright__ = 'Copyright 2020 Jared Suttles'
from .rumps import (separator, debug_mode, alert, notification, application_support, timers, quit_application, timer, from . import notifications as _notifications
clicked, notifications, MenuItem, SliderMenuItem, Timer, Window, App, slider) from .rumps import (separator, debug_mode, alert, application_support, timers, quit_application, timer,
clicked, MenuItem, SliderMenuItem, Timer, Window, App, slider)
notifications = _notifications.on_notification
notification = _notifications.notify

92
lib/rumps/_internal.py Normal file
View file

@ -0,0 +1,92 @@
# -*- coding: utf-8 -*-
from __future__ import print_function
import inspect
import traceback
import Foundation
from . import compat
from . import exceptions
def require_string(*objs):
for obj in objs:
if not isinstance(obj, compat.string_types):
raise TypeError(
'a string is required but given {0}, a {1}'.format(obj, type(obj).__name__)
)
def require_string_or_none(*objs):
for obj in objs:
if not(obj is None or isinstance(obj, compat.string_types)):
raise TypeError(
'a string or None is required but given {0}, a {1}'.format(obj, type(obj).__name__)
)
def call_as_function_or_method(func, *args, **kwargs):
# The idea here is that when using decorators in a class, the functions passed are not bound so we have to
# determine later if the functions we have (those saved as callbacks) for particular events need to be passed
# 'self'.
#
# This works for an App subclass method or a standalone decorated function. Will attempt to find function as
# a bound method of the App instance. If it is found, use it, otherwise simply call function.
from . import rumps
try:
app = getattr(rumps.App, '*app_instance')
except AttributeError:
pass
else:
for name, method in inspect.getmembers(app, predicate=inspect.ismethod):
if method.__func__ is func:
return method(*args, **kwargs)
return func(*args, **kwargs)
def guard_unexpected_errors(func):
"""Decorator to be used in PyObjC callbacks where an error bubbling up
would cause a crash. Instead of crashing, print the error to stderr and
prevent passing to PyObjC layer.
For Python 3, print the exception using chaining. Accomplished by setting
the cause of :exc:`rumps.exceptions.InternalRumpsError` to the exception.
For Python 2, emulate exception chaining by printing the original exception
followed by :exc:`rumps.exceptions.InternalRumpsError`.
"""
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
internal_error = exceptions.InternalRumpsError(
'an unexpected error occurred within an internal callback'
)
if compat.PY2:
import sys
traceback.print_exc()
print('\nThe above exception was the direct cause of the following exception:\n', file=sys.stderr)
traceback.print_exception(exceptions.InternalRumpsError, internal_error, None)
else:
internal_error.__cause__ = e
traceback.print_exception(exceptions.InternalRumpsError, internal_error, None)
return wrapper
def string_to_objc(x):
if isinstance(x, compat.binary_type):
return Foundation.NSData.alloc().initWithData_(x)
elif isinstance(x, compat.string_types):
return Foundation.NSString.alloc().initWithString_(x)
else:
raise TypeError(
"expected a string or a bytes-like object but provided %s, "
"having type '%s'" % (
x,
type(x).__name__
)
)

View file

@ -1,17 +1,33 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""
rumps.compat
~~~~~~~~~~~~
Compatibility for Python 2 and Python 3 major versions.
:copyright: (c) 2020 by Jared Suttles
:license: BSD-3-Clause, see LICENSE for details.
"""
import sys import sys
PY2 = sys.version_info[0] == 2 PY2 = sys.version_info[0] == 2
if not PY2: if not PY2:
binary_type = bytes
text_type = str text_type = str
string_types = (str,) string_types = (str,)
iteritems = lambda d: iter(d.items()) iteritems = lambda d: iter(d.items())
import collections.abc as collections_abc
else: else:
binary_type = ()
text_type = unicode text_type = unicode
string_types = (str, unicode) string_types = (str, unicode)
iteritems = lambda d: d.iteritems() iteritems = lambda d: d.iteritems()
import collections as collections_abc

40
lib/rumps/events.py Normal file
View file

@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
import traceback
from . import _internal
class EventEmitter(object):
def __init__(self, name):
self.name = name
self.callbacks = set()
self._executor = _internal.call_as_function_or_method
def register(self, func):
self.callbacks.add(func)
return func
def unregister(self, func):
try:
self.callbacks.remove(func)
return True
except KeyError:
return False
def emit(self, *args, **kwargs):
#print('EventEmitter("%s").emit called' % self.name)
for callback in self.callbacks:
try:
self._executor(callback, *args, **kwargs)
except Exception:
traceback.print_exc()
__call__ = register
before_start = EventEmitter('before_start')
on_notification = EventEmitter('on_notification')
on_sleep = EventEmitter('on_sleep')
on_wake = EventEmitter('on_wake')
before_quit = EventEmitter('before_quit')

9
lib/rumps/exceptions.py Normal file
View file

@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
class RumpsError(Exception):
"""A generic rumps error occurred."""
class InternalRumpsError(RumpsError):
"""Internal mechanism powering functionality of rumps failed."""

View file

@ -1,10 +1,266 @@
INFO_PLIST_TEMPLATE = '''\ # -*- coding: utf-8 -*-
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> _ENABLED = True
<plist version="1.0"> try:
<dict> from Foundation import NSUserNotification, NSUserNotificationCenter
<key>CFBundleIdentifier</key> except ImportError:
<string>%(name)s</string> _ENABLED = False
</dict>
</plist> import datetime
''' import os
import sys
import traceback
import Foundation
from . import _internal
from . import compat
from . import events
def on_notification(f):
"""Decorator for registering a function to serve as a "notification center"
for the application. This function will receive the data associated with an
incoming macOS notification sent using :func:`rumps.notification`. This
occurs whenever the user clicks on a notification for this application in
the macOS Notification Center.
.. code-block:: python
@rumps.notifications
def notification_center(info):
if 'unix' in info:
print 'i know this'
"""
return events.on_notification.register(f)
def _gather_info_issue_9(): # pragma: no cover
missing_plist = False
missing_bundle_ident = False
info_plist_path = os.path.join(os.path.dirname(sys.executable), 'Info.plist')
try:
with open(info_plist_path) as f:
import plistlib
try:
load_plist = plistlib.load
except AttributeError:
load_plist = plistlib.readPlist
try:
load_plist(f)['CFBundleIdentifier']
except Exception:
missing_bundle_ident = True
except IOError as e:
import errno
if e.errno == errno.ENOENT: # No such file or directory
missing_plist = True
info = '\n\n'
if missing_plist:
info += 'In this case there is no file at "%(info_plist_path)s"'
info += '\n\n'
confidence = 'should'
elif missing_bundle_ident:
info += 'In this case the file at "%(info_plist_path)s" does not contain a value for "CFBundleIdentifier"'
info += '\n\n'
confidence = 'should'
else:
confidence = 'may'
info += 'Running the following command %(confidence)s fix the issue:\n'
info += '/usr/libexec/PlistBuddy -c \'Add :CFBundleIdentifier string "rumps"\' %(info_plist_path)s\n'
return info % {'info_plist_path': info_plist_path, 'confidence': confidence}
def _default_user_notification_center():
notification_center = NSUserNotificationCenter.defaultUserNotificationCenter()
if notification_center is None: # pragma: no cover
info = (
'Failed to setup the notification center. This issue occurs when the "Info.plist" file '
'cannot be found or is missing "CFBundleIdentifier".'
)
try:
info += _gather_info_issue_9()
except Exception:
pass
raise RuntimeError(info)
else:
return notification_center
def _init_nsapp(nsapp):
if _ENABLED:
try:
notification_center = _default_user_notification_center()
except RuntimeError:
pass
else:
notification_center.setDelegate_(nsapp)
@_internal.guard_unexpected_errors
def _clicked(ns_user_notification_center, ns_user_notification):
from . import rumps
ns_user_notification_center.removeDeliveredNotification_(ns_user_notification)
ns_dict = ns_user_notification.userInfo()
if ns_dict is None:
data = None
else:
dumped = ns_dict['value']
app = getattr(rumps.App, '*app_instance', rumps.App)
try:
data = app.serializer.loads(dumped)
except Exception:
traceback.print_exc()
return
# notification center function not specified => no error but log warning
if not events.on_notification.callbacks:
rumps._log(
'WARNING: notification received but no function specified for '
'answering it; use @notifications decorator to register a function.'
)
else:
notification = Notification(ns_user_notification, data)
events.on_notification.emit(notification)
def notify(title, subtitle, message, data=None, sound=True,
action_button=None, other_button=None, has_reply_button=False,
icon=None, ignoreDnD=False):
"""Send a notification to Notification Center (OS X 10.8+). If running on a
version of macOS that does not support notifications, a ``RuntimeError``
will be raised. Apple says,
"The userInfo content must be of reasonable serialized size (less than
1k) or an exception will be thrown."
So don't do that!
:param title: text in a larger font.
:param subtitle: text in a smaller font below the `title`.
:param message: text representing the body of the notification below the
`subtitle`.
:param data: will be passed to the application's "notification center" (see
:func:`rumps.notifications`) when this notification is clicked.
:param sound: whether the notification should make a noise when it arrives.
:param action_button: title for the action button.
:param other_button: title for the other button.
:param has_reply_button: whether or not the notification has a reply button.
:param icon: the filename of an image for the notification's icon, will
replace the default.
:param ignoreDnD: whether the notification should ignore do not disturb,
e.g., appear also while screen sharing.
"""
from . import rumps
if not _ENABLED:
raise RuntimeError('OS X 10.8+ is required to send notifications')
_internal.require_string_or_none(title, subtitle, message)
notification = NSUserNotification.alloc().init()
notification.setTitle_(title)
notification.setSubtitle_(subtitle)
notification.setInformativeText_(message)
if data is not None:
app = getattr(rumps.App, '*app_instance', rumps.App)
dumped = app.serializer.dumps(data)
objc_string = _internal.string_to_objc(dumped)
ns_dict = Foundation.NSMutableDictionary.alloc().init()
ns_dict.setDictionary_({'value': objc_string})
notification.setUserInfo_(ns_dict)
if icon is not None:
notification.set_identityImage_(rumps._nsimage_from_file(icon))
if sound:
notification.setSoundName_("NSUserNotificationDefaultSoundName")
if action_button:
notification.setActionButtonTitle_(action_button)
notification.set_showsButtons_(True)
if other_button:
notification.setOtherButtonTitle_(other_button)
notification.set_showsButtons_(True)
if has_reply_button:
notification.setHasReplyButton_(True)
if ignoreDnD:
notification.set_ignoresDoNotDisturb_(True)
notification.setDeliveryDate_(Foundation.NSDate.dateWithTimeInterval_sinceDate_(0, Foundation.NSDate.date()))
notification_center = _default_user_notification_center()
notification_center.scheduleNotification_(notification)
class Notification(compat.collections_abc.Mapping):
def __init__(self, ns_user_notification, data):
self._ns = ns_user_notification
self._data = data
def __repr__(self):
return '<{0}: [data: {1}]>'.format(type(self).__name__, repr(self._data))
@property
def title(self):
return compat.text_type(self._ns.title())
@property
def subtitle(self):
return compat.text_type(self._ns.subtitle())
@property
def message(self):
return compat.text_type(self._ns.informativeText())
@property
def activation_type(self):
activation_type = self._ns.activationType()
if activation_type == 1:
return 'contents_clicked'
elif activation_type == 2:
return 'action_button_clicked'
elif activation_type == 3:
return 'replied'
elif activation_type == 4:
return 'additional_action_clicked'
@property
def delivered_at(self):
ns_date = self._ns.actualDeliveryDate()
seconds = ns_date.timeIntervalSince1970()
dt = datetime.datetime.fromtimestamp(seconds)
return dt
@property
def response(self):
ns_attributed_string = self._ns.response()
if ns_attributed_string is None:
return None
ns_string = ns_attributed_string.string()
return compat.text_type(ns_string)
@property
def data(self):
return self._data
def _check_if_mapping(self):
if not isinstance(self._data, compat.collections_abc.Mapping):
raise TypeError(
'notification cannot be used as a mapping when data is not a '
'mapping'
)
def __getitem__(self, key):
self._check_if_mapping()
return self._data[key]
def __iter__(self):
self._check_if_mapping()
return iter(self._data)
def __len__(self):
self._check_if_mapping()
return len(self._data)

View file

@ -82,7 +82,7 @@ class OrderedDict(dict):
def clear(self): def clear(self):
'od.clear() -> None. Remove all items from od.' 'od.clear() -> None. Remove all items from od.'
try: try:
for node in self.__map.itervalues(): for node in self.__map.values():
del node[:] del node[:]
root = self.__root root = self.__root
root[:] = [root, root, None] root[:] = [root, root, None]

View file

@ -1,38 +1,32 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
#
# rumps: Ridiculously Uncomplicated macOS Python Statusbar apps.
# Copyright: (c) 2017, Jared Suttles. All rights reserved.
# License: BSD, see LICENSE for details.
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
_NOTIFICATIONS = True # rumps: Ridiculously Uncomplicated macOS Python Statusbar apps.
# Copyright: (c) 2020, Jared Suttles. All rights reserved.
# License: BSD, see LICENSE for details.
# For compatibility with pyinstaller # For compatibility with pyinstaller
# See: http://stackoverflow.com/questions/21058889/pyinstaller-not-finding-pyobjc-library-macos-python # See: http://stackoverflow.com/questions/21058889/pyinstaller-not-finding-pyobjc-library-macos-python
import Foundation import Foundation
import AppKit import AppKit
try:
from Foundation import NSUserNotification, NSUserNotificationCenter
except ImportError:
_NOTIFICATIONS = False
from Foundation import (NSDate, NSTimer, NSRunLoop, NSDefaultRunLoopMode, NSSearchPathForDirectoriesInDomains, from Foundation import (NSDate, NSTimer, NSRunLoop, NSDefaultRunLoopMode, NSSearchPathForDirectoriesInDomains,
NSMakeRect, NSLog, NSObject, NSMutableDictionary, NSString) NSMakeRect, NSLog, NSObject, NSMutableDictionary, NSString, NSUserDefaults)
from AppKit import NSApplication, NSStatusBar, NSMenu, NSMenuItem, NSAlert, NSTextField, NSSecureTextField, NSImage, NSSlider, NSSize from AppKit import NSApplication, NSStatusBar, NSMenu, NSMenuItem, NSAlert, NSTextField, NSSecureTextField, NSImage, NSSlider, NSSize, NSWorkspace, NSWorkspaceWillSleepNotification, NSWorkspaceDidWakeNotification
from PyObjCTools import AppHelper from PyObjCTools import AppHelper
import inspect
import os import os
import pickle import pickle
import sys
import traceback import traceback
import weakref import weakref
from collections import Mapping, Iterable from .compat import text_type, string_types, iteritems, collections_abc
from .text_field import Editing, SecureEditing
from .utils import ListDict from .utils import ListDict
from .compat import text_type, string_types, iteritems
from . import _internal
from . import events
from . import notifications
_TIMERS = weakref.WeakKeyDictionary() _TIMERS = weakref.WeakKeyDictionary()
separator = object() separator = object()
@ -78,11 +72,13 @@ def alert(title=None, message='', ok=None, cancel=None, other=None, icon_path=No
message = message.replace('%', '%%') message = message.replace('%', '%%')
if title is not None: if title is not None:
title = text_type(title) title = text_type(title)
_require_string_or_none(ok) _internal.require_string_or_none(ok)
if not isinstance(cancel, string_types): if not isinstance(cancel, string_types):
cancel = 'Cancel' if cancel else None cancel = 'Cancel' if cancel else None
alert = NSAlert.alertWithMessageText_defaultButton_alternateButton_otherButton_informativeTextWithFormat_( alert = NSAlert.alertWithMessageText_defaultButton_alternateButton_otherButton_informativeTextWithFormat_(
title, ok, cancel, other, message) title, ok, cancel, other, message)
if NSUserDefaults.standardUserDefaults().stringForKey_('AppleInterfaceStyle') == 'Dark':
alert.window().setAppearance_(AppKit.NSAppearance.appearanceNamed_('NSAppearanceNameVibrantDark'))
alert.setAlertStyle_(0) # informational style alert.setAlertStyle_(0) # informational style
if icon_path is not None: if icon_path is not None:
icon = _nsimage_from_file(icon_path) icon = _nsimage_from_file(icon_path)
@ -91,119 +87,6 @@ def alert(title=None, message='', ok=None, cancel=None, other=None, icon_path=No
return alert.runModal() return alert.runModal()
def _gather_info_issue_9():
missing_plist = False
missing_bundle_ident = False
info_plist_path = os.path.join(os.path.dirname(sys.executable), 'Info.plist')
try:
with open(info_plist_path) as f:
import plistlib
try:
load_plist = plistlib.load
except AttributeError:
load_plist = plistlib.readPlist
try:
load_plist(f)['CFBundleIdentifier']
except Exception:
missing_bundle_ident = True
except IOError as e:
import errno
if e.errno == errno.ENOENT: # No such file or directory
missing_plist = True
info = '\n\n'
if missing_plist:
info += 'In this case there is no file at "%(info_plist_path)s"'
info += '\n\n'
confidence = 'should'
elif missing_bundle_ident:
info += 'In this case the file at "%(info_plist_path)s" does not contain a value for "CFBundleIdentifier"'
info += '\n\n'
confidence = 'should'
else:
confidence = 'may'
info += 'Running the following command %(confidence)s fix the issue:\n'
info += '/usr/libexec/PlistBuddy -c \'Add :CFBundleIdentifier string "rumps"\' %(info_plist_path)s\n'
return info % {'info_plist_path': info_plist_path, 'confidence': confidence}
def _default_user_notification_center():
notification_center = NSUserNotificationCenter.defaultUserNotificationCenter()
if notification_center is None:
info = (
'Failed to setup the notification center. This issue occurs when the "Info.plist" file '
'cannot be found or is missing "CFBundleIdentifier".'
)
try:
info += _gather_info_issue_9()
except Exception:
pass
raise RuntimeError(info)
else:
return notification_center
def notification(title, subtitle, message, data=None, sound=True, action_button=None, other_button=None,
has_reply_button=False, icon=None):
"""Send a notification to Notification Center (OS X 10.8+). If running on a version of macOS that does not
support notifications, a ``RuntimeError`` will be raised. Apple says,
"The userInfo content must be of reasonable serialized size (less than 1k) or an exception will be thrown."
So don't do that!
:param title: text in a larger font.
:param subtitle: text in a smaller font below the `title`.
:param message: text representing the body of the notification below the `subtitle`.
:param data: will be passed to the application's "notification center" (see :func:`rumps.notifications`) when this
notification is clicked.
:param sound: whether the notification should make a noise when it arrives.
:param action_button: title for the action button.
:param other_button: title for the other button.
:param has_reply_button: whether or not the notification has a reply button.
:param icon: the filename of an image for the notification's icon, will replace the default.
"""
if not _NOTIFICATIONS:
raise RuntimeError('OS X 10.8+ is required to send notifications')
if data is not None and not isinstance(data, Mapping):
raise TypeError('notification data must be a mapping')
_require_string_or_none(title, subtitle, message)
notification = NSUserNotification.alloc().init()
notification.setTitle_(title)
notification.setSubtitle_(subtitle)
notification.setInformativeText_(message)
if data is not None:
app = getattr(App, '*app_instance')
dumped = app.serializer.dumps(data)
ns_dict = NSMutableDictionary.alloc().init()
ns_string = NSString.alloc().initWithString_(dumped)
ns_dict.setDictionary_({'value': ns_string})
notification.setUserInfo_(ns_dict)
if icon is not None:
notification.set_identityImage_(_nsimage_from_file(icon))
if sound:
notification.setSoundName_("NSUserNotificationDefaultSoundName")
if action_button:
notification.setActionButtonTitle_(action_button)
notification.set_showsButtons_(True)
if other_button:
notification.setOtherButtonTitle_(other_button)
notification.set_showsButtons_(True)
if has_reply_button:
notification.setHasReplyButton_(True)
notification.setDeliveryDate_(NSDate.dateWithTimeInterval_sinceDate_(0, NSDate.date()))
notification_center = _default_user_notification_center()
notification_center.scheduleNotification_(notification)
def application_support(name): def application_support(name):
"""Return the application support folder path for the given `name`, creating it if it doesn't exist.""" """Return the application support folder path for the given `name`, creating it if it doesn't exist."""
app_support_path = os.path.join(NSSearchPathForDirectoriesInDomains(14, 1, 1).objectAtIndex_(0), name) app_support_path = os.path.join(NSSearchPathForDirectoriesInDomains(14, 1, 1).objectAtIndex_(0), name)
@ -248,18 +131,6 @@ def _nsimage_from_file(filename, dimensions=None, template=None):
return image return image
def _require_string(*objs):
for obj in objs:
if not isinstance(obj, string_types):
raise TypeError('a string is required but given {0}, a {1}'.format(obj, type(obj).__name__))
def _require_string_or_none(*objs):
for obj in objs:
if not(obj is None or isinstance(obj, string_types)):
raise TypeError('a string or None is required but given {0}, a {1}'.format(obj, type(obj).__name__))
# Decorators and helper function serving to register functions for dealing with interaction and events # Decorators and helper function serving to register functions for dealing with interaction and events
#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def timer(interval): def timer(interval):
@ -365,36 +236,6 @@ def slider(*args, **options):
return f return f
return decorator return decorator
def notifications(f):
"""Decorator for registering a function to serve as a "notification center" for the application. This function will
receive the data associated with an incoming macOS notification sent using :func:`rumps.notification`. This occurs
whenever the user clicks on a notification for this application in the macOS Notification Center.
.. code-block:: python
@rumps.notifications
def notification_center(info):
if 'unix' in info:
print 'i know this'
"""
notifications.__dict__['*notification_center'] = f
return f
def _call_as_function_or_method(func, event):
# The idea here is that when using decorators in a class, the functions passed are not bound so we have to
# determine later if the functions we have (those saved as callbacks) for particular events need to be passed
# 'self'.
#
# This works for an App subclass method or a standalone decorated function. Will attempt to find function as
# a bound method of the App instance. If it is found, use it, otherwise simply call function.
app = getattr(App, '*app_instance')
for name, method in inspect.getmembers(app, predicate=inspect.ismethod):
if method.__func__ is func:
return method(event)
return func(event)
#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@ -468,14 +309,14 @@ class Menu(ListDict):
menu.add(iterable) menu.add(iterable)
return return
for n, ele in enumerate(iteritems(iterable) if isinstance(iterable, Mapping) else iterable): for n, ele in enumerate(iteritems(iterable) if isinstance(iterable, collections_abc.Mapping) else iterable):
# for mappings we recurse but don't drop down a level in the menu # for mappings we recurse but don't drop down a level in the menu
if not isinstance(ele, MenuItem) and isinstance(ele, Mapping): if not isinstance(ele, MenuItem) and isinstance(ele, collections_abc.Mapping):
parse_menu(ele, menu, depth) parse_menu(ele, menu, depth)
# any iterables other than strings and MenuItems # any iterables other than strings and MenuItems
elif not isinstance(ele, (string_types, MenuItem)) and isinstance(ele, Iterable): elif not isinstance(ele, (string_types, MenuItem)) and isinstance(ele, collections_abc.Iterable):
try: try:
menuitem, submenu = ele menuitem, submenu = ele
except TypeError: except TypeError:
@ -580,7 +421,7 @@ class MenuItem(Menu):
# NOTE: # NOTE:
# Because of the quirks of PyObjC, a class level dictionary **inside an NSObject subclass for 10.9.x** is required # Because of the quirks of PyObjC, a class level dictionary **inside an NSObject subclass for 10.9.x** is required
# in order to have callback_ be a @classmethod. And we need callback_ to be class level because we can't use # in order to have callback_ be a @classmethod. And we need callback_ to be class level because we can't use
# instances in setTarget_ method of NSMenuItem. Otherwise this would be much more straightfoward like Timer class. # instances in setTarget_ method of NSMenuItem. Otherwise this would be much more straightforward like Timer class.
# #
# So the target is always the NSApp class and action is always the @classmethod callback_ -- for every function # So the target is always the NSApp class and action is always the @classmethod callback_ -- for every function
# decorated with @clicked(...). All we do is lookup the MenuItem instance and the user-provided callback function # decorated with @clicked(...). All we do is lookup the MenuItem instance and the user-provided callback function
@ -690,6 +531,35 @@ class MenuItem(Menu):
def state(self, new_state): def state(self, new_state):
self._menuitem.setState_(new_state) self._menuitem.setState_(new_state)
@property
def hidden(self):
"""Indicates whether the menu item is hidden.
.. versionadded:: 0.4.0
"""
return self._menuitem.isHidden()
@hidden.setter
def hidden(self, value):
self._menuitem.setHidden_(value)
def hide(self):
"""Hide the menu item.
.. versionadded:: 0.4.0
"""
self.hidden = True
def show(self):
"""Show the menu item.
.. versionadded:: 0.4.0
"""
self.hidden = False
def set_callback(self, callback, key=None): def set_callback(self, callback, key=None):
"""Set the function serving as callback for when a click event occurs on this menu item. When `callback` is """Set the function serving as callback for when a click event occurs on this menu item. When `callback` is
``None``, it will disable the callback function and grey out the menu item. If `key` is a string, set as the ``None``, it will disable the callback function and grey out the menu item. If `key` is a string, set as the
@ -703,7 +573,7 @@ class MenuItem(Menu):
:param callback: the function to be called when the user clicks on this menu item. :param callback: the function to be called when the user clicks on this menu item.
:param key: the key shortcut to click this menu item. :param key: the key shortcut to click this menu item.
""" """
_require_string_or_none(key) _internal.require_string_or_none(key)
if key is not None: if key is not None:
self._menuitem.setKeyEquivalent_(key) self._menuitem.setKeyEquivalent_(key)
NSApp._ns_to_py_and_callback[self._menuitem] = self, callback NSApp._ns_to_py_and_callback[self._menuitem] = self, callback
@ -744,7 +614,7 @@ class SliderMenuItem(object):
self._slider = NSSlider.alloc().init() self._slider = NSSlider.alloc().init()
self._slider.setMinValue_(min_value) self._slider.setMinValue_(min_value)
self._slider.setMaxValue_(max_value) self._slider.setMaxValue_(max_value)
self._slider.setValue_(value) self._slider.setDoubleValue_(value)
self._slider.setFrameSize_(NSSize(*dimensions)) self._slider.setFrameSize_(NSSize(*dimensions))
self._slider.setTarget_(NSApp) self._slider.setTarget_(NSApp)
self._menuitem = NSMenuItem.alloc().init() self._menuitem = NSMenuItem.alloc().init()
@ -774,11 +644,11 @@ class SliderMenuItem(object):
@property @property
def value(self): def value(self):
"""The current position of the slider.""" """The current position of the slider."""
return self._slider.value() return self._slider.doubleValue()
@value.setter @value.setter
def value(self, new_value): def value(self, new_value):
self._slider.setValue_(new_value) self._slider.setDoubleValue_(new_value)
class SeparatorMenuItem(object): class SeparatorMenuItem(object):
@ -860,9 +730,9 @@ class Timer(object):
def callback_(self, _): def callback_(self, _):
_log(self) _log(self)
try: try:
return _call_as_function_or_method(getattr(self, '*callback'), self) return _internal.call_as_function_or_method(getattr(self, '*callback'), self)
except Exception: except Exception:
_log(traceback.format_exc()) traceback.print_exc()
class Window(object): class Window(object):
@ -899,7 +769,7 @@ class Window(object):
self._cancel = bool(cancel) self._cancel = bool(cancel)
self._icon = None self._icon = None
_require_string_or_none(ok) _internal.require_string_or_none(ok)
if not isinstance(cancel, string_types): if not isinstance(cancel, string_types):
cancel = 'Cancel' if cancel else None cancel = 'Cancel' if cancel else None
@ -908,9 +778,9 @@ class Window(object):
self._alert.setAlertStyle_(0) # informational style self._alert.setAlertStyle_(0) # informational style
if secure: if secure:
self._textfield = NSSecureTextField.alloc().initWithFrame_(NSMakeRect(0, 0, *dimensions)) self._textfield = SecureEditing.alloc().initWithFrame_(NSMakeRect(0, 0, *dimensions))
else: else:
self._textfield = NSTextField.alloc().initWithFrame_(NSMakeRect(0, 0, *dimensions)) self._textfield = Editing.alloc().initWithFrame_(NSMakeRect(0, 0, *dimensions))
self._textfield.setSelectable_(True) self._textfield.setSelectable_(True)
self._alert.setAccessoryView_(self._textfield) self._alert.setAccessoryView_(self._textfield)
@ -982,7 +852,7 @@ class Window(object):
:param name: the text for a new button. Must be a string. :param name: the text for a new button. Must be a string.
""" """
_require_string(name) _internal.require_string(name)
self._alert.addButtonWithTitle_(name) self._alert.addButtonWithTitle_(name)
def add_buttons(self, iterable=None, *args): def add_buttons(self, iterable=None, *args):
@ -1009,6 +879,8 @@ class Window(object):
:return: a :class:`rumps.rumps.Response` object that contains the text and the button clicked as an integer. :return: a :class:`rumps.rumps.Response` object that contains the text and the button clicked as an integer.
""" """
_log(self) _log(self)
if NSUserDefaults.standardUserDefaults().stringForKey_('AppleInterfaceStyle') == 'Dark':
self._alert.window().setAppearance_(AppKit.NSAppearance.appearanceNamed_('NSAppearanceNameVibrantDark'))
clicked = self._alert.runModal() % 999 clicked = self._alert.runModal() % 999
if clicked > 2 and self._cancel: if clicked > 2 and self._cancel:
clicked -= 1 clicked -= 1
@ -1062,27 +934,7 @@ class NSApp(NSObject):
_ns_to_py_and_callback = {} _ns_to_py_and_callback = {}
def userNotificationCenter_didActivateNotification_(self, notification_center, notification): def userNotificationCenter_didActivateNotification_(self, notification_center, notification):
notification_center.removeDeliveredNotification_(notification) notifications._clicked(notification_center, notification)
ns_dict = notification.userInfo()
if ns_dict is None:
data = None
else:
dumped = ns_dict['value']
app = getattr(App, '*app_instance')
data = app.serializer.loads(dumped)
try:
notification_function = getattr(notifications, '*notification_center')
except AttributeError: # notification center function not specified -> no error but warning in log
_log('WARNING: notification received but no function specified for answering it; use @notifications '
'decorator to register a function.')
else:
try:
data['activationType'] = notification.activationType()
data['actualDeliveryDate'] = notification.actualDeliveryDate()
_call_as_function_or_method(notification_function, data)
except Exception:
_log(traceback.format_exc())
def initializeStatusBar(self): def initializeStatusBar(self):
self.nsstatusitem = NSStatusBar.systemStatusBar().statusItemWithLength_(-1) # variable dimensions self.nsstatusitem = NSStatusBar.systemStatusBar().statusItemWithLength_(-1) # variable dimensions
@ -1113,14 +965,42 @@ class NSApp(NSObject):
if not (self.nsstatusitem.title() or self.nsstatusitem.image()): if not (self.nsstatusitem.title() or self.nsstatusitem.image()):
self.nsstatusitem.setTitle_(self._app['_name']) self.nsstatusitem.setTitle_(self._app['_name'])
def applicationDidFinishLaunching_(self, notification):
workspace = NSWorkspace.sharedWorkspace()
notificationCenter = workspace.notificationCenter()
notificationCenter.addObserver_selector_name_object_(
self,
self.receiveSleepNotification_,
NSWorkspaceWillSleepNotification,
None
)
notificationCenter.addObserver_selector_name_object_(
self,
self.receiveWakeNotification_,
NSWorkspaceDidWakeNotification,
None
)
def receiveSleepNotification_(self, ns_notification):
_log('receiveSleepNotification')
events.on_sleep.emit()
def receiveWakeNotification_(self, ns_notification):
_log('receiveWakeNotification')
events.on_wake.emit()
def applicationWillTerminate_(self, ns_notification):
_log('applicationWillTerminate')
events.before_quit.emit()
@classmethod @classmethod
def callback_(cls, nsmenuitem): def callback_(cls, nsmenuitem):
self, callback = cls._ns_to_py_and_callback[nsmenuitem] self, callback = cls._ns_to_py_and_callback[nsmenuitem]
_log(self) _log(self)
try: try:
return _call_as_function_or_method(callback, self) return _internal.call_as_function_or_method(callback, self)
except Exception: except Exception:
_log(traceback.format_exc()) traceback.print_exc()
class App(object): class App(object):
@ -1150,7 +1030,7 @@ class App(object):
serializer = pickle serializer = pickle
def __init__(self, name, title=None, icon=None, template=None, menu=None, quit_button='Quit'): def __init__(self, name, title=None, icon=None, template=None, menu=None, quit_button='Quit'):
_require_string(name) _internal.require_string(name)
self._name = name self._name = name
self._icon = self._icon_nsimage = self._title = None self._icon = self._icon_nsimage = self._title = None
self._template = template self._template = template
@ -1186,7 +1066,7 @@ class App(object):
@title.setter @title.setter
def title(self, title): def title(self, title):
_require_string_or_none(title) _internal.require_string_or_none(title)
self._title = title self._title = title
try: try:
self._nsapp.setStatusBarTitle() self._nsapp.setStatusBarTitle()
@ -1308,13 +1188,7 @@ class App(object):
self._nsapp = NSApp.alloc().init() self._nsapp = NSApp.alloc().init()
self._nsapp._app = self.__dict__ # allow for dynamic modification based on this App instance self._nsapp._app = self.__dict__ # allow for dynamic modification based on this App instance
nsapplication.setDelegate_(self._nsapp) nsapplication.setDelegate_(self._nsapp)
if _NOTIFICATIONS: notifications._init_nsapp(self._nsapp)
try:
notification_center = _default_user_notification_center()
except RuntimeError:
pass
else:
notification_center.setDelegate_(self._nsapp)
setattr(App, '*app_instance', self) # class level ref to running instance (for passing self to App subclasses) setattr(App, '*app_instance', self) # class level ref to running instance (for passing self to App subclasses)
t = b = None t = b = None
@ -1326,4 +1200,6 @@ class App(object):
self._nsapp.initializeStatusBar() self._nsapp.initializeStatusBar()
AppHelper.installMachInterrupt()
events.before_start.emit()
AppHelper.runEventLoop() AppHelper.runEventLoop()

32
lib/rumps/text_field.py Normal file
View file

@ -0,0 +1,32 @@
from AppKit import NSApplication, NSTextField, NSSecureTextField, NSKeyDown, NSCommandKeyMask
class Editing(NSTextField):
"""NSTextField with cut, copy, paste, undo and selectAll"""
def performKeyEquivalent_(self, event):
return _perform_key_equivalent(self, event)
class SecureEditing(NSSecureTextField):
"""NSSecureTextField with cut, copy, paste, undo and selectAll"""
def performKeyEquivalent_(self, event):
return _perform_key_equivalent(self, event)
def _perform_key_equivalent(self, event):
if event.type() == NSKeyDown and event.modifierFlags() & NSCommandKeyMask:
if event.charactersIgnoringModifiers() == "x":
NSApplication.sharedApplication().sendAction_to_from_("cut:", None, self)
return True
elif event.charactersIgnoringModifiers() == "c":
NSApplication.sharedApplication().sendAction_to_from_("copy:", None, self)
return True
elif event.charactersIgnoringModifiers() == "v":
NSApplication.sharedApplication().sendAction_to_from_("paste:", None, self)
return True
elif event.charactersIgnoringModifiers() == "z":
NSApplication.sharedApplication().sendAction_to_from_("undo:", None, self)
return True
elif event.charactersIgnoringModifiers() == "a":
NSApplication.sharedApplication().sendAction_to_from_("selectAll:", None, self)
return True

View file

@ -1,10 +1,15 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
#
# rumps: Ridiculously Uncomplicated macOS Python Statusbar apps. """
# Copyright: (c) 2017, Jared Suttles. All rights reserved. rumps.utils
# License: BSD, see LICENSE for details. ~~~~~~~~~~~
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Generic container classes and utility functions.
:copyright: (c) 2020 by Jared Suttles
:license: BSD-3-Clause, see LICENSE for details.
"""
from .packages.ordereddict import OrderedDict as _OrderedDict from .packages.ordereddict import OrderedDict as _OrderedDict

View file

@ -37,7 +37,7 @@ python-twitter==3.5
pytz==2022.6 pytz==2022.6
requests==2.28.1 requests==2.28.1
requests-oauthlib==1.3.1 requests-oauthlib==1.3.1
rumps==0.3.0; platform_system == "Darwin" rumps==0.4.0; platform_system == "Darwin"
simplejson==3.17.6 simplejson==3.17.6
six==1.16.0 six==1.16.0
soupsieve==2.3.2.post1 soupsieve==2.3.2.post1