From ba83ce7b102f93ef4f0148a7dcc7b2374b71269c Mon Sep 17 00:00:00 2001 From: pagedown Date: Sat, 18 Feb 2023 14:02:19 +0800 Subject: [PATCH 1/3] 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') From 75a4f45a232c6728364b1012dee2ce017a9a8023 Mon Sep 17 00:00:00 2001 From: pagedown Date: Sat, 18 Feb 2023 17:43:45 +0800 Subject: [PATCH 2/3] ... --- kitty/glfw.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kitty/glfw.c b/kitty/glfw.c index 2f02f96f6..e02f82d5d 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -999,7 +999,7 @@ create_os_window(PyObject UNUSED *self, PyObject *args, PyObject *kw) { // 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); + if (window_state != WINDOW_NORMAL) change_state_for_os_window(w, window_state); #ifdef __APPLE__ // macOS: Show the window after it is ready glfwShowWindow(glfw_window); From b0c28148b125cf0ab0ffb584b610ed28b710fea2 Mon Sep 17 00:00:00 2001 From: pagedown Date: Sat, 18 Feb 2023 19:35:01 +0800 Subject: [PATCH 3/3] macOS: Fix window not taking up full height when the title bar is hidden When the remembered window size is the full screen height, the window height decreases after hiding the title bar. --- kitty/glfw.c | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/kitty/glfw.c b/kitty/glfw.c index e02f82d5d..e72a87c35 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -947,6 +947,18 @@ create_os_window(PyObject UNUSED *self, PyObject *args, PyObject *kw) { w->fonts_data = fonts_data; w->shown_once = true; w->last_focused_counter = ++focus_counter; +#ifdef __APPLE__ + if (OPT(hide_window_decorations) & 2) { + glfwHideCocoaTitlebar(glfw_window, true); + } else if (!(OPT(macos_show_window_title_in) & WINDOW)) { + if (glfwGetCocoaWindow) cocoa_hide_window_title(glfwGetCocoaWindow(glfw_window)); + else log_error("Failed to load glfwGetCocoaWindow"); + } + if (OPT(hide_window_decorations)) { + // Need to resize the window again after hiding decorations or title bar to take up screen space + glfwSetWindowSize(glfw_window, width, height); + } +#endif os_window_update_size_increments(w); #ifdef __APPLE__ if (OPT(macos_option_as_alt)) glfwSetCocoaTextInputFilter(glfw_window, filter_option); @@ -956,6 +968,9 @@ create_os_window(PyObject UNUSED *self, PyObject *args, PyObject *kw) { if (logo.pixels && logo.width && logo.height) glfwSetWindowIcon(glfw_window, 1, &logo); glfwSetCursor(glfw_window, standard_cursor); update_os_window_viewport(w, false); +#ifdef __APPLE__ + if (glfwGetCocoaWindow) cocoa_make_window_resizable(glfwGetCocoaWindow(glfw_window), OPT(macos_window_resizable)); +#endif glfwSetWindowPosCallback(glfw_window, window_pos_callback); // missing size callback glfwSetWindowCloseCallback(glfw_window, window_close_callback); @@ -973,16 +988,6 @@ create_os_window(PyObject UNUSED *self, PyObject *args, PyObject *kw) { glfwSetScrollCallback(glfw_window, scroll_callback); glfwSetKeyboardCallback(glfw_window, key_callback); glfwSetDropCallback(glfw_window, drop_callback); -#ifdef __APPLE__ - if (glfwGetCocoaWindow) { - if (OPT(hide_window_decorations) & 2) { - glfwHideCocoaTitlebar(glfw_window, true); - } else if (!(OPT(macos_show_window_title_in) & WINDOW)) { - cocoa_hide_window_title(glfwGetCocoaWindow(glfw_window)); - } - cocoa_make_window_resizable(glfwGetCocoaWindow(glfw_window), OPT(macos_window_resizable)); - } else log_error("Failed to load glfwGetCocoaWindow"); -#endif monotonic_t now = monotonic(); w->is_focused = true; w->cursor_blink_zero_time = now;