macOS: Display the newly created OS window in specified state

Fix the maximized window can't occupy full screen space when window
decoration or title bar is hidden.
Fix resize_in_steps being applied even when window is maximized.
Allows to specify `os_window_state` in startup session file.
This commit is contained in:
pagedown 2023-02-18 14:02:19 +08:00
parent 1b76cee9b4
commit ba83ce7b10
No known key found for this signature in database
GPG Key ID: E921CF18AC8FF6EB
12 changed files with 109 additions and 33 deletions

View File

@ -56,6 +56,12 @@ Detailed list of changes
- launch: Allow specifying the state (fullscreen/maximized/minimized) for newly created OS Windows (:iss:`6026`) - launch: Allow specifying the state (fullscreen/maximized/minimized) for newly created OS Windows (:iss:`6026`)
- Sessions: Allow specifying the OS window state via the ``os_window_state`` directive (:iss:`5863`)
- macOS: Display the newly created OS window in specified state to avoid or reduce the window transition animations (:pull:`6035`)
- macOS: Fix the maximized window not taking up full space when the title bar is hidden or when :opt:`resize_in_steps` is configured (:iss:`6021`)
0.27.1 [2023-02-07] 0.27.1 [2023-02-07]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -161,6 +161,8 @@ option in :file:`kitty.conf`. An example, showing all available commands:
os_window_size 80c 24c os_window_size 80c 24c
# Set the --class for the new OS window # Set the --class for the new OS window
os_window_class mywindow os_window_class mywindow
# Change the OS window state to normal, fullscreen, maximized or minimized
os_window_state normal
launch sh launch sh
# Resize the current window (see the resize_window action for details) # Resize the current window (see the resize_window action for details)
resize_window wider 2 resize_window wider 2

View File

@ -1868,8 +1868,9 @@ int _glfwPlatformCreateWindow(_GLFWwindow* window,
if (window->monitor) if (window->monitor)
{ {
_glfwPlatformShowWindow(window); // Do not show the window here until after setting the window size, maximized state, and full screen
_glfwPlatformFocusWindow(window); // _glfwPlatformShowWindow(window);
// _glfwPlatformFocusWindow(window);
acquireMonitor(window); acquireMonitor(window);
} }
@ -2061,8 +2062,12 @@ void _glfwPlatformRestoreWindow(_GLFWwindow* window)
void _glfwPlatformMaximizeWindow(_GLFWwindow* window) void _glfwPlatformMaximizeWindow(_GLFWwindow* window)
{ {
if (![window->ns.object isZoomed]) if (![window->ns.object isZoomed]) {
const NSSize original = [window->ns.object resizeIncrements];
[window->ns.object setResizeIncrements:NSMakeSize(1.0, 1.0)];
[window->ns.object zoom:nil]; [window->ns.object zoom:nil];
[window->ns.object setResizeIncrements:original];
}
} }
void _glfwPlatformShowWindow(_GLFWwindow* window) void _glfwPlatformShowWindow(_GLFWwindow* window)

View File

@ -75,7 +75,6 @@ from .fast_data_types import (
apply_options_update, apply_options_update,
background_opacity_of, background_opacity_of,
change_background_opacity, change_background_opacity,
change_os_window_state,
cocoa_hide_app, cocoa_hide_app,
cocoa_hide_other_apps, cocoa_hide_other_apps,
cocoa_minimize_os_window, cocoa_minimize_os_window,
@ -137,6 +136,7 @@ from .utils import (
macos_version, macos_version,
open_url, open_url,
parse_address_spec, parse_address_spec,
parse_os_window_state,
parse_uri_list, parse_uri_list,
platform_window_id, platform_window_id,
remove_socket_file, remove_socket_file,
@ -382,12 +382,12 @@ class Boss:
focused_os_window = wid = 0 focused_os_window = wid = 0
token = os.environ.pop('XDG_ACTIVATION_TOKEN', '') token = os.environ.pop('XDG_ACTIVATION_TOKEN', '')
for startup_session in si: for startup_session in si:
wid = self.add_os_window(startup_session, os_window_id=os_window_id) # The window state from the CLI options will override and apply to every single OS window in startup session
wstate = self.args.start_as if self.args.start_as and self.args.start_as != 'normal' else None
wid = self.add_os_window(startup_session, window_state=wstate, os_window_id=os_window_id)
if startup_session.focus_os_window: if startup_session.focus_os_window:
focused_os_window = wid focused_os_window = wid
os_window_id = None os_window_id = None
if self.args.start_as != 'normal':
change_os_window_state(self.args.start_as, wid)
if focused_os_window > 0: if focused_os_window > 0:
focus_os_window(focused_os_window, True, token) focus_os_window(focused_os_window, True, token)
elif token and is_wayland() and wid: elif token and is_wayland() and wid:
@ -399,6 +399,7 @@ class Boss:
os_window_id: Optional[int] = None, os_window_id: Optional[int] = None,
wclass: Optional[str] = None, wclass: Optional[str] = None,
wname: Optional[str] = None, wname: Optional[str] = None,
window_state: Optional[str] = None,
opts_for_size: Optional[Options] = None, opts_for_size: Optional[Options] = None,
startup_id: Optional[str] = None, startup_id: Optional[str] = None,
override_title: Optional[str] = None, override_title: Optional[str] = None,
@ -408,11 +409,13 @@ class Boss:
wclass = wclass or getattr(startup_session, 'os_window_class', None) or self.args.cls or appname wclass = wclass or getattr(startup_session, 'os_window_class', None) or self.args.cls or appname
wname = wname or self.args.name or wclass wname = wname or self.args.name or wclass
wtitle = override_title or self.args.title wtitle = override_title or self.args.title
window_state = window_state or getattr(startup_session, 'os_window_state', None)
wstate = parse_os_window_state(window_state) if window_state is not None else None
with startup_notification_handler(do_notify=startup_id is not None, startup_id=startup_id) as pre_show_callback: with startup_notification_handler(do_notify=startup_id is not None, startup_id=startup_id) as pre_show_callback:
os_window_id = create_os_window( os_window_id = create_os_window(
initial_window_size_func(size_data, self.cached_values), initial_window_size_func(size_data, self.cached_values),
pre_show_callback, pre_show_callback,
wtitle or appname, wname, wclass, disallow_override_title=bool(wtitle)) wtitle or appname, wname, wclass, wstate, disallow_override_title=bool(wtitle))
else: else:
wname = self.args.name or self.args.cls or appname wname = self.args.name or self.args.cls or appname
wclass = self.args.cls or appname wclass = self.args.cls or appname

View File

@ -294,6 +294,10 @@ PRESS: int
RELEASE: int RELEASE: int
DRAG: int DRAG: int
MOVE: int MOVE: int
WINDOW_NORMAL: int = 0
WINDOW_FULLSCREEN: int
WINDOW_MAXIMIZED: int
WINDOW_MINIMIZED: int
# }}} # }}}
@ -508,10 +512,11 @@ def create_os_window(
title: str, title: str,
wm_class_name: str, wm_class_name: str,
wm_class_class: str, wm_class_class: str,
window_state: Optional[int] = WINDOW_NORMAL,
load_programs: Optional[Callable[[bool], None]] = None, load_programs: Optional[Callable[[bool], None]] = None,
x: int = -1, x: Optional[int] = -1,
y: int = -1, y: Optional[int] = -1,
disallow_override_title: bool = False, disallow_override_title: Optional[bool] = False,
) -> int: ) -> int:
pass pass
@ -818,7 +823,7 @@ def cocoa_set_menubar_title(title: str) -> None:
pass pass
def change_os_window_state(state: str, os_window_id: int = 0) -> None: def change_os_window_state(state: int, os_window_id: Optional[int] = 0) -> None:
pass pass

View File

@ -738,6 +738,25 @@ toggle_maximized_for_os_window(OSWindow *w) {
return maximized; return maximized;
} }
static void
change_state_for_os_window(OSWindow *w, int state) {
if (!w || !w->handle) return;
switch (state) {
case WINDOW_MAXIMIZED:
glfwMaximizeWindow(w->handle);
break;
case WINDOW_MINIMIZED:
glfwIconifyWindow(w->handle);
break;
case WINDOW_FULLSCREEN:
if (!is_os_window_fullscreen(w)) toggle_fullscreen_for_os_window(w);
break;
case WINDOW_NORMAL:
if (is_os_window_fullscreen(w)) toggle_fullscreen_for_os_window(w);
else glfwRestoreWindow(w->handle);
break;
}
}
#ifdef __APPLE__ #ifdef __APPLE__
static GLFWwindow *apple_preserve_common_context = NULL; static GLFWwindow *apple_preserve_common_context = NULL;
@ -792,12 +811,14 @@ native_window_handle(GLFWwindow *w) {
static PyObject* static PyObject*
create_os_window(PyObject UNUSED *self, PyObject *args, PyObject *kw) { create_os_window(PyObject UNUSED *self, PyObject *args, PyObject *kw) {
int x = -1, y = -1, disallow_override_title = 0; int x = -1, y = -1, window_state = WINDOW_NORMAL, disallow_override_title = 0;
char *title, *wm_class_class, *wm_class_name; char *title, *wm_class_class, *wm_class_name;
PyObject *load_programs = NULL, *get_window_size, *pre_show_callback; PyObject *optional_window_state = NULL, *load_programs = NULL, *get_window_size, *pre_show_callback;
static const char* kwlist[] = {"get_window_size", "pre_show_callback", "title", "wm_class_name", "wm_class_class", "load_programs", "x", "y", "disallow_override_title", NULL}; static const char* kwlist[] = {"get_window_size", "pre_show_callback", "title", "wm_class_name", "wm_class_class", "window_state", "load_programs", "x", "y", "disallow_override_title", NULL};
if (!PyArg_ParseTupleAndKeywords(args, kw, "OOsss|Oiip", (char**)kwlist, if (!PyArg_ParseTupleAndKeywords(args, kw, "OOsss|OOiip", (char**)kwlist,
&get_window_size, &pre_show_callback, &title, &wm_class_name, &wm_class_class, &load_programs, &x, &y, &disallow_override_title)) return NULL; &get_window_size, &pre_show_callback, &title, &wm_class_name, &wm_class_class, &optional_window_state, &load_programs, &x, &y, &disallow_override_title)) return NULL;
if (optional_window_state && optional_window_state != Py_None) window_state = (int) PyLong_AsLong(optional_window_state);
if (window_state < WINDOW_NORMAL || window_state > WINDOW_MINIMIZED) window_state = WINDOW_NORMAL;
static bool is_first_window = true; static bool is_first_window = true;
if (is_first_window) { if (is_first_window) {
@ -886,7 +907,9 @@ create_os_window(PyObject UNUSED *self, PyObject *args, PyObject *kw) {
if (pret == NULL) return NULL; if (pret == NULL) return NULL;
Py_DECREF(pret); Py_DECREF(pret);
if (x != -1 && y != -1) glfwSetWindowPos(glfw_window, x, y); if (x != -1 && y != -1) glfwSetWindowPos(glfw_window, x, y);
#ifndef __APPLE__
glfwShowWindow(glfw_window); glfwShowWindow(glfw_window);
#endif
#ifdef __APPLE__ #ifdef __APPLE__
float n_xscale, n_yscale; float n_xscale, n_yscale;
double n_xdpi, n_ydpi; double n_xdpi, n_ydpi;
@ -973,6 +996,14 @@ create_os_window(PyObject UNUSED *self, PyObject *args, PyObject *kw) {
warned = true; warned = true;
} }
} }
// Update window state
// We do not call glfwWindowHint to set GLFW_MAXIMIZED before the window is created.
// That would cause the window to be set to maximize immediately after creation and use the wrong initial size when restored.
change_state_for_os_window(w, window_state);
#ifdef __APPLE__
// macOS: Show the window after it is ready
glfwShowWindow(glfw_window);
#endif
return PyLong_FromUnsignedLongLong(w->id); return PyLong_FromUnsignedLongLong(w->id);
} }
@ -1393,19 +1424,16 @@ cocoa_minimize_os_window(PyObject UNUSED *self, PyObject *args) {
static PyObject* static PyObject*
change_os_window_state(PyObject *self UNUSED, PyObject *args) { change_os_window_state(PyObject *self UNUSED, PyObject *args) {
char *state; int state;
id_type wid = 0; id_type wid = 0;
if (!PyArg_ParseTuple(args, "s|K", &state, &wid)) return NULL; if (!PyArg_ParseTuple(args, "i|K", &state, &wid)) return NULL;
OSWindow *w = wid ? os_window_for_id(wid) : current_os_window(); OSWindow *w = wid ? os_window_for_id(wid) : current_os_window();
if (!w || !w->handle) Py_RETURN_NONE; if (!w || !w->handle) Py_RETURN_NONE;
if (strcmp(state, "maximized") == 0) glfwMaximizeWindow(w->handle); if (state < WINDOW_NORMAL || state > WINDOW_MINIMIZED) {
else if (strcmp(state, "minimized") == 0) glfwIconifyWindow(w->handle); PyErr_SetString(PyExc_ValueError, "Unknown window state");
else if (strcmp(state, "fullscreen") == 0 || strcmp(state, "fullscreened") == 0) { return NULL;
if (!is_os_window_fullscreen(w)) toggle_fullscreen_for_os_window(w); }
} else if (strcmp(state, "normal") == 0) { change_state_for_os_window(w, state);
if (is_os_window_fullscreen(w)) toggle_fullscreen_for_os_window(w);
else glfwRestoreWindow(w->handle);
} else { PyErr_SetString(PyExc_ValueError, "Unknown window state"); return NULL; }
Py_RETURN_NONE; Py_RETURN_NONE;
} }

View File

@ -13,7 +13,7 @@ from .cli import parse_args
from .cli_stub import LaunchCLIOptions from .cli_stub import LaunchCLIOptions
from .clipboard import set_clipboard_string, set_primary_selection from .clipboard import set_clipboard_string, set_primary_selection
from .constants import kitten_exe, shell_path from .constants import kitten_exe, shell_path
from .fast_data_types import add_timer, change_os_window_state, get_boss, get_options, get_os_window_title, patch_color_profiles from .fast_data_types import add_timer, get_boss, get_options, get_os_window_title, patch_color_profiles
from .options.utils import env as parse_env from .options.utils import env as parse_env
from .tabs import Tab, TabManager from .tabs import Tab, TabManager
from .types import OverlayType, run_once from .types import OverlayType, run_once
@ -330,9 +330,11 @@ def tab_for_window(boss: Boss, opts: LaunchCLIOptions, target_tab: Optional[Tab]
def create_tab(tm: Optional[TabManager] = None) -> Tab: def create_tab(tm: Optional[TabManager] = None) -> Tab:
if tm is None: if tm is None:
oswid = boss.add_os_window(wclass=opts.os_window_class, wname=opts.os_window_name, override_title=opts.os_window_title or None) oswid = boss.add_os_window(
if opts.os_window_state != 'normal': wclass=opts.os_window_class,
change_os_window_state(opts.os_window_state, oswid) wname=opts.os_window_name,
window_state=opts.os_window_state,
override_title=opts.os_window_title or None)
tm = boss.os_window_map[oswid] tm = boss.os_window_map[oswid]
tab = tm.new_tab(empty_tab=True, location=opts.location) tab = tm.new_tab(empty_tab=True, location=opts.location)
if opts.tab_title: if opts.tab_title:

View File

@ -57,6 +57,7 @@ from .utils import (
detach, detach,
expandvars, expandvars,
log_error, log_error,
parse_os_window_state,
single_instance, single_instance,
startup_notification_handler, startup_notification_handler,
unix_socket_paths, unix_socket_paths,
@ -238,12 +239,16 @@ def _run_app(opts: Options, args: CLIOptions, prewarm: PrewarmProcess, bad_lines
with cached_values_for(run_app.cached_values_name) as cached_values: with cached_values_for(run_app.cached_values_name) as cached_values:
startup_sessions = tuple(create_sessions(opts, args, default_session=opts.startup_session)) startup_sessions = tuple(create_sessions(opts, args, default_session=opts.startup_session))
wincls = (startup_sessions[0].os_window_class if startup_sessions else '') or args.cls or appname wincls = (startup_sessions[0].os_window_class if startup_sessions else '') or args.cls or appname
window_state = (args.start_as if args.start_as and args.start_as != 'normal' else None) or (
getattr(startup_sessions[0], 'os_window_state', None) if startup_sessions else None
)
wstate = parse_os_window_state(window_state) if window_state is not None else None
with startup_notification_handler(extra_callback=run_app.first_window_callback) as pre_show_callback: with startup_notification_handler(extra_callback=run_app.first_window_callback) as pre_show_callback:
window_id = create_os_window( window_id = create_os_window(
run_app.initial_window_size_func(get_os_window_sizing_data(opts, startup_sessions[0] if startup_sessions else None), cached_values), run_app.initial_window_size_func(get_os_window_sizing_data(opts, startup_sessions[0] if startup_sessions else None), cached_values),
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,
wincls, load_all_shaders, disallow_override_title=bool(args.title)) wincls, wstate, load_all_shaders, disallow_override_title=bool(args.title))
boss = Boss(opts, args, cached_values, global_shortcuts, prewarm) boss = Boss(opts, args, cached_values, global_shortcuts, prewarm)
boss.start(window_id, startup_sessions) boss.start(window_id, startup_sessions)
if bad_lines: if bad_lines:

View File

@ -61,6 +61,7 @@ class Session:
self.default_title = default_title self.default_title = default_title
self.os_window_size: Optional[WindowSizes] = None self.os_window_size: Optional[WindowSizes] = None
self.os_window_class: Optional[str] = None self.os_window_class: Optional[str] = None
self.os_window_state: Optional[str] = None
self.focus_os_window: bool = False self.focus_os_window: bool = False
def add_tab(self, opts: Options, name: str = '') -> None: def add_tab(self, opts: Options, name: str = '') -> None:
@ -177,6 +178,8 @@ def parse_session(raw: str, opts: Options, environ: Optional[Mapping[str, str]]
ans.os_window_size = WindowSizes(WindowSize(*w), WindowSize(*h)) ans.os_window_size = WindowSizes(WindowSize(*w), WindowSize(*h))
elif cmd == 'os_window_class': elif cmd == 'os_window_class':
ans.os_window_class = rest ans.os_window_class = rest
elif cmd == 'os_window_state':
ans.os_window_state = rest
elif cmd == 'resize_window': elif cmd == 'resize_window':
ans.resize_window(rest.split()) ans.resize_window(rest.split())
else: else:

View File

@ -1422,6 +1422,10 @@ init_state(PyObject *module) {
PyModule_AddIntConstant(module, "IMPERATIVE_CLOSE_REQUESTED", IMPERATIVE_CLOSE_REQUESTED); PyModule_AddIntConstant(module, "IMPERATIVE_CLOSE_REQUESTED", IMPERATIVE_CLOSE_REQUESTED);
PyModule_AddIntConstant(module, "NO_CLOSE_REQUESTED", NO_CLOSE_REQUESTED); PyModule_AddIntConstant(module, "NO_CLOSE_REQUESTED", NO_CLOSE_REQUESTED);
PyModule_AddIntConstant(module, "CLOSE_BEING_CONFIRMED", CLOSE_BEING_CONFIRMED); PyModule_AddIntConstant(module, "CLOSE_BEING_CONFIRMED", CLOSE_BEING_CONFIRMED);
PyModule_AddIntMacro(module, WINDOW_NORMAL);
PyModule_AddIntMacro(module, WINDOW_FULLSCREEN);
PyModule_AddIntMacro(module, WINDOW_MAXIMIZED);
PyModule_AddIntMacro(module, WINDOW_MINIMIZED);
register_at_exit_cleanup_func(STATE_CLEANUP_FUNC, finalize); register_at_exit_cleanup_func(STATE_CLEANUP_FUNC, finalize);
return true; return true;
} }

View File

@ -15,6 +15,7 @@
typedef enum { LEFT_EDGE, TOP_EDGE, RIGHT_EDGE, BOTTOM_EDGE } Edge; typedef enum { LEFT_EDGE, TOP_EDGE, RIGHT_EDGE, BOTTOM_EDGE } Edge;
typedef enum { RESIZE_DRAW_STATIC, RESIZE_DRAW_SCALED, RESIZE_DRAW_BLANK, RESIZE_DRAW_SIZE } ResizeDrawStrategy; typedef enum { RESIZE_DRAW_STATIC, RESIZE_DRAW_SCALED, RESIZE_DRAW_BLANK, RESIZE_DRAW_SIZE } ResizeDrawStrategy;
typedef enum { REPEAT_MIRROR, REPEAT_CLAMP, REPEAT_DEFAULT } RepeatStrategy; typedef enum { REPEAT_MIRROR, REPEAT_CLAMP, REPEAT_DEFAULT } RepeatStrategy;
typedef enum { WINDOW_NORMAL, WINDOW_FULLSCREEN, WINDOW_MAXIMIZED, WINDOW_MINIMIZED } WindowState;
typedef struct { typedef struct {
char_type string[16]; char_type string[16];

View File

@ -25,7 +25,7 @@ from .constants import (
shell_path, shell_path,
ssh_control_master_template, ssh_control_master_template,
) )
from .fast_data_types import Color, open_tty from .fast_data_types import WINDOW_FULLSCREEN, WINDOW_MAXIMIZED, WINDOW_MINIMIZED, WINDOW_NORMAL, Color, open_tty
from .rgb import to_color from .rgb import to_color
from .types import run_once from .types import run_once
from .typing import AddressFamily, PopenType, Socket, StartupCtx from .typing import AddressFamily, PopenType, Socket, StartupCtx
@ -509,6 +509,18 @@ def parse_address_spec(spec: str) -> Tuple[AddressFamily, Union[Tuple[str, int],
return family, address, socket_path return family, address, socket_path
def parse_os_window_state(state: str) -> int:
if state == 'normal':
return WINDOW_NORMAL
elif state in ('fullscreen', 'fullscreened'):
return WINDOW_FULLSCREEN
elif state == 'maximized':
return WINDOW_MAXIMIZED
elif state == 'minimized':
return WINDOW_MINIMIZED
raise ValueError(f'Unknown OS window state: {state}')
def write_all(fd: int, data: Union[str, bytes], block_until_written: bool = True) -> None: def write_all(fd: int, data: Union[str, bytes], block_until_written: bool = True) -> None:
if isinstance(data, str): if isinstance(data, str):
data = data.encode('utf-8') data = data.encode('utf-8')