From 59eb760a80568b8e22661572fca06505761496fb Mon Sep 17 00:00:00 2001 From: Yee Cheng Chin Date: Sun, 30 Oct 2022 23:13:58 -0700 Subject: [PATCH] Add zoomLeft: / zoomRight: / macOS 13 Stage Manager actions New zoomLeft/Right actions let the user easily pin the MacVim window to the left/right of the screen using the private APIs _zoomLeft: / _zoomRight:. This is similar to Windows' Aero Snap mode, and the functionality has been in macOS since 10.15. Unfortunately there isn't a public API for calling this. Note that this could already be done if the user went to the Keyboard settings and added a shortcut key to the Window -> "Move Window to Right Side of Screen" menu, but I doubt a lot of people do that, and it's nicer to have script-level control of this. The other likely option users would have adopted is to use a third-party tool. This change mostly makes it possible to easily snap the windows without needing to use them. Also, add new macOS 13 Ventura hooks for interfacing with Stage Manager. Expose the `_removeWindowFromStageManagerSet:`, which is the private API behind the new "Remove Window from Set" menu item, which removes the window from a mixed application set in Stage Manager. Similar to _zoomLeft:, this is a UI-only feature, and hence no public API is avaialble. Also, expose the collections API so that we can call join/unjoinAllStageManagerSets: to have MacVim windows float among all Stage Manager sets, which is useful for windows that need to show up next to other windows (e.g. a copy-and-paste scratchpad file). Also add a new separator to the Window menu because macOS injects all the window/Stage Manager related menu items right after the "Zoom" item, and it kind of expects a separator to be right after it for the new items to be categorized right. --- runtime/doc/gui_mac.txt | 37 ++++++++++++--------- runtime/doc/tags | 1 + runtime/menu.vim | 5 +-- src/MacVim/Actions.plist | 10 ++++++ src/MacVim/MMWindowController.h | 8 +++-- src/MacVim/MMWindowController.m | 57 +++++++++++++++++++++++++++++++++ 6 files changed, 98 insertions(+), 20 deletions(-) diff --git a/runtime/doc/gui_mac.txt b/runtime/doc/gui_mac.txt index 79552e4e1d..b8f8c9728f 100644 --- a/runtime/doc/gui_mac.txt +++ b/runtime/doc/gui_mac.txt @@ -446,13 +446,13 @@ that is the action that instructs them to paste something.) Menus are configured using the |:macmenu| command and the |:macaction| command can be used to send action messages. - *:maca* *:macaction* -:maca[ction] {action:} Send the message "action:" to the first responder. - The list of allowed actions can be seen by typing + *E9001-M* *:maca* *:macaction* +:maca[ction] {action:} Send the message "action:" to the first responder in + MacVim. The list of allowed actions can be seen by + typing > :maca - An attempt to send an action not listed here will - result in an error. This list is specified in a - property list file called |Actions.plist|. +< An attempt to send an action not listed here will + result in an error. See |macvim-actions| below. *:macm* *:macmenu* :macm[enu] {menu} {key}={arg} ... @@ -466,10 +466,10 @@ can be used to send action messages. For convenience, a menu with "action=name:" which is bound to will act as if bound to - ":maca name:". Thus, if "Menu.Item" is given by + ":maca name:". Thus, if "Menu.Item" is given by > :an Menu.Item :macm Menu.Item action=name: - then ":emenu Menu.Item" is equivalent to +< then ":emenu Menu.Item" is equivalent to ":maca name:". The key equivalent is specified with the @@ -513,18 +513,17 @@ file for more examples on how to set up menus. Note: When no window is open a minimal default menu is used. The default menu is set up in MainMenu.nib which resides in "Resources/English.lproj/" folder inside the app bundle. - *E9001-M* *Actions.plist* -Some action messages would not be suitable to call from within Vim, so there -is a dictionary called "Actions.plist" (in the Resources folder of the -application bundle) which contains all actions that may be called. The key in -this dictionary is the name of the action message (case sensitive), the value -is not used. + *Actions.plist* *macvim-actions* +Some actions (e.g. changing the font size) are not directly Vim related, and +are handled by MacVim on the application level for the GUI. MacVim allows +these actions to be invoked by either directly calling |:macaction| or binding +to a menu via |:macmenu|. The full list of actions can be found in the file +"Actions.plist" (under MacVim.app/Contents/Resources/), but below are the +common ones which might be useful. Hint: The |:macaction| command supports command-line completion so you can enter ":maca" to see a list of all available actions. -Here are some of the actions from Actions.plist which might be useful. - Action Description ~ fileOpen: Show "File Open" dialog findNext: Search forward using the "Find Pasteboard" @@ -543,8 +542,14 @@ performMiniaturize: Minimize window to the dock performZoom: Zoom window (same as clicking the green blob) terminate: Quit MacVim zoomAll: Zoom all windows +zoomLeft: Pin the window to the left of the screen +zoomRight: Pin the window to the right of the screen _cycleWindows: Select next window (similar to ) _cycleWindowsBackwards: Select previous window (similar to ) +_removeWindowFromStageManagerSet Remove window from a Stage Manager Set. Same + as the "Remove Window from Set" menu item. +joinAllStageManagerSets Window will float among all Stage Manager sets +unjoinAllStageManagerSets Window will only show up in its own set ============================================================================== 7. Toolbar *macvim-toolbar* diff --git a/runtime/doc/tags b/runtime/doc/tags index 35c3af79b0..79533dbe6c 100644 --- a/runtime/doc/tags +++ b/runtime/doc/tags @@ -8371,6 +8371,7 @@ macintosh os_mac.txt /*macintosh* macro map.txt /*macro* macvim gui_mac.txt /*macvim* macvim-PATH gui_mac.txt /*macvim-PATH* +macvim-actions gui_mac.txt /*macvim-actions* macvim-appearance gui_mac.txt /*macvim-appearance* macvim-appearance-mode gui_mac.txt /*macvim-appearance-mode* macvim-autocommands gui_mac.txt /*macvim-autocommands* diff --git a/runtime/menu.vim b/runtime/menu.vim index 9367e4f8c1..27197f51e8 100644 --- a/runtime/menu.vim +++ b/runtime/menu.vim @@ -81,15 +81,16 @@ if has("gui_macvim") an 9998.301 Window.Minimize\ All an 9998.310 Window.Zoom an 9998.311 Window.Zoom\ All + an 9998.318 Window.-SEP1- an 9998.320 Window.Toggle\ Full\ Screen\ Mode :set invfullscreen tln 9998.320 Window.Toggle\ Full\ Screen\ Mode :set invfullscreen - an 9998.330 Window.-SEP1- + an 9998.330 Window.-SEP2- " TODO! Grey out if no tabs are visible. an 9998.340 Window.Show\ Next\ Tab :tabnext tln 9998.340 Window.Show\ Next\ Tab :tabnext an 9998.350 Window.Show\ Previous\ Tab :tabprevious tln 9998.350 Window.Show\ Previous\ Tab :tabprevious - an 9998.360 Window.-SEP2- + an 9998.360 Window.-SEP3- an 9998.370 Window.Bring\ All\ To\ Front an 9998.380 Window.Stay\ in\ Front an 9998.390 Window.Stay\ in\ Back diff --git a/src/MacVim/Actions.plist b/src/MacVim/Actions.plist index 6db599bc62..2415e6b16f 100644 --- a/src/MacVim/Actions.plist +++ b/src/MacVim/Actions.plist @@ -74,11 +74,21 @@ zoomAll: + zoomLeft: + + zoomRight: + stayInFront: stayInBack: stayLevelNormal: + _removeWindowFromStageManagerSet: + + joinAllStageManagerSets: + + unjoinAllStageManagerSets: + diff --git a/src/MacVim/MMWindowController.h b/src/MacVim/MMWindowController.h index e1e83176e2..6bae3ff145 100644 --- a/src/MacVim/MMWindowController.h +++ b/src/MacVim/MMWindowController.h @@ -35,8 +35,8 @@ int updateToolbarFlag; BOOL keepOnScreen; NSString *windowAutosaveKey; - BOOL fullScreenEnabled; - MMFullScreenWindow *fullScreenWindow; + BOOL fullScreenEnabled; ///< Whether full screen is on (native or not) + MMFullScreenWindow *fullScreenWindow; ///< The window used for non-native full screen. Will only be non-nil when in non-native full screen. int fullScreenOptions; BOOL delayEnterFullScreen; NSRect preFullScreenFrame; @@ -127,5 +127,9 @@ - (IBAction)fontSizeDown:(id)sender; - (IBAction)findAndReplace:(id)sender; - (IBAction)zoom:(id)sender; +- (IBAction)zoomLeft:(id)sender; +- (IBAction)zoomRight:(id)sender; +- (IBAction)joinAllStageManagerSets:(id)sender; +- (IBAction)unjoinAllStageManagerSets:(id)sender; @end diff --git a/src/MacVim/MMWindowController.m b/src/MacVim/MMWindowController.m index 8496e8d522..000f057243 100644 --- a/src/MacVim/MMWindowController.m +++ b/src/MacVim/MMWindowController.m @@ -1351,7 +1351,64 @@ - (IBAction)zoom:(id)sender [vimController sendMessage:ZoomMsgID data:data]; } +/// Pin the window to the left of the screen. +/// +/// @note We expose this as a method instead of just having Actions.plist +/// expose NSWindow's private API `_zoomLeft` because it's a little nicer this +/// way instead of having to confuse the user with the underscore, and also so +/// that we can block this while full screen is active. +- (IBAction)zoomLeft:(id)sender +{ + if (fullScreenEnabled) + return; + + // macOS (as of 13.0) doesn't currently have an API to do "zoom left/right" + // aka Aero Snap in Windows, even though macOS 10.15 added UI to do so if + // you hover over the full screen button with Option key pressed. Because + // of that, we have to cheat a little bit and use private APIs + // (_zoomLeft/_zoomRight) which seems to work just fine. We also + // future-proof by detecting if this API gets graduated to public API + // (without the "_") and call that if that exists. + if ([decoratedWindow respondsToSelector:@selector(zoomLeft:)]) { + [decoratedWindow performSelector:@selector(zoomLeft:) withObject:sender]; + } else if ([decoratedWindow respondsToSelector:@selector(_zoomLeft:)]) { + [decoratedWindow performSelector:@selector(_zoomLeft:) withObject:sender]; + } +} + +/// Pin the window to the right of the screen. See zoomLeft: for comments. +- (IBAction)zoomRight:(id)sender +{ + if (fullScreenEnabled) + return; + if ([decoratedWindow respondsToSelector:@selector(zoomRight:)]) { + [decoratedWindow performSelector:@selector(zoomRight:) withObject:sender]; + } else if ([decoratedWindow respondsToSelector:@selector(_zoomRight:)]) { + [decoratedWindow performSelector:@selector(_zoomRight:) withObject:sender]; + } +} + +/// Make this window join all app sets +- (IBAction)joinAllStageManagerSets:(id)sender +{ +#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_VERSION_13_0 + if (@available(macos 13.0, *)) { + [decoratedWindow setCollectionBehavior:NSWindowCollectionBehaviorCanJoinAllApplications]; + } +#endif +} + +/// Make this window only show up in its own set. This is the default, so calling +/// this is only necessary if joinAllStageManagerSets: was previousaly called. +- (IBAction)unjoinAllStageManagerSets:(id)sender +{ +#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_VERSION_13_0 + if (@available(macos 13.0, *)) { + [decoratedWindow setCollectionBehavior:NSWindowCollectionBehaviorPrimary]; + } +#endif +} // -- Services menu delegate -------------------------------------------------