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