1#!/usr/local/bin/python3.8 2# vim:fileencoding=utf-8 3# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net> 4 5import weakref 6from collections import deque 7from contextlib import suppress 8from itertools import count 9from typing import ( 10 Any, Deque, Dict, Generator, Iterator, List, Optional, Tuple, Union 11) 12 13from .types import WindowGeometry 14from .typing import EdgeLiteral, TabType, WindowType 15 16WindowOrId = Union[WindowType, int] 17group_id_counter = count(start=1) 18 19 20def reset_group_id_counter() -> None: 21 global group_id_counter 22 group_id_counter = count(start=1) 23 24 25def wrap_increment(val: int, num: int, delta: int) -> int: 26 mult = -1 if delta < 0 else 1 27 delta = mult * (abs(delta) % num) 28 return (val + num + delta) % num 29 30 31class WindowGroup: 32 33 def __init__(self) -> None: 34 self.windows: List[WindowType] = [] 35 self.id = next(group_id_counter) 36 37 def __len__(self) -> int: 38 return len(self.windows) 39 40 def __bool__(self) -> bool: 41 return bool(self.windows) 42 43 def __iter__(self) -> Iterator[WindowType]: 44 return iter(self.windows) 45 46 def __contains__(self, window: WindowType) -> bool: 47 for w in self.windows: 48 if w is window: 49 return True 50 return False 51 52 @property 53 def needs_attention(self) -> bool: 54 for w in self.windows: 55 if w.needs_attention: 56 return True 57 return False 58 59 @property 60 def base_window_id(self) -> int: 61 return self.windows[0].id if self.windows else 0 62 63 @property 64 def active_window_id(self) -> int: 65 return self.windows[-1].id if self.windows else 0 66 67 def add_window(self, window: WindowType) -> None: 68 self.windows.append(window) 69 70 def remove_window(self, window: WindowType) -> None: 71 with suppress(ValueError): 72 self.windows.remove(window) 73 74 def serialize_state(self) -> Dict[str, Any]: 75 return { 76 'id': self.id, 77 'windows': [w.serialize_state() for w in self.windows] 78 } 79 80 def decoration(self, which: EdgeLiteral, border_mult: int = 1, is_single_window: bool = False) -> int: 81 if not self.windows: 82 return 0 83 w = self.windows[0] 84 return w.effective_margin(which, is_single_window=is_single_window) + w.effective_border() * border_mult + w.effective_padding(which) 85 86 def effective_padding(self, which: EdgeLiteral) -> int: 87 if not self.windows: 88 return 0 89 w = self.windows[0] 90 return w.effective_padding(which) 91 92 def effective_border(self) -> int: 93 if not self.windows: 94 return 0 95 w = self.windows[0] 96 return w.effective_border() 97 98 def set_geometry(self, geom: WindowGeometry) -> None: 99 for w in self.windows: 100 w.set_geometry(geom) 101 102 @property 103 def default_bg(self) -> int: 104 if self.windows: 105 w: WindowType = self.windows[-1] 106 return w.screen.color_profile.default_bg 107 return 0 108 109 @property 110 def geometry(self) -> Optional[WindowGeometry]: 111 if self.windows: 112 w: WindowType = self.windows[-1] 113 return w.geometry 114 115 @property 116 def is_visible_in_layout(self) -> bool: 117 if self.windows: 118 w: WindowType = self.windows[-1] 119 return w.is_visible_in_layout 120 return False 121 122 123class WindowList: 124 125 def __init__(self, tab: TabType) -> None: 126 self.all_windows: List[WindowType] = [] 127 self.id_map: Dict[int, WindowType] = {} 128 self.groups: List[WindowGroup] = [] 129 self._active_group_idx: int = -1 130 self.active_group_history: Deque[int] = deque((), 64) 131 self.tabref = weakref.ref(tab) 132 133 def __len__(self) -> int: 134 return len(self.all_windows) 135 136 def __bool__(self) -> bool: 137 return bool(self.all_windows) 138 139 def __iter__(self) -> Iterator[WindowType]: 140 return iter(self.all_windows) 141 142 def __contains__(self, window: WindowOrId) -> bool: 143 q = window if isinstance(window, int) else window.id 144 return q in self.id_map 145 146 def serialize_state(self) -> Dict[str, Any]: 147 return { 148 'active_group_idx': self.active_group_idx, 149 'active_group_history': list(self.active_group_history), 150 'window_groups': [g.serialize_state() for g in self.groups] 151 } 152 153 @property 154 def active_group_idx(self) -> int: 155 return self._active_group_idx 156 157 @property 158 def active_window_history(self) -> List[int]: 159 ans = [] 160 seen = set() 161 gid_map = {g.id: g for g in self.groups} 162 for gid in self.active_group_history: 163 g = gid_map.get(gid) 164 if g is not None: 165 w = g.active_window_id 166 if w > 0 and w not in seen: 167 seen.add(w) 168 ans.append(w) 169 return ans 170 171 def notify_on_active_window_change(self, old_active_window: Optional[WindowType], new_active_window: Optional[WindowType]) -> None: 172 if old_active_window is not None: 173 old_active_window.focus_changed(False) 174 if new_active_window is not None: 175 new_active_window.focus_changed(True) 176 tab = self.tabref() 177 if tab is not None: 178 tab.active_window_changed() 179 180 def set_active_group_idx(self, i: int, notify: bool = True) -> bool: 181 changed = False 182 if i != self._active_group_idx and 0 <= i < len(self.groups): 183 old_active_window = self.active_window 184 g = self.active_group 185 if g is not None: 186 with suppress(ValueError): 187 self.active_group_history.remove(g.id) 188 self.active_group_history.append(g.id) 189 self._active_group_idx = i 190 new_active_window = self.active_window 191 if old_active_window is not new_active_window: 192 if notify: 193 self.notify_on_active_window_change(old_active_window, new_active_window) 194 changed = True 195 return changed 196 197 def set_active_group(self, group_id: int) -> bool: 198 for i, gr in enumerate(self.groups): 199 if gr.id == group_id: 200 return self.set_active_group_idx(i) 201 202 def change_tab(self, tab: TabType) -> None: 203 self.tabref = weakref.ref(tab) 204 205 def iter_windows_with_visibility(self) -> Generator[Tuple[WindowType, bool], None, None]: 206 for g in self.groups: 207 aw = g.active_window_id 208 for window in g: 209 yield window, window.id == aw 210 211 def iter_all_layoutable_groups(self, only_visible: bool = False) -> Iterator[WindowGroup]: 212 return iter(g for g in self.groups if g.is_visible_in_layout) if only_visible else iter(self.groups) 213 214 def make_previous_group_active(self, which: int = 1, notify: bool = True) -> None: 215 which = max(1, which) 216 gid_map = {g.id: i for i, g in enumerate(self.groups)} 217 num = len(self.active_group_history) 218 for i in range(num): 219 idx = num - i - 1 220 gid = self.active_group_history[idx] 221 x = gid_map.get(gid) 222 if x is not None: 223 which -= 1 224 if which < 1: 225 self.set_active_group_idx(x, notify=notify) 226 return 227 self.set_active_group_idx(len(self.groups) - 1, notify=notify) 228 229 @property 230 def num_groups(self) -> int: 231 return len(self.groups) 232 233 def group_for_window(self, x: WindowOrId) -> Optional[WindowGroup]: 234 q = self.id_map[x] if isinstance(x, int) else x 235 for g in self.groups: 236 if q in g: 237 return g 238 239 def group_idx_for_window(self, x: WindowOrId) -> Optional[int]: 240 q = self.id_map[x] if isinstance(x, int) else x 241 for i, g in enumerate(self.groups): 242 if q in g: 243 return i 244 245 def windows_in_group_of(self, x: WindowOrId) -> Iterator[WindowType]: 246 g = self.group_for_window(x) 247 if g is not None: 248 return iter(g) 249 250 @property 251 def active_group(self) -> Optional[WindowGroup]: 252 with suppress(Exception): 253 return self.groups[self.active_group_idx] 254 255 @property 256 def active_window(self) -> Optional[WindowType]: 257 with suppress(Exception): 258 return self.id_map[self.groups[self.active_group_idx].active_window_id] 259 260 @property 261 def active_group_base(self) -> Optional[WindowType]: 262 with suppress(Exception): 263 return self.id_map[self.groups[self.active_group_idx].base_window_id] 264 265 def set_active_window_group_for(self, x: WindowOrId) -> None: 266 try: 267 q = self.id_map[x] if isinstance(x, int) else x 268 except KeyError: 269 return 270 for i, group in enumerate(self.groups): 271 if q in group: 272 self.set_active_group_idx(i) 273 break 274 275 def add_window( 276 self, 277 window: WindowType, 278 group_of: Optional[WindowOrId] = None, 279 next_to: Optional[WindowOrId] = None, 280 before: bool = False, 281 make_active: bool = True 282 ) -> WindowGroup: 283 self.all_windows.append(window) 284 self.id_map[window.id] = window 285 target_group: Optional[WindowGroup] = None 286 287 if group_of is not None: 288 target_group = self.group_for_window(group_of) 289 if target_group is None and next_to is not None: 290 q = self.id_map[next_to] if isinstance(next_to, int) else next_to 291 pos = -1 292 for i, g in enumerate(self.groups): 293 if q in g: 294 pos = i 295 break 296 if pos > -1: 297 target_group = WindowGroup() 298 self.groups.insert(pos + (0 if before else 1), target_group) 299 if target_group is None: 300 target_group = WindowGroup() 301 if before: 302 self.groups.insert(0, target_group) 303 else: 304 self.groups.append(target_group) 305 306 old_active_window = self.active_window 307 target_group.add_window(window) 308 if make_active: 309 for i, g in enumerate(self.groups): 310 if g is target_group: 311 self.set_active_group_idx(i, notify=False) 312 break 313 new_active_window = self.active_window 314 if new_active_window is not old_active_window: 315 self.notify_on_active_window_change(old_active_window, new_active_window) 316 return target_group 317 318 def remove_window(self, x: WindowOrId) -> None: 319 old_active_window = self.active_window 320 q = self.id_map[x] if isinstance(x, int) else x 321 try: 322 self.all_windows.remove(q) 323 except ValueError: 324 pass 325 self.id_map.pop(q.id, None) 326 for i, g in enumerate(tuple(self.groups)): 327 g.remove_window(q) 328 if not g: 329 del self.groups[i] 330 if self.groups: 331 if self.active_group_idx == i: 332 self.make_previous_group_active(notify=False) 333 elif self.active_group_idx >= len(self.groups): 334 self._active_group_idx -= 1 335 else: 336 self._active_group_idx = -1 337 break 338 new_active_window = self.active_window 339 if old_active_window is not new_active_window: 340 self.notify_on_active_window_change(old_active_window, new_active_window) 341 342 def active_window_in_nth_group(self, n: int, clamp: bool = False) -> Optional[WindowType]: 343 if clamp: 344 n = max(0, min(n, self.num_groups - 1)) 345 if 0 <= n < self.num_groups: 346 return self.id_map.get(self.groups[n].active_window_id) 347 348 def activate_next_window_group(self, delta: int) -> None: 349 self.set_active_group_idx(wrap_increment(self.active_group_idx, self.num_groups, delta)) 350 351 def move_window_group(self, by: Optional[int] = None, to_group: Optional[int] = None) -> bool: 352 if self.active_group_idx < 0 or not self.groups: 353 return False 354 target = -1 355 if by is not None: 356 target = wrap_increment(self.active_group_idx, self.num_groups, by) 357 if to_group is not None: 358 for i, group in enumerate(self.groups): 359 if group.id == to_group: 360 target = i 361 break 362 if target > -1: 363 if target == self.active_group_idx: 364 return False 365 self.groups[self.active_group_idx], self.groups[target] = self.groups[target], self.groups[self.active_group_idx] 366 self.set_active_group_idx(target) 367 return True 368 return False 369 370 def compute_needs_borders_map(self, draw_active_borders: bool) -> Dict[int, bool]: 371 ag = self.active_group 372 return {gr.id: ((gr is ag and draw_active_borders) or gr.needs_attention) for gr in self.groups} 373 374 @property 375 def num_visble_groups(self) -> int: 376 ans = 0 377 for gr in self.groups: 378 if gr.is_visible_in_layout: 379 ans += 1 380 return ans 381