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",