macOS: Add menu items to close the OS window and the current tab

Fixes #3246
This commit is contained in:
Kovid Goyal
2021-01-17 06:49:25 +05:30
parent 4c9bd368c6
commit aa63bf71cf
7 changed files with 80 additions and 39 deletions

View File

@@ -27,7 +27,7 @@ To update |kitty|, :doc:`follow the instructions <binary>`.
- Panel kitten: Allow setting WM_CLASS (:iss:`3233`) - Panel kitten: Allow setting WM_CLASS (:iss:`3233`)
- macOS: Add a menu item to close the OS window (:pull:`3240`) - macOS: Add menu items to close the OS window and the current tab (:pull:`3240`, :iss:`3246`)
0.19.3 [2020-12-19] 0.19.3 [2020-12-19]

View File

@@ -141,7 +141,7 @@ class Boss:
opts: Options, opts: Options,
args: CLIOptions, args: CLIOptions,
cached_values: Dict[str, Any], cached_values: Dict[str, Any],
new_os_window_trigger: Optional[SingleKey] global_shortcuts: Dict[str, SingleKey]
): ):
set_layout_options(opts) set_layout_options(opts)
self.clipboard_buffers: Dict[str, str] = {} self.clipboard_buffers: Dict[str, str] = {}
@@ -168,8 +168,8 @@ class Boss:
set_boss(self) set_boss(self)
self.opts, self.args = opts, args self.opts, self.args = opts, args
self.keymap = self.opts.keymap.copy() self.keymap = self.opts.keymap.copy()
if new_os_window_trigger is not None: for sc in global_shortcuts.values():
self.keymap.pop(new_os_window_trigger, None) self.keymap.pop(sc, None)
if is_macos: if is_macos:
from .fast_data_types import ( from .fast_data_types import (
cocoa_set_notification_activated_callback cocoa_set_notification_activated_callback

View File

@@ -968,6 +968,8 @@ process_global_state(void *data) {
if (cocoa_pending_actions) { if (cocoa_pending_actions) {
if (cocoa_pending_actions & PREFERENCES_WINDOW) { call_boss(edit_config_file, NULL); } if (cocoa_pending_actions & PREFERENCES_WINDOW) { call_boss(edit_config_file, NULL); }
if (cocoa_pending_actions & NEW_OS_WINDOW) { call_boss(new_os_window, NULL); } if (cocoa_pending_actions & NEW_OS_WINDOW) { call_boss(new_os_window, NULL); }
if (cocoa_pending_actions & CLOSE_OS_WINDOW) { call_boss(close_os_window, NULL); }
if (cocoa_pending_actions & CLOSE_TAB) { call_boss(close_tab, NULL); }
if (cocoa_pending_actions_wd) { if (cocoa_pending_actions_wd) {
if (cocoa_pending_actions & NEW_OS_WINDOW_WITH_WD) { call_boss(new_os_window_with_wd, "s", cocoa_pending_actions_wd); } if (cocoa_pending_actions & NEW_OS_WINDOW_WITH_WD) { call_boss(new_os_window_with_wd, "s", cocoa_pending_actions_wd); }
if (cocoa_pending_actions & NEW_TAB_WITH_WD) { call_boss(new_tab_with_wd, "s", cocoa_pending_actions_wd); } if (cocoa_pending_actions & NEW_TAB_WITH_WD) { call_boss(new_tab_with_wd, "s", cocoa_pending_actions_wd); }

View File

@@ -87,6 +87,16 @@ find_app_name(void) {
set_cocoa_pending_action(NEW_OS_WINDOW, NULL); set_cocoa_pending_action(NEW_OS_WINDOW, NULL);
} }
- (void) close_os_window:(id)sender {
(void)sender;
set_cocoa_pending_action(CLOSE_OS_WINDOW, NULL);
}
- (void)close_tab:(id)sender {
(void)sender;
set_cocoa_pending_action(CLOSE_TAB, NULL);
}
- (void)open_kitty_website_url:(id)sender { - (void)open_kitty_website_url:(id)sender {
(void)sender; (void)sender;
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"https://sw.kovidgoyal.net/kitty/"]]; [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"https://sw.kovidgoyal.net/kitty/"]];
@@ -106,18 +116,30 @@ find_app_name(void) {
@end @end
static char new_window_key[32] = {0}; typedef struct {
static NSEventModifierFlags new_window_mods = 0; char key[32];
NSEventModifierFlags mods;
} GlobalShortcut;
typedef struct {
GlobalShortcut new_os_window, close_os_window, close_tab;
} GlobalShortcuts;
static GlobalShortcuts global_shortcuts;
static PyObject* static PyObject*
cocoa_set_new_window_trigger(PyObject *self UNUSED, PyObject *args) { cocoa_set_global_shortcut(PyObject *self UNUSED, PyObject *args) {
int mods; int mods;
unsigned int key; unsigned int key;
if (!PyArg_ParseTuple(args, "iI", &mods, &key)) return NULL; const char *name;
int nwm; if (!PyArg_ParseTuple(args, "siI", &name, &mods, &key)) return NULL;
get_cocoa_key_equivalent(key, mods, new_window_key, sizeof(new_window_key), &nwm); GlobalShortcut *gs = NULL;
new_window_mods = nwm; if (strcmp(name, "new_os_window") == 0) gs = &global_shortcuts.new_os_window;
if (new_window_key[0]) Py_RETURN_TRUE; else if (strcmp(name, "close_os_window") == 0) gs = &global_shortcuts.close_os_window;
else if (strcmp(name, "close_tab") == 0) gs = &global_shortcuts.close_tab;
if (gs == NULL) { PyErr_SetString(PyExc_KeyError, "Unknown shortcut name"); return NULL; }
int cocoa_mods;
get_cocoa_key_equivalent(key, mods, gs->key, 32, &cocoa_mods);
gs->mods = cocoa_mods;
if (gs->key[0]) Py_RETURN_TRUE;
Py_RETURN_FALSE; Py_RETURN_FALSE;
} }
@@ -358,8 +380,8 @@ cocoa_create_global_menu(void) {
NSMenuItem* new_os_window_menu_item = NSMenuItem* new_os_window_menu_item =
[appMenu addItemWithTitle:@"New OS window" [appMenu addItemWithTitle:@"New OS window"
action:@selector(new_os_window:) action:@selector(new_os_window:)
keyEquivalent:@(new_window_key)]; keyEquivalent:@(global_shortcuts.new_os_window.key)];
[new_os_window_menu_item setKeyEquivalentModifierMask:new_window_mods]; [new_os_window_menu_item setKeyEquivalentModifierMask:global_shortcuts.new_os_window.mods];
[new_os_window_menu_item setTarget:global_menu_target]; [new_os_window_menu_item setTarget:global_menu_target];
@@ -396,10 +418,6 @@ cocoa_create_global_menu(void) {
NSMenu* windowMenu = [[NSMenu alloc] initWithTitle:@"Window"]; NSMenu* windowMenu = [[NSMenu alloc] initWithTitle:@"Window"];
[windowMenuItem setSubmenu:windowMenu]; [windowMenuItem setSubmenu:windowMenu];
[windowMenu addItemWithTitle:@"Close"
action:@selector(performClose:)
keyEquivalent:@"w"];
[windowMenu addItemWithTitle:@"Minimize" [windowMenu addItemWithTitle:@"Minimize"
action:@selector(performMiniaturize:) action:@selector(performMiniaturize:)
keyEquivalent:@"m"]; keyEquivalent:@"m"];
@@ -411,6 +429,20 @@ cocoa_create_global_menu(void) {
action:@selector(arrangeInFront:) action:@selector(arrangeInFront:)
keyEquivalent:@""]; keyEquivalent:@""];
[windowMenu addItem:[NSMenuItem separatorItem]];
NSMenuItem* close_tab_item =
[windowMenu addItemWithTitle:@"Close Tab"
action:@selector(close_tab:)
keyEquivalent:@(global_shortcuts.close_tab.key)];
[close_tab_item setKeyEquivalentModifierMask:global_shortcuts.close_tab.mods];
[close_tab_item setTarget:global_menu_target];
NSMenuItem* close_os_window_menu_item =
[windowMenu addItemWithTitle:@"Close OS Window"
action:@selector(close_os_window:)
keyEquivalent:@(global_shortcuts.close_os_window.key)];
[close_os_window_menu_item setKeyEquivalentModifierMask:global_shortcuts.close_os_window.mods];
[close_os_window_menu_item setTarget:global_menu_target];
[windowMenu addItem:[NSMenuItem separatorItem]]; [windowMenu addItem:[NSMenuItem separatorItem]];
[[windowMenu addItemWithTitle:@"Enter Full Screen" [[windowMenu addItemWithTitle:@"Enter Full Screen"
action:@selector(toggleFullScreen:) action:@selector(toggleFullScreen:)
@@ -627,7 +659,7 @@ cocoa_system_beep(void) {
static PyMethodDef module_methods[] = { static PyMethodDef module_methods[] = {
{"cocoa_get_lang", (PyCFunction)cocoa_get_lang, METH_NOARGS, ""}, {"cocoa_get_lang", (PyCFunction)cocoa_get_lang, METH_NOARGS, ""},
{"cocoa_set_new_window_trigger", (PyCFunction)cocoa_set_new_window_trigger, METH_VARARGS, ""}, {"cocoa_set_global_shortcut", (PyCFunction)cocoa_set_global_shortcut, METH_VARARGS, ""},
{"cocoa_send_notification", (PyCFunction)cocoa_send_notification, METH_VARARGS, ""}, {"cocoa_send_notification", (PyCFunction)cocoa_send_notification, METH_VARARGS, ""},
{"cocoa_set_notification_activated_callback", (PyCFunction)set_notification_activated_callback, METH_O, ""}, {"cocoa_set_notification_activated_callback", (PyCFunction)set_notification_activated_callback, METH_O, ""},
{NULL, NULL, 0, NULL} /* Sentinel */ {NULL, NULL, 0, NULL} /* Sentinel */
@@ -635,6 +667,7 @@ static PyMethodDef module_methods[] = {
bool bool
init_cocoa(PyObject *module) { init_cocoa(PyObject *module) {
memset(&global_shortcuts, 0, sizeof(global_shortcuts));
if (PyModule_AddFunctions(module, module_methods) != 0) return false; if (PyModule_AddFunctions(module, module_methods) != 0) return false;
if (Py_AtExit(cleanup) != 0) { if (Py_AtExit(cleanup) != 0) {
PyErr_SetString(PyExc_RuntimeError, "Failed to register the cocoa_window at exit handler"); PyErr_SetString(PyExc_RuntimeError, "Failed to register the cocoa_window at exit handler");

View File

@@ -640,7 +640,7 @@ def cocoa_set_notification_activated_callback(identifier: Callable[[str], None])
pass pass
def cocoa_set_new_window_trigger(mods: int, key: int) -> bool: def cocoa_set_global_shortcut(name: str, mods: int, key: int) -> bool:
pass pass

View File

@@ -7,7 +7,7 @@ import os
import shutil import shutil
import sys import sys
from contextlib import contextmanager, suppress from contextlib import contextmanager, suppress
from typing import Generator, List, Mapping, Optional, Sequence from typing import Dict, Generator, List, Mapping, Optional, Sequence
from .borders import load_borders_program from .borders import load_borders_program
from .boss import Boss from .boss import Boss
@@ -104,26 +104,30 @@ def init_glfw(opts: OptionsStub, debug_keyboard: bool = False) -> str:
return glfw_module return glfw_module
def get_new_os_window_trigger(opts: OptionsStub) -> Optional[SingleKey]: def get_macos_shortcut_for(opts: OptionsStub, function: str = 'new_os_window') -> Optional[SingleKey]:
new_os_window_trigger = None ans = None
if is_macos: candidates = []
new_os_window_shortcuts = [] for k, v in opts.keymap.items():
for k, v in opts.keymap.items(): if v.func == function:
if v.func == 'new_os_window': candidates.append(k)
new_os_window_shortcuts.append(k) if candidates:
if new_os_window_shortcuts: from .fast_data_types import cocoa_set_global_shortcut
from .fast_data_types import cocoa_set_new_window_trigger
# Reverse list so that later defined keyboard shortcuts take priority over earlier defined ones # Reverse list so that later defined keyboard shortcuts take priority over earlier defined ones
for candidate in reversed(new_os_window_shortcuts): for candidate in reversed(candidates):
if cocoa_set_new_window_trigger(candidate[0], candidate[2]): if cocoa_set_global_shortcut(function, candidate[0], candidate[2]):
new_os_window_trigger = candidate ans = candidate
break break
return new_os_window_trigger return ans
def _run_app(opts: OptionsStub, args: CLIOptions, bad_lines: Sequence[BadLine] = ()) -> None: def _run_app(opts: OptionsStub, args: CLIOptions, bad_lines: Sequence[BadLine] = ()) -> None:
new_os_window_trigger = get_new_os_window_trigger(opts) global_shortcuts: Dict[str, SingleKey] = {}
if is_macos:
for ac in ('new_os_window', 'close_os_window', 'close_tab'):
val = get_macos_shortcut_for(opts, ac)
if val is not None:
global_shortcuts[ac] = val
if is_macos and opts.macos_custom_beam_cursor: if is_macos and opts.macos_custom_beam_cursor:
set_custom_ibeam_cursor() set_custom_ibeam_cursor()
if not is_wayland() and not is_macos: # no window icons on wayland if not is_wayland() and not is_macos: # no window icons on wayland
@@ -137,7 +141,7 @@ def _run_app(opts: OptionsStub, args: CLIOptions, bad_lines: Sequence[BadLine] =
pre_show_callback, pre_show_callback,
args.title or appname, args.name or args.cls or appname, args.title or appname, args.name or args.cls or appname,
args.cls or appname, load_all_shaders) args.cls or appname, load_all_shaders)
boss = Boss(opts, args, cached_values, new_os_window_trigger) boss = Boss(opts, args, cached_values, global_shortcuts)
boss.start(window_id) boss.start(window_id)
if bad_lines: if bad_lines:
boss.show_bad_config_lines(bad_lines) boss.show_bad_config_lines(bad_lines)

View File

@@ -263,7 +263,9 @@ typedef enum {
PREFERENCES_WINDOW = 1, PREFERENCES_WINDOW = 1,
NEW_OS_WINDOW = 2, NEW_OS_WINDOW = 2,
NEW_OS_WINDOW_WITH_WD = 4, NEW_OS_WINDOW_WITH_WD = 4,
NEW_TAB_WITH_WD = 8 NEW_TAB_WITH_WD = 8,
CLOSE_OS_WINDOW = 16,
CLOSE_TAB = 32
} CocoaPendingAction; } CocoaPendingAction;
void set_cocoa_pending_action(CocoaPendingAction action, const char*); void set_cocoa_pending_action(CocoaPendingAction action, const char*);
#endif #endif