diff --git a/src/calibre/devices/usbobserver/usbobserver.c b/src/calibre/devices/usbobserver/usbobserver.c index 801feb1804..395e511456 100644 --- a/src/calibre/devices/usbobserver/usbobserver.c +++ b/src/calibre/devices/usbobserver/usbobserver.c @@ -53,27 +53,6 @@ #define NUKE(x) Py_XDECREF(x); x = NULL; -/* This function only works on 10.5 and later. Pass in a unicode object as path */ -static PyObject* usbobserver_send2trash(PyObject *self, PyObject *args) -{ - UInt8 *utf8_chars; - FSRef fp; - OSStatus op_result; - - if (!PyArg_ParseTuple(args, "es", "utf-8", &utf8_chars)) { - return NULL; - } - - FSPathMakeRefWithOptions(utf8_chars, kFSPathMakeRefDoNotFollowLeafSymlink, &fp, NULL); - op_result = FSMoveObjectToTrashSync(&fp, NULL, kFSFileOperationDefaultOptions); - PyMem_Free(utf8_chars); - if (op_result != noErr) { - PyErr_SetString(PyExc_OSError, GetMacOSStatusCommentString(op_result)); - return NULL; - } - Py_RETURN_NONE; -} - static PyObject* usbobserver_get_iokit_string_property(io_service_t dev, CFStringRef prop) { @@ -469,9 +448,6 @@ static PyMethodDef usbobserver_methods[] = { {"get_mounted_filesystems", usbobserver_get_mounted_filesystems, METH_VARARGS, "Get mapping of mounted filesystems. Mapping is from BSD name to mount point." }, - {"send2trash", usbobserver_send2trash, METH_VARARGS, - "send2trash(unicode object) -> Send specified file/dir to trash" - }, {"user_locale", usbobserver_user_locale, METH_VARARGS, "user_locale() -> The name of the current user's locale or None if an error occurred" }, diff --git a/src/calibre/gui2/notify.py b/src/calibre/gui2/notify.py index 171daa2370..97308ba887 100644 --- a/src/calibre/gui2/notify.py +++ b/src/calibre/gui2/notify.py @@ -9,7 +9,7 @@ __docformat__ = 'restructuredtext en' import time from calibre import prints -from calibre.constants import islinux, isosx, get_osx_version, DEBUG, ispy3 +from calibre.constants import islinux, isosx, get_osx_version, DEBUG, plugins from polyglot.builtins import unicode_type @@ -132,32 +132,15 @@ class DummyNotifier(Notifier): class AppleNotifier(Notifier): def __init__(self): - self.ok = False - import os, sys - try: - self.exe = os.path.join(sys.console_binaries_path.replace( - 'console.app', 'calibre-notifier.app'), 'Calibre') - self.ok = os.access(self.exe, os.X_OK) - import subprocess - self.call = subprocess.Popen - except: - pass + self.cocoa, err = plugins['cocoa'] + self.ok = not err def notify(self, body, summary): - def encode(x): - if ispy3: - if isinstance(x, bytes): - x = x.decode('utf-8') - else: - if isinstance(x, unicode_type): - x = x.encode('utf-8') - return x - - cmd = [self.exe, '-activate', - 'net.kovidgoyal.calibre', '-message', encode(body)] if summary: - cmd += ['-title', encode(summary)] - self.call(cmd) + title, informative_text = summary, body + else: + title, informative_text = body, None + self.cocoa.send_notification(None, title, informative_text) def __call__(self, body, summary=None, replaces_id=None, timeout=0): timeout, body, summary = self.get_msg_parms(timeout, body, summary) diff --git a/src/calibre/utils/cocoa.m b/src/calibre/utils/cocoa.m index 86ce000304..7b7f098ef8 100644 --- a/src/calibre/utils/cocoa.m +++ b/src/calibre/utils/cocoa.m @@ -7,6 +7,80 @@ #include +#include + +const char* +cocoa_send2trash(const char *utf8_path) { + NSString *path = [[NSString alloc] initWithUTF8String:utf8_path]; + NSURL *url = [NSURL fileURLWithPath:path]; + const char *ret = NULL; + NSError* ns_error = nil; + if (![[NSFileManager defaultManager] trashItemAtURL:url resultingItemURL:nil error:&ns_error]) { + ret = strdup([[ns_error localizedDescription] UTF8String]); + } + [url release]; + [path release]; + return ret; +} + + +extern void macos_notification_callback(const char*); + +@interface NotificationDelegate : NSObject +@end + + +void +cocoa_send_notification(const char *identifier, const char *title, const char *subtitle, const char *informativeText, const char* path_to_image) { + NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter]; + if (!center) {return;} + if (!center.delegate) center.delegate = [[NotificationDelegate alloc] init]; + NSUserNotification *n = [NSUserNotification new]; + NSImage *img = nil; + if (path_to_image) { + NSString *p = [NSString stringWithUTF8String:path_to_image]; + NSURL *url = [NSURL fileURLWithPath:p]; + img = [[NSImage alloc] initWithContentsOfURL:url]; + [url release]; [p release]; + if (img) { + [n setValue:img forKey:@"_identityImage"]; + [n setValue:@(false) forKey:@"_identityImageHasBorder"]; + } + [img release]; + } +#define SET(x) { \ + if (x) { \ + NSString *t = [NSString stringWithUTF8String:x]; \ + n.x = t; \ + [t release]; \ + }} + SET(title); SET(subtitle); SET(informativeText); +#undef SET + if (identifier) { + n.userInfo = @{@"user_id": [NSString stringWithUTF8String:identifier]}; + } + [center deliverNotification:n]; + +} + +@implementation NotificationDelegate + - (void)userNotificationCenter:(NSUserNotificationCenter *)center + didDeliverNotification:(NSUserNotification *)notification { + (void)(center); (void)(notification); + } + + - (BOOL) userNotificationCenter:(NSUserNotificationCenter *)center + shouldPresentNotification:(NSUserNotification *)notification { + (void)(center); (void)(notification); + return YES; + } + + - (void) userNotificationCenter:(NSUserNotificationCenter *)center + didActivateNotification:(NSUserNotification *)notification { + (void)(center); + macos_notification_callback(notification.userInfo[@"user_id"] ? [notification.userInfo[@"user_id"] UTF8String] : NULL); + } +@end double diff --git a/src/calibre/utils/cocoa_wrapper.c b/src/calibre/utils/cocoa_wrapper.c index 1d65c4ecb5..b16242a902 100644 --- a/src/calibre/utils/cocoa_wrapper.c +++ b/src/calibre/utils/cocoa_wrapper.c @@ -8,6 +8,10 @@ #include extern double cocoa_cursor_blink_time(void); +extern void cocoa_send_notification(const char *identitifer, const char *title, const char *subtitle, const char *informativeText, const char* path_to_image); +extern const char* cocoa_send2trash(const char *utf8_path); + +static PyObject *notification_activated_callback = NULL; static PyObject* cursor_blink_time(PyObject *self) { @@ -16,8 +20,54 @@ cursor_blink_time(PyObject *self) { return PyFloat_FromDouble(ans); } +void +macos_notification_callback(const char* user_id) { + if (notification_activated_callback) { + PyObject *ret = PyObject_CallFunction(notification_activated_callback, "z", user_id); + if (ret == NULL) PyErr_Print(); + else Py_DECREF(ret); + } +} + +static PyObject* +set_notification_activated_callback(PyObject *self, PyObject *callback) { + (void)self; + if (notification_activated_callback) Py_DECREF(notification_activated_callback); + notification_activated_callback = callback; + Py_INCREF(callback); + Py_RETURN_NONE; + +} + +static PyObject* +send_notification(PyObject *self, PyObject *args) { + (void)self; + char *identifier = NULL, *title = NULL, *subtitle = NULL, *informativeText = NULL, *path_to_image = NULL; + if (!PyArg_ParseTuple(args, "zsz|zz", &identifier, &title, &informativeText, &path_to_image, &subtitle)) return NULL; + cocoa_send_notification(identifier, title, subtitle, informativeText, path_to_image); + + Py_RETURN_NONE; +} + +static PyObject* +send2trash(PyObject *self, PyObject *args) { + (void)self; + char *path = NULL; + if (!PyArg_ParseTuple(args, "s", &path)) return NULL; + const char *err = cocoa_send2trash(path); + if (err) { + PyErr_SetString(PyExc_OSError, err); + free((void*)err); + return NULL; + } + Py_RETURN_NONE; +} + static PyMethodDef module_methods[] = { {"cursor_blink_time", (PyCFunction)cursor_blink_time, METH_NOARGS, ""}, + {"set_notification_activated_callback", (PyCFunction)set_notification_activated_callback, METH_O, ""}, + {"send_notification", (PyCFunction)send_notification, METH_VARARGS, ""}, + {"send2trash", (PyCFunction)send2trash, METH_VARARGS, ""}, {NULL, NULL, 0, NULL} /* Sentinel */ }; diff --git a/src/calibre/utils/recycle_bin.py b/src/calibre/utils/recycle_bin.py index 34a8e2bcc8..65cc46a49c 100644 --- a/src/calibre/utils/recycle_bin.py +++ b/src/calibre/utils/recycle_bin.py @@ -89,7 +89,7 @@ if iswindows: return delegate_recycle(path) elif isosx: - u = plugins['usbobserver'][0] + u = plugins['cocoa'][0] if hasattr(u, 'send2trash'): def osx_recycle(path): if isbytestring(path):