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:
Kovid Goyal 2023-03-02 13:29:05 +05:30
parent 719fe9ea04
commit eab3b2a689
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
3 changed files with 134 additions and 71 deletions

View File

@ -7,7 +7,7 @@ import json
import os
import re
import sys
from contextlib import suppress
from contextlib import contextmanager, suppress
from functools import partial
from gettext import gettext as _
from time import monotonic, sleep
@ -17,6 +17,7 @@ from typing import (
Callable,
Container,
Dict,
Generator,
Iterable,
Iterator,
List,
@ -381,17 +382,20 @@ class Boss:
si = startup_sessions or create_sessions(get_options(), self.args, default_session=get_options().startup_session)
focused_os_window = wid = 0
token = os.environ.pop('XDG_ACTIVATION_TOKEN', '')
for startup_session in si:
# 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 focused_os_window > 0:
focus_os_window(focused_os_window, True, token)
elif token and is_wayland() and wid:
focus_os_window(wid, True, token)
with Window.set_ignore_focus_changes_for_new_windows():
for startup_session in si:
# 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 focused_os_window > 0:
focus_os_window(focused_os_window, 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(
self,
@ -807,37 +811,51 @@ class Boss:
if not self.shutting_down:
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:
prev_active_window = self.active_window
window = self.window_id_map.pop(window_id, None)
if window is None:
return
for close_action in window.actions_on_close:
try:
close_action(window)
except Exception:
import traceback
traceback.print_exc()
os_window_id = window.os_window_id
window.destroy()
tm = self.os_window_map.get(os_window_id)
tab = None
if tm is not None:
for q in tm:
if window in q:
tab = q
break
if tab is not None:
tab.remove_window(window)
self._cleanup_tab_after_window_removal(tab)
for removal_action in window.actions_on_removal:
try:
removal_action(window)
except Exception:
import traceback
traceback.print_exc()
del window.actions_on_close[:], window.actions_on_removal[:]
window = self.active_window
with self.suppress_focus_change_events():
for close_action in window.actions_on_close:
try:
close_action(window)
except Exception:
import traceback
traceback.print_exc()
os_window_id = window.os_window_id
window.destroy()
tm = self.os_window_map.get(os_window_id)
tab = None
if tm is not None:
for q in tm:
if window in q:
tab = q
break
if tab is not None:
tab.remove_window(window)
self._cleanup_tab_after_window_removal(tab)
for removal_action in window.actions_on_removal:
try:
removal_action(window)
except Exception:
import traceback
traceback.print_exc()
del window.actions_on_close[:], window.actions_on_removal[:]
window = self.active_window
if window is not prev_active_window:
if prev_active_window is not None:
prev_active_window.focus_changed(False)
@ -2498,36 +2516,37 @@ class Boss:
src_tab = self.tab_for_window(window)
if src_tab is None:
return
if target_os_window_id == 'new':
target_os_window_id = self.add_os_window()
tm = self.os_window_map[target_os_window_id]
target_tab = tm.new_tab(empty_tab=True)
else:
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)
with self.suppress_focus_change_events():
if target_os_window_id == 'new':
target_os_window_id = self.add_os_window()
tm = self.os_window_map[target_os_window_id]
target_tab = tm.new_tab(empty_tab=True)
else:
for tab in self.all_tabs:
if tab.id == target_tab_id:
target_tab = tab
target_os_window_id = tab.os_window_id
break
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:
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):
target_tab.attach_window(detached_window)
self._cleanup_tab_after_window_removal(src_tab)
target_tab.make_active()
for detached_window in src_tab.detach_window(window):
target_tab.attach_window(detached_window)
self._cleanup_tab_after_window_removal(src_tab)
target_tab.make_active()
def _move_tab_to(self, tab: Optional[Tab] = None, target_os_window_id: Optional[int] = None) -> None:
tab = tab or self.active_tab

View File

@ -435,7 +435,7 @@ force_window_launch = ForceWindowLaunch()
non_window_launch_types = 'background', 'clipboard', 'primary'
def launch(
def _launch(
boss: Boss,
opts: LaunchCLIOptions,
args: List[str],
@ -588,14 +588,19 @@ def launch(
tab = tab_for_window(boss, opts, target_tab)
if tab is not None:
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:
patch_window_edges(new_window, spacing)
tab.relayout()
if opts.color:
apply_colors(new_window, opts.color)
if opts.keep_focus and active:
boss.set_active_window(active, switch_os_window_if_needed=True, for_keep_focus=True)
if opts.keep_focus:
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:
new_window.set_logo(opts.logo, opts.logo_position or '', opts.logo_alpha)
if opts.type == 'overlay-main':
@ -604,6 +609,25 @@ def launch(
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
def clone_safe_opts() -> FrozenSet[str]:
return frozenset((

View File

@ -7,7 +7,7 @@ import re
import sys
import weakref
from collections import deque
from contextlib import suppress
from contextlib import contextmanager, suppress
from enum import Enum, IntEnum, auto
from functools import lru_cache, partial
from gettext import gettext as _
@ -19,6 +19,7 @@ from typing import (
Callable,
Deque,
Dict,
Generator,
Iterable,
List,
NamedTuple,
@ -542,6 +543,22 @@ class Window:
window_custom_type: str = ''
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__(
self,
@ -560,6 +577,7 @@ class Window:
else:
self.watchers = global_watchers().copy()
self.last_focused_at = 0.
self.is_focused: bool = False
self.last_resized_at = 0.
self.started_at = monotonic()
self.current_remote_data: List[str] = []
@ -574,6 +592,7 @@ class Window:
self.pty_resized_once = False
self.last_reported_pty_size = (-1, -1, -1, -1)
self.needs_attention = False
self.ignore_focus_changes = self.initial_ignore_focus_changes
self.override_title = override_title
self.default_title = os.path.basename(child.argv[0] or appname)
self.child_title = self.default_title
@ -993,8 +1012,9 @@ class Window:
return False
def focus_changed(self, focused: bool) -> None:
if self.destroyed:
if self.destroyed or self.ignore_focus_changes or self.is_focused == focused:
return
self.is_focused = focused
call_watchers(weakref.ref(self), 'on_focus_change', {'focused': focused})
for c in self.actions_on_focus_change:
try: