Reduce the number of spurious focus events
1) When performing operations known to cause lots of focus changes such as creating new sessions/windows or moving windows, forcibly ignore focus events 2) Track window focus state and dont report focus events when the state is unchanged by a focus_changed() call This allows focus specific code to be restricted to just 2-3 places instead of having to track every possible function that could change focus. Fixes #6083
This commit is contained in:
parent
719fe9ea04
commit
eab3b2a689
149
kitty/boss.py
149
kitty/boss.py
@ -7,7 +7,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
from contextlib import suppress
|
from contextlib import contextmanager, suppress
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from gettext import gettext as _
|
from gettext import gettext as _
|
||||||
from time import monotonic, sleep
|
from time import monotonic, sleep
|
||||||
@ -17,6 +17,7 @@ from typing import (
|
|||||||
Callable,
|
Callable,
|
||||||
Container,
|
Container,
|
||||||
Dict,
|
Dict,
|
||||||
|
Generator,
|
||||||
Iterable,
|
Iterable,
|
||||||
Iterator,
|
Iterator,
|
||||||
List,
|
List,
|
||||||
@ -381,17 +382,20 @@ class Boss:
|
|||||||
si = startup_sessions or create_sessions(get_options(), self.args, default_session=get_options().startup_session)
|
si = startup_sessions or create_sessions(get_options(), self.args, default_session=get_options().startup_session)
|
||||||
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:
|
with Window.set_ignore_focus_changes_for_new_windows():
|
||||||
# The window state from the CLI options will override and apply to every single OS window in startup session
|
for startup_session in si:
|
||||||
wstate = self.args.start_as if self.args.start_as and self.args.start_as != 'normal' else None
|
# The window state from the CLI options will override and apply to every single OS window in startup session
|
||||||
wid = self.add_os_window(startup_session, window_state=wstate, os_window_id=os_window_id)
|
wstate = self.args.start_as if self.args.start_as and self.args.start_as != 'normal' else None
|
||||||
if startup_session.focus_os_window:
|
wid = self.add_os_window(startup_session, window_state=wstate, os_window_id=os_window_id)
|
||||||
focused_os_window = wid
|
if startup_session.focus_os_window:
|
||||||
os_window_id = None
|
focused_os_window = wid
|
||||||
if focused_os_window > 0:
|
os_window_id = None
|
||||||
focus_os_window(focused_os_window, True, token)
|
if focused_os_window > 0:
|
||||||
elif token and is_wayland() and wid:
|
focus_os_window(focused_os_window, True, token)
|
||||||
focus_os_window(wid, True, token)
|
elif token and is_wayland() and wid:
|
||||||
|
focus_os_window(wid, True, token)
|
||||||
|
for w in self.all_windows:
|
||||||
|
w.ignore_focus_changes = False
|
||||||
|
|
||||||
def add_os_window(
|
def add_os_window(
|
||||||
self,
|
self,
|
||||||
@ -807,37 +811,51 @@ class Boss:
|
|||||||
if not self.shutting_down:
|
if not self.shutting_down:
|
||||||
self.mark_os_window_for_close(src_tab.os_window_id)
|
self.mark_os_window_for_close(src_tab.os_window_id)
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def suppress_focus_change_events(self) -> Generator[None, None, None]:
|
||||||
|
changes = {}
|
||||||
|
for w in self.window_id_map.values():
|
||||||
|
changes[w] = w.ignore_focus_changes
|
||||||
|
w.ignore_focus_changes = True
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
for w, val in changes.items():
|
||||||
|
w.ignore_focus_changes = val
|
||||||
|
|
||||||
def on_child_death(self, window_id: int) -> None:
|
def on_child_death(self, window_id: int) -> None:
|
||||||
prev_active_window = self.active_window
|
prev_active_window = self.active_window
|
||||||
window = self.window_id_map.pop(window_id, None)
|
window = self.window_id_map.pop(window_id, None)
|
||||||
if window is None:
|
if window is None:
|
||||||
return
|
return
|
||||||
for close_action in window.actions_on_close:
|
with self.suppress_focus_change_events():
|
||||||
try:
|
for close_action in window.actions_on_close:
|
||||||
close_action(window)
|
try:
|
||||||
except Exception:
|
close_action(window)
|
||||||
import traceback
|
except Exception:
|
||||||
traceback.print_exc()
|
import traceback
|
||||||
os_window_id = window.os_window_id
|
traceback.print_exc()
|
||||||
window.destroy()
|
os_window_id = window.os_window_id
|
||||||
tm = self.os_window_map.get(os_window_id)
|
window.destroy()
|
||||||
tab = None
|
tm = self.os_window_map.get(os_window_id)
|
||||||
if tm is not None:
|
tab = None
|
||||||
for q in tm:
|
if tm is not None:
|
||||||
if window in q:
|
for q in tm:
|
||||||
tab = q
|
if window in q:
|
||||||
break
|
tab = q
|
||||||
if tab is not None:
|
break
|
||||||
tab.remove_window(window)
|
if tab is not None:
|
||||||
self._cleanup_tab_after_window_removal(tab)
|
tab.remove_window(window)
|
||||||
for removal_action in window.actions_on_removal:
|
self._cleanup_tab_after_window_removal(tab)
|
||||||
try:
|
for removal_action in window.actions_on_removal:
|
||||||
removal_action(window)
|
try:
|
||||||
except Exception:
|
removal_action(window)
|
||||||
import traceback
|
except Exception:
|
||||||
traceback.print_exc()
|
import traceback
|
||||||
del window.actions_on_close[:], window.actions_on_removal[:]
|
traceback.print_exc()
|
||||||
window = self.active_window
|
del window.actions_on_close[:], window.actions_on_removal[:]
|
||||||
|
window = self.active_window
|
||||||
|
|
||||||
if window is not prev_active_window:
|
if window is not prev_active_window:
|
||||||
if prev_active_window is not None:
|
if prev_active_window is not None:
|
||||||
prev_active_window.focus_changed(False)
|
prev_active_window.focus_changed(False)
|
||||||
@ -2498,36 +2516,37 @@ class Boss:
|
|||||||
src_tab = self.tab_for_window(window)
|
src_tab = self.tab_for_window(window)
|
||||||
if src_tab is None:
|
if src_tab is None:
|
||||||
return
|
return
|
||||||
if target_os_window_id == 'new':
|
with self.suppress_focus_change_events():
|
||||||
target_os_window_id = self.add_os_window()
|
if target_os_window_id == 'new':
|
||||||
tm = self.os_window_map[target_os_window_id]
|
target_os_window_id = self.add_os_window()
|
||||||
target_tab = tm.new_tab(empty_tab=True)
|
tm = self.os_window_map[target_os_window_id]
|
||||||
else:
|
target_tab = tm.new_tab(empty_tab=True)
|
||||||
target_os_window_id = target_os_window_id or current_os_window()
|
|
||||||
if isinstance(target_tab_id, str):
|
|
||||||
if not isinstance(target_os_window_id, int):
|
|
||||||
q = self.active_tab_manager
|
|
||||||
assert q is not None
|
|
||||||
tm = q
|
|
||||||
else:
|
|
||||||
tm = self.os_window_map[target_os_window_id]
|
|
||||||
if target_tab_id == 'new':
|
|
||||||
target_tab = tm.new_tab(empty_tab=True)
|
|
||||||
else:
|
|
||||||
target_tab = tm.tab_at_location(target_tab_id) or tm.new_tab(empty_tab=True)
|
|
||||||
else:
|
else:
|
||||||
for tab in self.all_tabs:
|
target_os_window_id = target_os_window_id or current_os_window()
|
||||||
if tab.id == target_tab_id:
|
if isinstance(target_tab_id, str):
|
||||||
target_tab = tab
|
if not isinstance(target_os_window_id, int):
|
||||||
target_os_window_id = tab.os_window_id
|
q = self.active_tab_manager
|
||||||
break
|
assert q is not None
|
||||||
|
tm = q
|
||||||
|
else:
|
||||||
|
tm = self.os_window_map[target_os_window_id]
|
||||||
|
if target_tab_id == 'new':
|
||||||
|
target_tab = tm.new_tab(empty_tab=True)
|
||||||
|
else:
|
||||||
|
target_tab = tm.tab_at_location(target_tab_id) or tm.new_tab(empty_tab=True)
|
||||||
else:
|
else:
|
||||||
return
|
for tab in self.all_tabs:
|
||||||
|
if tab.id == target_tab_id:
|
||||||
|
target_tab = tab
|
||||||
|
target_os_window_id = tab.os_window_id
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
for detached_window in src_tab.detach_window(window):
|
for detached_window in src_tab.detach_window(window):
|
||||||
target_tab.attach_window(detached_window)
|
target_tab.attach_window(detached_window)
|
||||||
self._cleanup_tab_after_window_removal(src_tab)
|
self._cleanup_tab_after_window_removal(src_tab)
|
||||||
target_tab.make_active()
|
target_tab.make_active()
|
||||||
|
|
||||||
def _move_tab_to(self, tab: Optional[Tab] = None, target_os_window_id: Optional[int] = None) -> None:
|
def _move_tab_to(self, tab: Optional[Tab] = None, target_os_window_id: Optional[int] = None) -> None:
|
||||||
tab = tab or self.active_tab
|
tab = tab or self.active_tab
|
||||||
|
|||||||
@ -435,7 +435,7 @@ force_window_launch = ForceWindowLaunch()
|
|||||||
non_window_launch_types = 'background', 'clipboard', 'primary'
|
non_window_launch_types = 'background', 'clipboard', 'primary'
|
||||||
|
|
||||||
|
|
||||||
def launch(
|
def _launch(
|
||||||
boss: Boss,
|
boss: Boss,
|
||||||
opts: LaunchCLIOptions,
|
opts: LaunchCLIOptions,
|
||||||
args: List[str],
|
args: List[str],
|
||||||
@ -588,14 +588,19 @@ def launch(
|
|||||||
tab = tab_for_window(boss, opts, target_tab)
|
tab = tab_for_window(boss, opts, target_tab)
|
||||||
if tab is not None:
|
if tab is not None:
|
||||||
watchers = load_watch_modules(opts.watcher)
|
watchers = load_watch_modules(opts.watcher)
|
||||||
new_window: Window = tab.new_window(env=env or None, watchers=watchers or None, is_clone_launch=is_clone_launch, **kw)
|
with Window.set_ignore_focus_changes_for_new_windows(opts.keep_focus):
|
||||||
|
new_window: Window = tab.new_window(
|
||||||
|
env=env or None, watchers=watchers or None, is_clone_launch=is_clone_launch, **kw)
|
||||||
if spacing:
|
if spacing:
|
||||||
patch_window_edges(new_window, spacing)
|
patch_window_edges(new_window, spacing)
|
||||||
tab.relayout()
|
tab.relayout()
|
||||||
if opts.color:
|
if opts.color:
|
||||||
apply_colors(new_window, opts.color)
|
apply_colors(new_window, opts.color)
|
||||||
if opts.keep_focus and active:
|
if opts.keep_focus:
|
||||||
boss.set_active_window(active, switch_os_window_if_needed=True, for_keep_focus=True)
|
if active:
|
||||||
|
boss.set_active_window(active, switch_os_window_if_needed=True, for_keep_focus=True)
|
||||||
|
if not Window.initial_ignore_focus_changes_context_manager_in_operation:
|
||||||
|
new_window.ignore_focus_changes = False
|
||||||
if opts.logo:
|
if opts.logo:
|
||||||
new_window.set_logo(opts.logo, opts.logo_position or '', opts.logo_alpha)
|
new_window.set_logo(opts.logo, opts.logo_position or '', opts.logo_alpha)
|
||||||
if opts.type == 'overlay-main':
|
if opts.type == 'overlay-main':
|
||||||
@ -604,6 +609,25 @@ def launch(
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def launch(
|
||||||
|
boss: Boss,
|
||||||
|
opts: LaunchCLIOptions,
|
||||||
|
args: List[str],
|
||||||
|
target_tab: Optional[Tab] = None,
|
||||||
|
force_target_tab: bool = False,
|
||||||
|
active: Optional[Window] = None,
|
||||||
|
is_clone_launch: str = '',
|
||||||
|
rc_from_window: Optional[Window] = None,
|
||||||
|
) -> Optional[Window]:
|
||||||
|
active = active or boss.active_window_for_cwd
|
||||||
|
if opts.keep_focus and active:
|
||||||
|
orig, active.ignore_focus_changes = active.ignore_focus_changes, True
|
||||||
|
try:
|
||||||
|
return _launch(boss, opts, args, target_tab, force_target_tab, active, is_clone_launch, rc_from_window)
|
||||||
|
finally:
|
||||||
|
if opts.keep_focus and active:
|
||||||
|
active.ignore_focus_changes = orig
|
||||||
|
|
||||||
@run_once
|
@run_once
|
||||||
def clone_safe_opts() -> FrozenSet[str]:
|
def clone_safe_opts() -> FrozenSet[str]:
|
||||||
return frozenset((
|
return frozenset((
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import re
|
|||||||
import sys
|
import sys
|
||||||
import weakref
|
import weakref
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from contextlib import suppress
|
from contextlib import contextmanager, suppress
|
||||||
from enum import Enum, IntEnum, auto
|
from enum import Enum, IntEnum, auto
|
||||||
from functools import lru_cache, partial
|
from functools import lru_cache, partial
|
||||||
from gettext import gettext as _
|
from gettext import gettext as _
|
||||||
@ -19,6 +19,7 @@ from typing import (
|
|||||||
Callable,
|
Callable,
|
||||||
Deque,
|
Deque,
|
||||||
Dict,
|
Dict,
|
||||||
|
Generator,
|
||||||
Iterable,
|
Iterable,
|
||||||
List,
|
List,
|
||||||
NamedTuple,
|
NamedTuple,
|
||||||
@ -542,6 +543,22 @@ class Window:
|
|||||||
|
|
||||||
window_custom_type: str = ''
|
window_custom_type: str = ''
|
||||||
overlay_type = OverlayType.transient
|
overlay_type = OverlayType.transient
|
||||||
|
initial_ignore_focus_changes: bool = False
|
||||||
|
initial_ignore_focus_changes_context_manager_in_operation: bool = False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@contextmanager
|
||||||
|
def set_ignore_focus_changes_for_new_windows(cls, value: bool = True) -> Generator[None, None, None]:
|
||||||
|
if cls.initial_ignore_focus_changes_context_manager_in_operation:
|
||||||
|
yield
|
||||||
|
else:
|
||||||
|
orig, cls.initial_ignore_focus_changes = cls.initial_ignore_focus_changes, value
|
||||||
|
cls.initial_ignore_focus_changes_context_manager_in_operation = True
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
cls.initial_ignore_focus_changes = orig
|
||||||
|
cls.initial_ignore_focus_changes_context_manager_in_operation = False
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@ -560,6 +577,7 @@ class Window:
|
|||||||
else:
|
else:
|
||||||
self.watchers = global_watchers().copy()
|
self.watchers = global_watchers().copy()
|
||||||
self.last_focused_at = 0.
|
self.last_focused_at = 0.
|
||||||
|
self.is_focused: bool = False
|
||||||
self.last_resized_at = 0.
|
self.last_resized_at = 0.
|
||||||
self.started_at = monotonic()
|
self.started_at = monotonic()
|
||||||
self.current_remote_data: List[str] = []
|
self.current_remote_data: List[str] = []
|
||||||
@ -574,6 +592,7 @@ class Window:
|
|||||||
self.pty_resized_once = False
|
self.pty_resized_once = False
|
||||||
self.last_reported_pty_size = (-1, -1, -1, -1)
|
self.last_reported_pty_size = (-1, -1, -1, -1)
|
||||||
self.needs_attention = False
|
self.needs_attention = False
|
||||||
|
self.ignore_focus_changes = self.initial_ignore_focus_changes
|
||||||
self.override_title = override_title
|
self.override_title = override_title
|
||||||
self.default_title = os.path.basename(child.argv[0] or appname)
|
self.default_title = os.path.basename(child.argv[0] or appname)
|
||||||
self.child_title = self.default_title
|
self.child_title = self.default_title
|
||||||
@ -993,8 +1012,9 @@ class Window:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def focus_changed(self, focused: bool) -> None:
|
def focus_changed(self, focused: bool) -> None:
|
||||||
if self.destroyed:
|
if self.destroyed or self.ignore_focus_changes or self.is_focused == focused:
|
||||||
return
|
return
|
||||||
|
self.is_focused = focused
|
||||||
call_watchers(weakref.ref(self), 'on_focus_change', {'focused': focused})
|
call_watchers(weakref.ref(self), 'on_focus_change', {'focused': focused})
|
||||||
for c in self.actions_on_focus_change:
|
for c in self.actions_on_focus_change:
|
||||||
try:
|
try:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user