From ba83ce7b102f93ef4f0148a7dcc7b2374b71269c Mon Sep 17 00:00:00 2001 From: pagedown Date: Sat, 18 Feb 2023 14:02:19 +0800 Subject: [PATCH] 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. --- docs/changelog.rst | 6 ++++ docs/overview.rst | 2 ++ glfw/cocoa_window.m | 11 ++++++-- kitty/boss.py | 13 +++++---- kitty/fast_data_types.pyi | 13 ++++++--- kitty/glfw.c | 58 +++++++++++++++++++++++++++++---------- kitty/launch.py | 10 ++++--- kitty/main.py | 7 ++++- kitty/session.py | 3 ++ kitty/state.c | 4 +++ kitty/state.h | 1 + kitty/utils.py | 14 +++++++++- 12 files changed, 109 insertions(+), 33 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index dedac96f8..463149449 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -56,6 +56,12 @@ Detailed list of changes - 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] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/overview.rst b/docs/overview.rst index ba8565488..ad2942e22 100644 --- a/docs/overview.rst +++ b/docs/overview.rst @@ -161,6 +161,8 @@ option in :file:`kitty.conf`. An example, showing all available commands: os_window_size 80c 24c # Set the --class for the new OS window os_window_class mywindow + # Change the OS window state to normal, fullscreen, maximized or minimized + os_window_state normal launch sh # Resize the current window (see the resize_window action for details) resize_window wider 2 diff --git a/glfw/cocoa_window.m b/glfw/cocoa_window.m index 390983694..9b66956d4 100644 --- a/glfw/cocoa_window.m +++ b/glfw/cocoa_window.m @@ -1868,8 +1868,9 @@ int _glfwPlatformCreateWindow(_GLFWwindow* window, if (window->monitor) { - _glfwPlatformShowWindow(window); - _glfwPlatformFocusWindow(window); + // Do not show the window here until after setting the window size, maximized state, and full screen + // _glfwPlatformShowWindow(window); + // _glfwPlatformFocusWindow(window); acquireMonitor(window); } @@ -2061,8 +2062,12 @@ void _glfwPlatformRestoreWindow(_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 setResizeIncrements:original]; + } } void _glfwPlatformShowWindow(_GLFWwindow* window) diff --git a/kitty/boss.py b/kitty/boss.py index 664923a4f..94726b241 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -75,7 +75,6 @@ from .fast_data_types import ( apply_options_update, background_opacity_of, change_background_opacity, - change_os_window_state, cocoa_hide_app, cocoa_hide_other_apps, cocoa_minimize_os_window, @@ -137,6 +136,7 @@ from .utils import ( macos_version, open_url, parse_address_spec, + parse_os_window_state, parse_uri_list, platform_window_id, remove_socket_file, @@ -382,12 +382,12 @@ class Boss: focused_os_window = wid = 0 token = os.environ.pop('XDG_ACTIVATION_TOKEN', '') 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: focused_os_window = wid os_window_id = None - if self.args.start_as != 'normal': - change_os_window_state(self.args.start_as, wid) if focused_os_window > 0: focus_os_window(focused_os_window, True, token) elif token and is_wayland() and wid: @@ -399,6 +399,7 @@ class Boss: os_window_id: Optional[int] = None, wclass: Optional[str] = None, wname: Optional[str] = None, + window_state: Optional[str] = None, opts_for_size: Optional[Options] = None, startup_id: 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 wname = wname or self.args.name or wclass 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: os_window_id = create_os_window( initial_window_size_func(size_data, self.cached_values), 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: wname = self.args.name or self.args.cls or appname wclass = self.args.cls or appname diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index 615c91fdf..60d8d0850 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -294,6 +294,10 @@ PRESS: int RELEASE: int DRAG: 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, wm_class_name: str, wm_class_class: str, + window_state: Optional[int] = WINDOW_NORMAL, load_programs: Optional[Callable[[bool], None]] = None, - x: int = -1, - y: int = -1, - disallow_override_title: bool = False, + x: Optional[int] = -1, + y: Optional[int] = -1, + disallow_override_title: Optional[bool] = False, ) -> int: pass @@ -818,7 +823,7 @@ def cocoa_set_menubar_title(title: str) -> None: 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 diff --git a/kitty/glfw.c b/kitty/glfw.c index 9c9e5cdcc..2f02f96f6 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -738,6 +738,25 @@ toggle_maximized_for_os_window(OSWindow *w) { 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__ static GLFWwindow *apple_preserve_common_context = NULL; @@ -792,12 +811,14 @@ native_window_handle(GLFWwindow *w) { static PyObject* 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; - PyObject *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}; - if (!PyArg_ParseTupleAndKeywords(args, kw, "OOsss|Oiip", (char**)kwlist, - &get_window_size, &pre_show_callback, &title, &wm_class_name, &wm_class_class, &load_programs, &x, &y, &disallow_override_title)) return NULL; + 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", "window_state", "load_programs", "x", "y", "disallow_override_title", NULL}; + if (!PyArg_ParseTupleAndKeywords(args, kw, "OOsss|OOiip", (char**)kwlist, + &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; if (is_first_window) { @@ -886,7 +907,9 @@ create_os_window(PyObject UNUSED *self, PyObject *args, PyObject *kw) { if (pret == NULL) return NULL; Py_DECREF(pret); if (x != -1 && y != -1) glfwSetWindowPos(glfw_window, x, y); +#ifndef __APPLE__ glfwShowWindow(glfw_window); +#endif #ifdef __APPLE__ float n_xscale, n_yscale; double n_xdpi, n_ydpi; @@ -973,6 +996,14 @@ create_os_window(PyObject UNUSED *self, PyObject *args, PyObject *kw) { 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); } @@ -1393,19 +1424,16 @@ cocoa_minimize_os_window(PyObject UNUSED *self, PyObject *args) { static PyObject* change_os_window_state(PyObject *self UNUSED, PyObject *args) { - char *state; + int state; 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(); if (!w || !w->handle) Py_RETURN_NONE; - if (strcmp(state, "maximized") == 0) glfwMaximizeWindow(w->handle); - else if (strcmp(state, "minimized") == 0) glfwIconifyWindow(w->handle); - else if (strcmp(state, "fullscreen") == 0 || strcmp(state, "fullscreened") == 0) { - if (!is_os_window_fullscreen(w)) toggle_fullscreen_for_os_window(w); - } else if (strcmp(state, "normal") == 0) { - 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; } + if (state < WINDOW_NORMAL || state > WINDOW_MINIMIZED) { + PyErr_SetString(PyExc_ValueError, "Unknown window state"); + return NULL; + } + change_state_for_os_window(w, state); Py_RETURN_NONE; } diff --git a/kitty/launch.py b/kitty/launch.py index 23c7c594e..85adcd386 100644 --- a/kitty/launch.py +++ b/kitty/launch.py @@ -13,7 +13,7 @@ from .cli import parse_args from .cli_stub import LaunchCLIOptions from .clipboard import set_clipboard_string, set_primary_selection 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 .tabs import Tab, TabManager 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: 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) - if opts.os_window_state != 'normal': - change_os_window_state(opts.os_window_state, oswid) + oswid = boss.add_os_window( + wclass=opts.os_window_class, + 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] tab = tm.new_tab(empty_tab=True, location=opts.location) if opts.tab_title: diff --git a/kitty/main.py b/kitty/main.py index 4f20f6c95..f09c5965a 100644 --- a/kitty/main.py +++ b/kitty/main.py @@ -57,6 +57,7 @@ from .utils import ( detach, expandvars, log_error, + parse_os_window_state, single_instance, startup_notification_handler, 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: 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 + 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: 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), pre_show_callback, 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.start(window_id, startup_sessions) if bad_lines: diff --git a/kitty/session.py b/kitty/session.py index 586ed3d7d..e814700ab 100644 --- a/kitty/session.py +++ b/kitty/session.py @@ -61,6 +61,7 @@ class Session: self.default_title = default_title self.os_window_size: Optional[WindowSizes] = None self.os_window_class: Optional[str] = None + self.os_window_state: Optional[str] = None self.focus_os_window: bool = False 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)) elif cmd == 'os_window_class': ans.os_window_class = rest + elif cmd == 'os_window_state': + ans.os_window_state = rest elif cmd == 'resize_window': ans.resize_window(rest.split()) else: diff --git a/kitty/state.c b/kitty/state.c index 303155a6c..53cf43be6 100644 --- a/kitty/state.c +++ b/kitty/state.c @@ -1422,6 +1422,10 @@ init_state(PyObject *module) { PyModule_AddIntConstant(module, "IMPERATIVE_CLOSE_REQUESTED", IMPERATIVE_CLOSE_REQUESTED); PyModule_AddIntConstant(module, "NO_CLOSE_REQUESTED", NO_CLOSE_REQUESTED); 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); return true; } diff --git a/kitty/state.h b/kitty/state.h index 1356e3cd1..dc693ca2d 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -15,6 +15,7 @@ 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 { REPEAT_MIRROR, REPEAT_CLAMP, REPEAT_DEFAULT } RepeatStrategy; +typedef enum { WINDOW_NORMAL, WINDOW_FULLSCREEN, WINDOW_MAXIMIZED, WINDOW_MINIMIZED } WindowState; typedef struct { char_type string[16]; diff --git a/kitty/utils.py b/kitty/utils.py index 4a0cf4294..01fde340a 100644 --- a/kitty/utils.py +++ b/kitty/utils.py @@ -25,7 +25,7 @@ from .constants import ( shell_path, 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 .types import run_once 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 +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: if isinstance(data, str): data = data.encode('utf-8')