From e3dd8063adb8d79c197afd08e8371cd98b0fccb7 Mon Sep 17 00:00:00 2001 From: Yee Cheng Chin Date: Sun, 30 Oct 2022 15:14:48 -0700 Subject: [PATCH] Add SF Symbol support for tool bar, Touch Bar, menu icons Can now specify SF Symbols for tool bar / Touch Bar icons. The API remains the same where we use the "icon=" syntax in Vim menus to specify the icon, and just passing in the symbol name (e.g. 'gear.circle'). Also extended this to system named images like 'NSAdvanced' (the old gear shaped image), as previously we only had a specical case for Touch Bar system named template images. When loading the icon, MacVim will automatically determine whether it's an SF Symbol, system named image, or a file. SF Symbols can also be customized to be of a particular symbol style, or have a variable number set, by using colon-delimted option strings. For example: `aqi.high:palette:variable-0.5` is a symbol that uses the palette style, set to 0.5 variable value. Menu items now also support icons, the same as tool bars. We still don't support specifying icons for a submenu (which has been an issue for Touch Bar) since the Vim menu API doesn't support a way to do so. Also add an ability to use a `:template` value to specify that an image file is a template image. This is important to fix a minor regression introduced in #1214 where every image loaded in were assumed to be template. Add documentation to make this clear. See `:help macvim-toolbar-icon`. Also see comment in #1105 which requested this feature --- runtime/doc/gui.txt | 3 + runtime/doc/gui_mac.txt | 64 ++++++++--- runtime/doc/tags | 1 + src/MacVim/MMPreferenceController.m | 2 +- src/MacVim/MMVimController.m | 169 +++++++++++++++++++++++++--- src/MacVim/MacVim.h | 6 + src/MacVim/gui_macvim.m | 66 +++++++---- 7 files changed, 261 insertions(+), 50 deletions(-) diff --git a/runtime/doc/gui.txt b/runtime/doc/gui.txt index 8a63bef9c6..86c496312c 100644 --- a/runtime/doc/gui.txt +++ b/runtime/doc/gui.txt @@ -771,6 +771,9 @@ level. Vim interprets the items in this menu as follows: A space in the file name must be escaped with a backslash. A menu priority must come _after_ the icon argument: > :amenu icon=foo 1.42 ToolBar.Foo :echo "42!" +< + In MacVim, the "icon=" argument can also be used to specify SF symbols or + system named images. See |macvim-toolbar-icon| for more details. 2) An item called 'BuiltIn##', where ## is a number, is taken as number ## of the built-in bitmaps available in Vim. Currently there are 31 numbered from 0 to 30 which cover most common editing operations |builtin-tools|. > diff --git a/runtime/doc/gui_mac.txt b/runtime/doc/gui_mac.txt index c259fdb340..79552e4e1d 100644 --- a/runtime/doc/gui_mac.txt +++ b/runtime/doc/gui_mac.txt @@ -420,6 +420,11 @@ Default Menus~ See |macvim-default-menus|. +Icons~ + +Unlike regular Vim, MacVim menus can be customized with an icon. Simply use +the "icon=" parameter similar to toolbar. See |macvim-toolbar-icon| for usage. + Customization~ Menus in macOS behave slightly different from other platforms. For that @@ -554,16 +559,40 @@ empty space which will shink or expand so that the items to the right of it are right-aligned. A space (flexspace) will be created for any toolbar item whose name begins with "-space" ("-flexspace") and ends with "-" -Toolbar icons should be tiff, png, icns, or heic, of dimension 32x32 or 24x24 -pixels. The larger size is used when 'tbis' is "medium" or "large", otherwise -the smaller size is used (which is the default). If the icon file only -contains one dimension then macOS will scale the icon to the appropriate -dimension if necessary. To avoid this, use a file format which supports -multiple resolutions (such as icns) and provide both 32x32 and 24x24 versions -of the icon. + *macvim-toolbar-icon* +In regular Vim, the "icon=" argument (see |toolbar-icon|) can be used to +specify an image file by file path. In MacVim, the argument could also be +used for specifying an SF symbol or a macOS system image. Simply use the SF +symbol name or the system image name and MacVim will use load them instead of +an image file. Below are examples for using an SF symbol "gearshape.2" and a +macOS system image named "NSAdvanced": > + :an icon=gearshape.2 ToolBar.Setting1 + :an icon=NSAdvanced ToolBar.Setting2 +Some SF symbols in macOS can be customized with different styles. You can do +so by using colon-delimited options (most of them require macOS 13 Ventura). +The available options are `monochrome`, `hierarchical`, `palette`, +`multicolor`, and `variable-0.5` (where 0.5 can be substituted with any number +between 0 and 1). Download Apple's SF Symbols app to find out what the symbol +names are and what styling options each one supports. Some examples below: > + :an icon=bolt.circle:hierarchical ToolBar.Bolt :echo '⚡️' + :an icon=cloud.sun.rain.fill:multicolor ToolBar.Cloud :echo '🌦️' + :an icon=homekit:variable-0.4:palette ToolBar.Home :echo '🏠' +< +If your icon image is a template image (meaning that it is a grayscale image +designed to be mapped to whatever foreground color is), you can add +`:template` to the end of an image name, which will mark it as a template +image to macOS: > + :an icon=/a/b/black-and-white.png:template ToolBar.Foo +< +Supported image formats depend on the version of macOS. Safe formats include +png, icns, ico. Later macOS versions also support heic and webp. -Note: Only a subset of the builtin toolbar items presently have icons. If no -icon can be found a warning triangle is displayed instead. +Toolbar icons should be of dimension 32x32 or 24x24 pixels. The larger size +is used when 'tbis' is "medium" or "large", otherwise the smaller size is used +(which is the default). If the icon file only contains one dimension then +macOS will scale the icon to the appropriate dimension if necessary. To avoid +this, use a file format which supports multiple resolutions (such as icns) and +provide both 32x32 and 24x24 versions of the icon. ============================================================================== 8. Touch Bar *macvim-touchbar* @@ -573,6 +602,9 @@ Touch Bar in MacVim is configurable, and works similar to the toolbar (see instead of "ToolBar": > :an TouchBar.Hello :echo "Hello" < +This feature only works on Mac devices that come with Touch Bars. On the ones +that don't, nothing will show up. + You can also create submenus. Due to macOS restrictions, submenus can only be one level deep: > :an TouchBar.Navigate.Next :next @@ -593,13 +625,14 @@ begin with "-flexspace" and ends with "-". *macvim-touchbar-icon* You can specify icons for Touch Bar buttons the same way for toolbar icons -(see |macvim-toolbar|). When a button has an icon, it won't show the menu +(see |macvim-toolbar-icon|). When a button has an icon, it won't show the menu name. Touch Bar icons should ideally be 36x36 pixels, and no larger than 44x44 pixels. > - :an icon=/home/foo/bar.png TouchBar.DoSomething :echo 'Do' -You can also use default template icons provided by Apple by using their -template names. An example: > + :an icon=/home/foo/bar.png TouchBar.DoThing :echo 'Do' +You can use any image for the icon, but macOS comes with a few default +template images designed for use with Touch Bar. Some examples: > :an icon=NSTouchBarListViewTemplate TouchBar.ShowList :ls + :an icon=NSTouchBarRefreshTemplate TouchBar.Refresh :e! < *macvim-touchbar-title* By default, the Touch Bar buttons will use the menu names as the title. If an @@ -614,10 +647,7 @@ the icon. Example: > You can also insert emojis by adding a character picker button (specified by using a name that begin wtih "-characterpicker" and ends with "-"): > :inoremenu TouchBar.-characterpicker- - -This feature only works on Mac devices that come with Touch Bars. On the ones -that don't, nothing will show up. - +< *macvim-touchbar-defaults* Here is a list of default Touch Bar buttons that MacVim sets up: diff --git a/runtime/doc/tags b/runtime/doc/tags index 06ddf5c301..35c3af79b0 100644 --- a/runtime/doc/tags +++ b/runtime/doc/tags @@ -8410,6 +8410,7 @@ macvim-start gui_mac.txt /*macvim-start* macvim-tablabel gui_mac.txt /*macvim-tablabel* macvim-todo gui_mac.txt /*macvim-todo* macvim-toolbar gui_mac.txt /*macvim-toolbar* +macvim-toolbar-icon gui_mac.txt /*macvim-toolbar-icon* macvim-touchbar gui_mac.txt /*macvim-touchbar* macvim-touchbar-characterpicker gui_mac.txt /*macvim-touchbar-characterpicker* macvim-touchbar-defaults gui_mac.txt /*macvim-touchbar-defaults* diff --git a/src/MacVim/MMPreferenceController.m b/src/MacVim/MMPreferenceController.m index 42dec49653..fcc590dd8e 100644 --- a/src/MacVim/MMPreferenceController.m +++ b/src/MacVim/MMPreferenceController.m @@ -58,7 +58,7 @@ - (IBAction)showWindow:(id)sender - (void)setupToolbar { -#if MAC_OS_X_VERSION_MAX_ALLOWED >= 110000 +#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_VERSION_11_0 if (@available(macos 11.0, *)) { // Use SF Symbols for versions of the OS that supports it to be more unified with OS appearance. [self addView:generalPreferences diff --git a/src/MacVim/MMVimController.m b/src/MacVim/MMVimController.m index de28ea1574..c9e1063cbd 100644 --- a/src/MacVim/MMVimController.m +++ b/src/MacVim/MMVimController.m @@ -125,6 +125,7 @@ - (void)addMenuItemWithDescriptor:(NSArray *)desc - (void)removeMenuItemWithDescriptor:(NSArray *)desc; - (void)enableMenuItemWithDescriptor:(NSArray *)desc state:(BOOL)on; - (void)updateMenuItemTooltipWithDescriptor:(NSArray *)desc tip:(NSString *)tip; +- (NSImage*)findToolbarIcon:(NSString*)icon; - (void)addToolbarItemToDictionaryWithLabel:(NSString *)title toolTip:(NSString *)tip icon:(NSString *)icon; - (void)addToolbarItemWithLabel:(NSString *)label @@ -1547,6 +1548,11 @@ - (void)addMenuItemWithDescriptor:(NSArray *)desc } [item setAlternate:isAlternate]; + NSImage *img = [self findToolbarIcon:icon]; + if (img) { + [item setImage: img]; + } + // The tag is used to indicate whether Vim thinks a menu item should be // enabled or disabled. By default Vim thinks menu items are enabled. [item setTag:1]; @@ -1700,6 +1706,154 @@ - (void)updateMenuItemTooltipWithDescriptor:(NSArray *)desc [[MMAppController sharedInstance] markMainMenuDirty:mainMenu]; } +/// Load an icon image for the provided name. This will try multiple things to find the best image that fits the name. +/// @param icon Can be an SF Symbol name (with colon-separated formatting strings), named system image, or just a file. +- (NSImage*)findToolbarIcon:(NSString*)icon +{ + if ([icon length] == 0) { + return nil; + } + NSImage *img = nil; + + // Detect whether this is explicitly specified to be a template image, via a ":template" configuration suffix. + BOOL template = NO; + if ([icon hasSuffix:@":template"]) { + icon = [icon substringToIndex:([icon length] - 9)]; + template = YES; + } + + // Attempt 1: Load an SF Symbol image. This is first try because it's what Apple is pushing for and also likely + // what our users are going to want to use. We also allows for customization of the symbol. +#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_VERSION_11_0 + if (@available(macos 11.0, *)) { + // All SF Symbol functionality were introduced in macOS 11.0. + NSString *sfSymbolName = icon; + + BOOL monochrome = NO, hierarchical = NO, palette = NO, multicolor = NO; + double variableValue = -1; + + if ([sfSymbolName rangeOfString:@":"].location != NSNotFound) { + // We support using colon-separated strings to customize the symbol. First item is the icon name itself. + NSArray *splitComponents = [sfSymbolName componentsSeparatedByString:@":"]; + sfSymbolName = splitComponents[0]; + + for (int i = 1, count = splitComponents.count; i < count; i++) { + NSString *component = splitComponents[i]; + if ([component isEqualToString:@"monochrome"]) { + monochrome = YES; + } else if ([component isEqualToString:@"hierarchical"]) { + hierarchical = YES; + } else if ([component isEqualToString:@"palette"]) { + palette = YES; + } else if ([component isEqualToString:@"multicolor"]) { + multicolor = YES; + } else if ([component hasPrefix:@"variable-"]) { + NSString *variableString = [component substringFromIndex:9]; + variableValue = [variableString floatValue]; + } + } + } + +#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_VERSION_13_0 + if (@available(macos 13.0, *)) { + if (variableValue >= 0.0 && variableValue <= 1.0) { + img = [NSImage imageWithSystemSymbolName:sfSymbolName variableValue:variableValue accessibilityDescription:nil]; + } + } +#endif // MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_VERSION_13_0 + + if (img == nil) { + img = [NSImage imageWithSystemSymbolName:sfSymbolName accessibilityDescription:nil]; + } + + // Apply style customization to the symbol. This feature was added in macOS 12. + if (img) { +#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_VERSION_12_0 + if (@available(macos 12.0, *)) { + NSImageSymbolConfiguration *config = nil; + if (monochrome) { +#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_VERSION_13_0 + if (@available(macos 13.0, *)) { + config = [NSImageSymbolConfiguration configurationPreferringMonochrome]; + } +#endif + } + if (hierarchical) { + NSImageSymbolConfiguration *config2; +#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_VERSION_13_0 + if (@available(macos 13.0, *)) + { + // This version is preferred as it seems to set the color up automatically and therefore will use the correct ones. + config2 = [NSImageSymbolConfiguration configurationPreferringHierarchical]; + } + else +#endif + { + // Just guess which color to use. AppKit doesn't really give you a color that you can pick so we just guess one. + config2 = [NSImageSymbolConfiguration configurationWithHierarchicalColor:NSColor.controlTextColor]; + } + if (config) { + config = [config configurationByApplyingConfiguration:config2]; + } else { + config = config2; + } + } + if (palette) { + // The palette colors aren't completely correct. It doesn't appear for there to be a good way to query the primary colors + // for Touch Bar, tool bar, etc, so we just use controlTextColor. It would be nice if Apple just provides a "Preferring" + // version of this API like the other ones. + NSImageSymbolConfiguration *config2 = [NSImageSymbolConfiguration configurationWithPaletteColors:@[NSColor.controlTextColor, NSColor.controlAccentColor]]; + if (config) { + config = [config configurationByApplyingConfiguration:config2]; + } else { + config = config2; + } + } + if (multicolor) { + NSImageSymbolConfiguration *config2 = [NSImageSymbolConfiguration configurationPreferringMulticolor]; + if (config) { + config = [config configurationByApplyingConfiguration:config2]; + } else { + config = config2; + } + } + + if (config) { + NSImage *img2 = [img imageWithSymbolConfiguration:config]; + if (img2) { + img = img2; + } + } + } +#endif // MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_VERSION_12_0 + + // Just mark them as used so compiling on older SDKs won't complain about unused variables. + (void)multicolor; + (void)hierarchical; + (void)palette; + (void)variableValue; + } + } +#endif // MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_VERSION_11_0 + + // Attempt 2: Load a named image. + if (!img) { + img = [NSImage imageNamed:icon]; + } + + // Attempt 3: Load from a file. + if (!img) { + img = [[[NSImage alloc] initByReferencingFile:icon] autorelease]; + if (!(img && [img isValid])) + img = nil; + } + + if (img && template) { + [img setTemplate:YES]; + } + return img; +} + - (void)addToolbarItemToDictionaryWithLabel:(NSString *)title toolTip:(NSString *)tip icon:(NSString *)icon @@ -1717,12 +1871,7 @@ - (void)addToolbarItemToDictionaryWithLabel:(NSString *)title [item setAction:@selector(vimToolbarItemAction:)]; [item setAutovalidates:NO]; - NSImage *img = [NSImage imageNamed:icon]; - if (!img) { - img = [[[NSImage alloc] initByReferencingFile:icon] autorelease]; - if (!(img && [img isValid])) - img = nil; - } + NSImage *img = [self findToolbarIcon:icon]; if (!img) { ASLogNotice(@"Could not find image with name '%@' to use as toolbar" " image for identifier '%@';" @@ -1732,7 +1881,6 @@ - (void)addToolbarItemToDictionaryWithLabel:(NSString *)title img = [NSImage imageNamed:MMDefaultToolbarImageName]; } - [img setTemplate:YES]; [item setImage:img]; [toolbarItemDict setObject:item forKey:title]; @@ -1809,13 +1957,8 @@ - (void)addTouchbarItemWithLabel:(NSString *)label [button setDesc:desc]; NSCustomTouchBarItem *item = [[[NSCustomTouchBarItem alloc] initWithIdentifier:label] autorelease]; - NSImage *img = [NSImage imageNamed:icon]; - if (!img) { - img = [[[NSImage alloc] initByReferencingFile:icon] autorelease]; - if (!(img && [img isValid])) - img = nil; - } + NSImage *img = [self findToolbarIcon:icon];; if (img) { [button setImage: img]; if (useTip) { diff --git a/src/MacVim/MacVim.h b/src/MacVim/MacVim.h index cd5174db87..48a14e04a8 100644 --- a/src/MacVim/MacVim.h +++ b/src/MacVim/MacVim.h @@ -43,9 +43,15 @@ #ifndef MAC_OS_X_VERSION_10_14 # define MAC_OS_X_VERSION_10_14 101400 #endif +#ifndef MAC_OS_VERSION_11_0 +# define MAC_OS_VERSION_11_0 110000 +#endif #ifndef MAC_OS_VERSION_12_0 # define MAC_OS_VERSION_12_0 120000 #endif +#ifndef MAC_OS_VERSION_13_0 +# define MAC_OS_VERSION_13_0 130000 +#endif #ifndef NSAppKitVersionNumber10_10 # define NSAppKitVersionNumber10_10 1343 diff --git a/src/MacVim/gui_macvim.m b/src/MacVim/gui_macvim.m index 8c675f58cb..ff665f7a35 100644 --- a/src/MacVim/gui_macvim.m +++ b/src/MacVim/gui_macvim.m @@ -749,6 +749,8 @@ } +// Look up the icon file. If it's a full path, return that. Otherwise, look for +// it under a 'bitmaps' folder under runtimepath, using common file extensions. // Taken from gui_gtk.c (slightly modified) static int lookup_menu_iconfile(char_u *iconfile, char_u *dest) @@ -758,7 +760,9 @@ if (mch_isFullName(dest)) return vim_fexists(dest); - static const char suffixes[][4] = {"png", "bmp"}; + // Just find the popular image formats that macOS supports. + static const char suffixes[][5] = { + "png", "bmp", "ico", "icns", "jpeg", "jpg", "heic", "webp"}; char_u buf[MAXPATHL]; unsigned int i; @@ -786,39 +790,63 @@ (unsigned short)specialKeyToNSKey(menu->mac_key)] : [NSString string]; int modifierMask = vimModMaskToEventModifierFlags(menu->mac_mods); - char_u *icon = NULL; + NSString *icon = nil; vimmenu_T *rootMenu = menu; while (rootMenu->parent) { rootMenu = rootMenu->parent; } if (menu_is_toolbar(rootMenu->name)) { - // - // Find out what file to load for the icon. This is only relevant for the - // toolbar and TouchBar. - // + // Find out what file to load for the toolbar icon. char_u fname[MAXPATHL]; - // Try to use the icon=.. argument + // Try to use the file path from the icon=.. argument if (menu->iconfile && lookup_menu_iconfile(menu->iconfile, fname)) - icon = fname; + icon = [NSString stringWithVimString:fname]; // If not found and not builtin specified try using the menu name - if (!icon && !menu->icon_builtin + if (icon == nil && !menu->icon_builtin && lookup_menu_iconfile(menu->name, fname)) - icon = fname; + icon = [NSString stringWithVimString:fname]; // Still no icon found, try using a builtin icon. (If this also fails, // then a warning icon will be displayed). - if (!icon) - icon = lookup_toolbar_item(menu->iconidx); - - // Last step is to see if this is a standard Apple template icon. The - // touch bar templates are of the form "NSTouchBar*Template". - if (!icon) - if (menu->iconfile && STRNCMP(menu->iconfile, "NSTouchBar", 10) == 0) { - icon = menu->iconfile; + if (icon == nil) { + char_u* toolbar_item = lookup_toolbar_item(menu->iconidx); + if (toolbar_item) { + icon = [NSString stringWithVimString:toolbar_item]; + + // All the default icons that MacVim ships with are templates + // to make them work better in light/dark modes. + icon = [icon stringByAppendingString:@":template"]; } + } + + // Last step is to simply pass the icon argument up the chain as there + // are more complicated logic to determine what this is (e.g. SF Symbol + // or raw image). + if (icon == nil) { + if (menu->iconfile && *menu->iconfile != '\0') { + icon = [NSString stringWithVimString:menu->iconfile]; + } + } + } else { + // For regular menus, we support icons as well, but only if it's + // specified by the icon=... argument. This is a MacVim-extension. + char_u fname[MAXPATHL]; + + if (menu->iconfile && *menu->iconfile != '\0') { + if (lookup_menu_iconfile(menu->iconfile, fname)) { + icon = [NSString stringWithVimString:fname]; + } else { + icon = [NSString stringWithVimString:menu->iconfile]; + } + } + } + + if (icon == nil) { + // Need non-nil items for dictionaryWithObjectsAndKeys: below. + icon = @""; } [[MMBackend sharedInstance] queueMessage:AddMenuItemMsgID properties: @@ -826,7 +854,7 @@ desc, @"descriptor", [NSNumber numberWithInt:idx], @"index", [NSString stringWithVimString:tip], @"tip", - [NSString stringWithVimString:icon], @"icon", + icon, @"icon", keyEquivalent, @"keyEquivalent", [NSNumber numberWithInt:modifierMask], @"modifierMask", [NSString stringWithVimString:menu->mac_action], @"action",