396 lines
13 KiB
Python
396 lines
13 KiB
Python
#!/usr/bin/env python
|
|
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
|
|
|
|
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
|