#!/usr/bin/env python # License: GPLv3 Copyright: 2020, Kovid Goyal import weakref from collections import deque from contextlib import suppress from itertools import count from typing import Any, Deque, Dict, Iterator, List, Optional, Tuple, Union from .types import WindowGeometry from .typing import EdgeLiteral, TabType, WindowType WindowOrId = Union[WindowType, int] group_id_counter = count(start=1) def reset_group_id_counter() -> None: global group_id_counter group_id_counter = count(start=1) def wrap_increment(val: int, num: int, delta: int) -> int: mult = -1 if delta < 0 else 1 delta = mult * (abs(delta) % num) return (val + num + delta) % num class WindowGroup: def __init__(self) -> None: self.windows: List[WindowType] = [] self.id = next(group_id_counter) def __len__(self) -> int: return len(self.windows) def __bool__(self) -> bool: return bool(self.windows) def __iter__(self) -> Iterator[WindowType]: return iter(self.windows) def __contains__(self, window: WindowType) -> bool: for w in self.windows: if w is window: return True return False @property def needs_attention(self) -> bool: for w in self.windows: if w.needs_attention: return True return False @property def base_window_id(self) -> int: return self.windows[0].id if self.windows else 0 @property def active_window_id(self) -> int: return self.windows[-1].id if self.windows else 0 def add_window(self, window: WindowType) -> None: self.windows.append(window) def remove_window(self, window: WindowType) -> None: with suppress(ValueError): self.windows.remove(window) def serialize_state(self) -> Dict[str, Any]: return { 'id': self.id, 'windows': [w.serialize_state() for w in self.windows] } def decoration(self, which: EdgeLiteral, border_mult: int = 1, is_single_window: bool = False) -> int: if not self.windows: return 0 w = self.windows[0] return w.effective_margin(which, is_single_window=is_single_window) + w.effective_border() * border_mult + w.effective_padding(which) def effective_padding(self, which: EdgeLiteral) -> int: if not self.windows: return 0 w = self.windows[0] return w.effective_padding(which) def effective_border(self) -> int: if not self.windows: return 0 w = self.windows[0] return w.effective_border() def set_geometry(self, geom: WindowGeometry) -> None: for w in self.windows: w.set_geometry(geom) @property def default_bg(self) -> int: if self.windows: w: WindowType = self.windows[-1] return w.screen.color_profile.default_bg return 0 @property def geometry(self) -> Optional[WindowGeometry]: if self.windows: w: WindowType = self.windows[-1] return w.geometry return None @property def is_visible_in_layout(self) -> bool: if self.windows: w: WindowType = self.windows[-1] return w.is_visible_in_layout return False class WindowList: def __init__(self, tab: TabType) -> None: self.all_windows: List[WindowType] = [] self.id_map: Dict[int, WindowType] = {} self.groups: List[WindowGroup] = [] self._active_group_idx: int = -1 self.active_group_history: Deque[int] = deque((), 64) self.tabref = weakref.ref(tab) def __len__(self) -> int: return len(self.all_windows) def __bool__(self) -> bool: return bool(self.all_windows) def __iter__(self) -> Iterator[WindowType]: return iter(self.all_windows) def __contains__(self, window: WindowOrId) -> bool: q = window if isinstance(window, int) else window.id return q in self.id_map def serialize_state(self) -> Dict[str, Any]: return { 'active_group_idx': self.active_group_idx, 'active_group_history': list(self.active_group_history), 'window_groups': [g.serialize_state() for g in self.groups] } @property def active_group_idx(self) -> int: return self._active_group_idx @property def active_window_history(self) -> List[int]: ans = [] seen = set() gid_map = {g.id: g for g in self.groups} for gid in self.active_group_history: g = gid_map.get(gid) if g is not None: w = g.active_window_id if w > 0 and w not in seen: seen.add(w) ans.append(w) return ans def notify_on_active_window_change(self, old_active_window: Optional[WindowType], new_active_window: Optional[WindowType]) -> None: if old_active_window is not None: old_active_window.focus_changed(False) if new_active_window is not None: new_active_window.focus_changed(True) tab = self.tabref() if tab is not None: tab.active_window_changed() def set_active_group_idx(self, i: int, notify: bool = True) -> bool: changed = False if i != self._active_group_idx and 0 <= i < len(self.groups): old_active_window = self.active_window g = self.active_group if g is not None: with suppress(ValueError): self.active_group_history.remove(g.id) self.active_group_history.append(g.id) self._active_group_idx = i new_active_window = self.active_window if old_active_window is not new_active_window: if notify: self.notify_on_active_window_change(old_active_window, new_active_window) changed = True return changed def set_active_group(self, group_id: int) -> bool: for i, gr in enumerate(self.groups): if gr.id == group_id: return self.set_active_group_idx(i) return False def change_tab(self, tab: TabType) -> None: self.tabref = weakref.ref(tab) def iter_windows_with_visibility(self) -> Iterator[Tuple[WindowType, bool]]: for g in self.groups: aw = g.active_window_id for window in g: yield window, window.id == aw def iter_all_layoutable_groups(self, only_visible: bool = False) -> Iterator[WindowGroup]: return iter(g for g in self.groups if g.is_visible_in_layout) if only_visible else iter(self.groups) def iter_windows_with_number(self, only_visible: bool = True) -> Iterator[Tuple[int, WindowType]]: for i, g in enumerate(self.groups): if not only_visible or g.is_visible_in_layout: aw = g.active_window_id for window in g: if window.id == aw: yield i, window break def make_previous_group_active(self, which: int = 1, notify: bool = True) -> None: which = max(1, which) gid_map = {g.id: i for i, g in enumerate(self.groups)} num = len(self.active_group_history) for i in range(num): idx = num - i - 1 gid = self.active_group_history[idx] x = gid_map.get(gid) if x is not None: which -= 1 if which < 1: self.set_active_group_idx(x, notify=notify) return self.set_active_group_idx(len(self.groups) - 1, notify=notify) @property def num_groups(self) -> int: return len(self.groups) def group_for_window(self, x: WindowOrId) -> Optional[WindowGroup]: q = self.id_map[x] if isinstance(x, int) else x for g in self.groups: if q in g: return g return None def group_idx_for_window(self, x: WindowOrId) -> Optional[int]: q = self.id_map[x] if isinstance(x, int) else x for i, g in enumerate(self.groups): if q in g: return i return None def windows_in_group_of(self, x: WindowOrId) -> Iterator[WindowType]: g = self.group_for_window(x) if g is not None: return iter(g) return iter(()) @property def active_group(self) -> Optional[WindowGroup]: with suppress(Exception): return self.groups[self.active_group_idx] return None @property def active_window(self) -> Optional[WindowType]: with suppress(Exception): return self.id_map[self.groups[self.active_group_idx].active_window_id] return None @property def active_group_base(self) -> Optional[WindowType]: with suppress(Exception): return self.id_map[self.groups[self.active_group_idx].base_window_id] return None def set_active_window_group_for(self, x: WindowOrId) -> None: try: q = self.id_map[x] if isinstance(x, int) else x except KeyError: return for i, group in enumerate(self.groups): if q in group: self.set_active_group_idx(i) break def add_window( self, window: WindowType, group_of: Optional[WindowOrId] = None, next_to: Optional[WindowOrId] = None, before: bool = False, make_active: bool = True ) -> WindowGroup: self.all_windows.append(window) self.id_map[window.id] = window target_group: Optional[WindowGroup] = None if group_of is not None: target_group = self.group_for_window(group_of) if target_group is None and next_to is not None: q = self.id_map[next_to] if isinstance(next_to, int) else next_to pos = -1 for i, g in enumerate(self.groups): if q in g: pos = i break if pos > -1: target_group = WindowGroup() self.groups.insert(pos + (0 if before else 1), target_group) if target_group is None: target_group = WindowGroup() if before: self.groups.insert(0, target_group) else: self.groups.append(target_group) old_active_window = self.active_window target_group.add_window(window) if make_active: for i, g in enumerate(self.groups): if g is target_group: self.set_active_group_idx(i, notify=False) break new_active_window = self.active_window if new_active_window is not old_active_window: self.notify_on_active_window_change(old_active_window, new_active_window) return target_group def remove_window(self, x: WindowOrId) -> None: old_active_window = self.active_window q = self.id_map[x] if isinstance(x, int) else x try: self.all_windows.remove(q) except ValueError: pass self.id_map.pop(q.id, None) for i, g in enumerate(tuple(self.groups)): g.remove_window(q) if not g: del self.groups[i] if self.groups: if self.active_group_idx == i: self.make_previous_group_active(notify=False) elif self.active_group_idx >= len(self.groups): self._active_group_idx -= 1 else: self._active_group_idx = -1 break new_active_window = self.active_window if old_active_window is not new_active_window: self.notify_on_active_window_change(old_active_window, new_active_window) def active_window_in_nth_group(self, n: int, clamp: bool = False) -> Optional[WindowType]: if clamp: n = max(0, min(n, self.num_groups - 1)) if 0 <= n < self.num_groups: return self.id_map.get(self.groups[n].active_window_id) return None def activate_next_window_group(self, delta: int) -> None: self.set_active_group_idx(wrap_increment(self.active_group_idx, self.num_groups, delta)) def move_window_group(self, by: Optional[int] = None, to_group: Optional[int] = None) -> bool: if self.active_group_idx < 0 or not self.groups: return False target = -1 if by is not None: target = wrap_increment(self.active_group_idx, self.num_groups, by) if to_group is not None: for i, group in enumerate(self.groups): if group.id == to_group: target = i break if target > -1: if target == self.active_group_idx: return False self.groups[self.active_group_idx], self.groups[target] = self.groups[target], self.groups[self.active_group_idx] self.set_active_group_idx(target) return True return False def compute_needs_borders_map(self, draw_active_borders: bool) -> Dict[int, bool]: ag = self.active_group return {gr.id: ((gr is ag and draw_active_borders) or gr.needs_attention) for gr in self.groups} @property def num_visble_groups(self) -> int: ans = 0 for gr in self.groups: if gr.is_visible_in_layout: ans += 1 return ans