Implement special symbol names on macOS

This commit is contained in:
Kovid Goyal
2024-07-31 07:28:11 +05:30
parent 2c92240f8d
commit 01eacef387
4 changed files with 73 additions and 46 deletions

View File

@@ -460,7 +460,7 @@ schedule_notification(const char *appname, const char *identifier, const char *t
UNUserNotificationCenter *center = get_notification_center_safely();
if (!center) return;
// Configure the notification's payload.
RAII_CoreFoundation(UNMutableNotificationContent, *content, [[UNMutableNotificationContent alloc] init]);
UNMutableNotificationContent *content = [[[UNMutableNotificationContent alloc] init] autorelease];
if (title) content.title = @(title);
if (body) content.body = @(body);
if (appname) content.threadIdentifier = @(appname);
@@ -1072,13 +1072,7 @@ cocoa_set_uncaught_exception_handler(void) {
}
static PyObject*
convert_image_to_png(NSImage *icon, unsigned image_size, const char *output_path) {
NSRect r = NSMakeRect(0, 0, image_size, image_size);
RAII_CoreFoundation(CGColorSpaceRef, colorSpace, CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB));
RAII_CoreFoundation(CGContextRef, cgContext, CGBitmapContextCreate(NULL, image_size, image_size, 8, 4*image_size, colorSpace, kCGBitmapByteOrderDefault|kCGImageAlphaPremultipliedLast));
NSGraphicsContext *context = [NSGraphicsContext graphicsContextWithCGContext:cgContext flipped:NO]; // autoreleased
RAII_CoreFoundation(CGImageRef, cg, [icon CGImageForProposedRect:&r context:context hints:nil]);
NSBitmapImageRep *rep = [[NSBitmapImageRep alloc] initWithCGImage:cg]; // autoreleased
convert_imagerep_to_png(NSBitmapImageRep *rep, const char *output_path) {
NSData *png = [rep representationUsingType:NSBitmapImageFileTypePNG properties:@{NSImageCompressionFactor: @1.0}]; // autoreleased
if (output_path) {
@@ -1091,38 +1085,69 @@ convert_image_to_png(NSImage *icon, unsigned image_size, const char *output_path
return PyBytes_FromStringAndSize(png.bytes, png.length);
}
static PyObject*
convert_image_to_png(NSImage *icon, unsigned image_size, const char *output_path) {
NSRect r = NSMakeRect(0, 0, image_size, image_size);
RAII_CoreFoundation(CGColorSpaceRef, colorSpace, CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB));
RAII_CoreFoundation(CGContextRef, cgContext, CGBitmapContextCreate(NULL, image_size, image_size, 8, 4*image_size, colorSpace, kCGBitmapByteOrderDefault|kCGImageAlphaPremultipliedLast));
NSGraphicsContext *context = [NSGraphicsContext graphicsContextWithCGContext:cgContext flipped:NO]; // autoreleased
CGImageRef cg = [icon CGImageForProposedRect:&r context:context hints:nil];
NSBitmapImageRep *rep = [[[NSBitmapImageRep alloc] initWithCGImage:cg] autorelease];
return convert_imagerep_to_png(rep, output_path);
}
static PyObject*
render_emoji(NSString *text, unsigned image_size, const char *output_path) {
NSFont *font = [NSFont fontWithName:@"AppleColorEmoji" size:12];
CTFontRef ctfont = (__bridge CTFontRef)(font);
CGFloat line_height = MAX(1, floor(CTFontGetAscent(ctfont) + CTFontGetDescent(ctfont) + MAX(0, CTFontGetLeading(ctfont)) + 0.5));
CGFloat pts_per_px = CTFontGetSize(ctfont) / line_height;
CGFloat desired_size = image_size * pts_per_px;
NSFont *final_font = [NSFont fontWithName:@"AppleColorEmoji" size:desired_size];
NSAttributedString *attr_string = [[[NSAttributedString alloc] initWithString:text attributes:@{NSFontAttributeName: final_font}] autorelease];
NSBitmapImageRep *bmp = [[[NSBitmapImageRep alloc] initWithBitmapDataPlanes:nil pixelsWide:image_size pixelsHigh:image_size bitsPerSample:8 samplesPerPixel:4 hasAlpha:YES isPlanar:NO colorSpaceName:NSDeviceRGBColorSpace bytesPerRow:0 bitsPerPixel:0] autorelease];
[NSGraphicsContext saveGraphicsState];
NSGraphicsContext *context = [NSGraphicsContext graphicsContextWithBitmapImageRep:bmp];
[NSGraphicsContext setCurrentContext:context];
[attr_string drawInRect:NSMakeRect(0, 0, image_size, image_size)];
[NSGraphicsContext restoreGraphicsState];
return convert_imagerep_to_png(bmp, output_path);
}
static PyObject*
bundle_image_as_png(PyObject *self UNUSED, PyObject *args, PyObject *kw) {@autoreleasepool {
const char *b, *output_path = NULL; int is_identifier = 0; unsigned image_size = 256;
static const char* kwlist[] = {"path_or_identifier", "output_path", "image_size", "is_identifier", NULL};
if (!PyArg_ParseTupleAndKeywords(args, kw, "s|sIp", (char**)kwlist, &b, &output_path, &image_size, &is_identifier)) return NULL;
NSWorkspace *workspace = [NSWorkspace sharedWorkspace]; // autoreleased
const char *b, *output_path = NULL; int image_type = 1; unsigned image_size = 256;
static const char* kwlist[] = {"path_or_identifier", "output_path", "image_size", "image_type", NULL};
if (!PyArg_ParseTupleAndKeywords(args, kw, "s|sIi", (char**)kwlist, &b, &output_path, &image_size, &image_type)) return NULL;
NSImage *icon = nil;
if (is_identifier) {
NSURL *url = [workspace URLForApplicationWithBundleIdentifier:@(b)]; // autoreleased
if (!url) {
PyErr_Format(PyExc_KeyError, "Failed to find bundle path for identifier: %s", b); return NULL;
}
icon = [workspace iconForFile:@(url.fileSystemRepresentation)];
} else icon = [workspace iconForFile:@(b)];
switch (image_type) {
case 0: case 1: {
NSWorkspace *workspace = [NSWorkspace sharedWorkspace]; // autoreleased
if (image_type == 1) {
NSURL *url = [workspace URLForApplicationWithBundleIdentifier:@(b)]; // autoreleased
if (!url) {
PyErr_Format(PyExc_KeyError, "Failed to find bundle path for identifier: %s", b); return NULL;
}
icon = [workspace iconForFile:@(url.fileSystemRepresentation)];
} else icon = [workspace iconForFile:@(b)];
} break;
case 2:
return render_emoji(@(b), image_size, output_path);
default:
if (@available(macOS 11.0, *)) {
icon = [NSImage imageWithSystemSymbolName:@(b) accessibilityDescription:@""]; // autoreleased
} else {
PyErr_SetString(PyExc_ValueError, "Your version of macOS is too old to use symbol images, need >= 11.0"); return NULL;
}
break;
}
if (!icon) {
PyErr_Format(PyExc_ValueError, "Failed to load icon for bundle: %s", b); return NULL;
}
return convert_image_to_png(icon, image_size, output_path);
}}
static PyObject*
symbol_image_as_png(PyObject *self UNUSED, PyObject *args, PyObject *kw) {@autoreleasepool {
const char *b, *output_path = NULL; unsigned image_size = 256;
static const char* kwlist[] = {"symbol", "output_path", "image_size", NULL};
if (!PyArg_ParseTupleAndKeywords(args, kw, "s|sI", (char**)kwlist, &b, &output_path, &image_size)) return NULL;
NSImage *img = [NSImage imageWithSystemSymbolName:@(b) accessibilityDescription:@""]; // autoreleased
if (!img) {
PyErr_Format(PyExc_KeyError, "Failed to find image for symbol name: %s", b); return NULL;
}
return convert_image_to_png(img, image_size, output_path);
}}
static PyMethodDef module_methods[] = {
{"cocoa_get_lang", (PyCFunction)cocoa_get_lang, METH_NOARGS, ""},
{"cocoa_set_global_shortcut", (PyCFunction)cocoa_set_global_shortcut, METH_VARARGS, ""},
@@ -1134,7 +1159,6 @@ static PyMethodDef module_methods[] = {
{"cocoa_set_app_icon", (PyCFunction)cocoa_set_app_icon, METH_VARARGS, ""},
{"cocoa_set_dock_icon", (PyCFunction)cocoa_set_dock_icon, METH_VARARGS, ""},
{"cocoa_bundle_image_as_png", (PyCFunction)(void(*)(void))bundle_image_as_png, METH_VARARGS | METH_KEYWORDS, ""},
{"cocoa_symbol_image_as_png", (PyCFunction)(void(*)(void))symbol_image_as_png, METH_VARARGS | METH_KEYWORDS, ""},
{NULL, NULL, 0, NULL} /* Sentinel */
};

View File

@@ -190,16 +190,16 @@ ssh_control_master_template = 'kssh-{kitty_pid}-{ssh_placeholder}'
# See https://specifications.freedesktop.org/icon-naming-spec/latest/ar01s04.html
standard_icon_names = {
'error': 'dialog-error',
'warning': 'dialog-warning',
'warn': 'dialog-warning',
'info': 'dialog-information',
'question': 'dialog-question',
'error': ('dialog-error', ''),
'warning': ('dialog-warning',''),
'warn': ('dialog-warning', ''),
'info': ('dialog-information', ''),
'question': ('dialog-question', ''),
'help': 'system-help',
'file-manager': 'system-file-manager',
'system-monitor': 'utilities-system-monitor',
'text-editor': 'utilities-text-editor',
'help': ('system-help', '📖'),
'file-manager': ('system-file-manager', '🗄'),
'system-monitor': ('utilities-system-monitor', '🎛'),
'text-editor': ('utilities-text-editor', '📄'),
}

View File

@@ -571,8 +571,7 @@ def cocoa_send_notification(
) -> None:
pass
def cocoa_bundle_image_as_png(path_or_identifier: str, output_path: str = '', image_size: int = 256, is_identifier: bool = False) -> bytes: ...
def cocoa_symbol_image_as_png(symbol_name: str, output_path: str = '', image_size: int = 256) -> bytes: ...
def cocoa_bundle_image_as_png(path_or_identifier: str, output_path: str = '', image_size: int = 256, image_type: int = 1) -> bytes: ...
def cocoa_remove_delivered_notification(identifier: str) -> bool: ...
def cocoa_live_delivered_notifications() -> bool: ...

View File

@@ -488,16 +488,20 @@ class MacOSIntegration(DesktopIntegration):
return close_succeeded
def get_icon_for_name(self, name: str) -> str:
from .fast_data_types import cocoa_bundle_image_as_png
if name in self.failed_icons:
return ''
image_type, image_name = 1, name
if sic := standard_icon_names.get(name):
image_name = sic[1]
image_type = 2
icd = self.notification_manager.icon_data_cache
icd_key = self.icd_key_prefix + name
ans = icd.get_icon(icd_key)
if ans:
return ans
from .fast_data_types import cocoa_bundle_image_as_png
try:
data = cocoa_bundle_image_as_png(name, is_identifier=True)
data = cocoa_bundle_image_as_png(image_name, image_type=image_type)
except Exception as err:
if debug_desktop_integration:
self.notification_manager.log(f'Failed to get icon for {name} with error: {err}')
@@ -624,7 +628,7 @@ class FreeDesktopIntegration(DesktopIntegration):
if nc.icon_names:
for name in nc.icon_names:
if sn := standard_icon_names.get(name):
app_icon = sn
app_icon = sn[0]
break
if icon_exists(name):
app_icon = name