mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-07-06 05:01:14 -07:00
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:
parent
b31d75aeee
commit
79cf61c53e
11 changed files with 573 additions and 243 deletions
|
@ -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
92
lib/rumps/_internal.py
Normal 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__
|
||||||
|
)
|
||||||
|
)
|
|
@ -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
40
lib/rumps/events.py
Normal 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
9
lib/rumps/exceptions.py
Normal 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."""
|
|
@ -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)
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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
32
lib/rumps/text_field.py
Normal 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
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue