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 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

View File

@ -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((

View File

@ -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: