Start work on refactoring management of windows
This commit is contained in:
parent
e39da2b2bb
commit
50d9718c68
@ -36,7 +36,7 @@ from .fast_data_types import (
|
||||
toggle_fullscreen, toggle_maximized
|
||||
)
|
||||
from .keys import get_shortcut, shortcut_matches
|
||||
from .layout import set_layout_options
|
||||
from .layout.base import set_layout_options
|
||||
from .options_stub import Options
|
||||
from .rgb import Color, color_from_int
|
||||
from .session import Session, create_sessions
|
||||
@ -1318,11 +1318,8 @@ class Boss:
|
||||
else:
|
||||
return
|
||||
|
||||
underlaid_window, overlaid_window = src_tab.detach_window(window)
|
||||
if underlaid_window:
|
||||
target_tab.attach_window(underlaid_window)
|
||||
if overlaid_window:
|
||||
target_tab.attach_window(overlaid_window)
|
||||
for detached_window in src_tab.detach_window(window):
|
||||
target_tab.attach_window(detached_window)
|
||||
self._cleanup_tab_after_window_removal(src_tab)
|
||||
target_tab.make_active()
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@ from .conf.utils import (
|
||||
)
|
||||
from .constants import FloatEdges, config_dir, is_macos
|
||||
from .fast_data_types import CURSOR_BEAM, CURSOR_BLOCK, CURSOR_UNDERLINE
|
||||
from .layout import all_layouts
|
||||
from .layout.interface import all_layouts
|
||||
from .rgb import Color, color_as_int, color_as_sharp, color_from_int
|
||||
from .utils import log_error
|
||||
|
||||
|
||||
@ -273,7 +273,7 @@ def launch(boss: Boss, opts: LaunchCLIOptions, args: List[str], target_tab: Opti
|
||||
x = str(active.id)
|
||||
final_cmd.append(x)
|
||||
kw['cmd'] = final_cmd
|
||||
if opts.type == 'overlay' and active and not active.overlay_window_id:
|
||||
if opts.type == 'overlay' and active:
|
||||
kw['overlay_for'] = active.id
|
||||
if opts.stdin_source != 'none':
|
||||
q = str(opts.stdin_source)
|
||||
|
||||
1573
kitty/layout.py
1573
kitty/layout.py
File diff suppressed because it is too large
Load Diff
0
kitty/layout/__init__.py
Normal file
0
kitty/layout/__init__.py
Normal file
552
kitty/layout/base.py
Normal file
552
kitty/layout/base.py
Normal file
@ -0,0 +1,552 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
from functools import partial
|
||||
from itertools import repeat
|
||||
from typing import (
|
||||
Dict, FrozenSet, Generator, Iterable, List, NamedTuple, Optional, Sequence,
|
||||
Tuple, Union, cast
|
||||
)
|
||||
|
||||
from kitty.constants import Edges, WindowGeometry
|
||||
from kitty.fast_data_types import (
|
||||
Region, set_active_window, swap_windows, viewport_for_window
|
||||
)
|
||||
from kitty.options_stub import Options
|
||||
from kitty.typing import TypedDict, WindowType
|
||||
from kitty.window_list import WindowList
|
||||
|
||||
|
||||
class Borders(NamedTuple):
|
||||
left: bool
|
||||
top: bool
|
||||
right: bool
|
||||
bottom: bool
|
||||
|
||||
|
||||
class LayoutOpts:
|
||||
|
||||
def __init__(self, data: Dict[str, str]):
|
||||
pass
|
||||
|
||||
|
||||
class LayoutData(NamedTuple):
|
||||
content_pos: int
|
||||
cells_per_window: int
|
||||
space_before: int
|
||||
space_after: int
|
||||
content_size: int
|
||||
|
||||
|
||||
all_borders = Borders(True, True, True, True)
|
||||
no_borders = Borders(False, False, False, False)
|
||||
DecorationPairs = Sequence[Tuple[int, int]]
|
||||
LayoutDimension = Generator[LayoutData, None, None]
|
||||
ListOfWindows = List[WindowType]
|
||||
|
||||
|
||||
class InternalNeighborsMap(TypedDict):
|
||||
left: List[int]
|
||||
top: List[int]
|
||||
right: List[int]
|
||||
bottom: List[int]
|
||||
|
||||
|
||||
class NeighborsMap(TypedDict):
|
||||
left: Tuple[int, ...]
|
||||
top: Tuple[int, ...]
|
||||
right: Tuple[int, ...]
|
||||
bottom: Tuple[int, ...]
|
||||
|
||||
|
||||
class LayoutGlobalData:
|
||||
draw_minimal_borders: bool = True
|
||||
draw_active_borders: bool = True
|
||||
align_top_left: bool = False
|
||||
|
||||
central: Region = Region((0, 0, 199, 199, 200, 200))
|
||||
cell_width: int = 20
|
||||
cell_height: int = 20
|
||||
|
||||
|
||||
lgd = LayoutGlobalData()
|
||||
|
||||
|
||||
def idx_for_id(win_id: int, windows: Iterable[WindowType]) -> Optional[int]:
|
||||
for i, w in enumerate(windows):
|
||||
if w.id == win_id:
|
||||
return i
|
||||
|
||||
|
||||
def set_layout_options(opts: Options) -> None:
|
||||
lgd.draw_minimal_borders = opts.draw_minimal_borders and sum(opts.window_margin_width) == 0
|
||||
lgd.draw_active_borders = opts.active_border_color is not None
|
||||
lgd.align_top_left = opts.placement_strategy == 'top-left'
|
||||
|
||||
|
||||
def calculate_cells_map(bias: Optional[Sequence[float]], number_of_windows: int, number_of_cells: int) -> List[int]:
|
||||
cells_per_window = number_of_cells // number_of_windows
|
||||
if bias is not None and 1 < number_of_windows == len(bias) and cells_per_window > 5:
|
||||
cells_map = [int(b * number_of_cells) for b in bias]
|
||||
while min(cells_map) < 5:
|
||||
maxi, mini = map(cells_map.index, (max(cells_map), min(cells_map)))
|
||||
if maxi == mini:
|
||||
break
|
||||
cells_map[mini] += 1
|
||||
cells_map[maxi] -= 1
|
||||
else:
|
||||
cells_map = list(repeat(cells_per_window, number_of_windows))
|
||||
extra = number_of_cells - sum(cells_map)
|
||||
if extra > 0:
|
||||
cells_map[-1] += extra
|
||||
return cells_map
|
||||
|
||||
|
||||
def layout_dimension(
|
||||
start_at: int, length: int, cell_length: int,
|
||||
decoration_pairs: DecorationPairs,
|
||||
left_align: bool = False,
|
||||
bias: Optional[Sequence[float]] = None
|
||||
) -> LayoutDimension:
|
||||
number_of_windows = len(decoration_pairs)
|
||||
number_of_cells = length // cell_length
|
||||
space_needed_for_decorations: int = sum(map(sum, decoration_pairs))
|
||||
extra = length - number_of_cells * cell_length
|
||||
while extra < space_needed_for_decorations:
|
||||
number_of_cells -= 1
|
||||
extra = length - number_of_cells * cell_length
|
||||
cells_map = calculate_cells_map(bias, number_of_windows, number_of_cells)
|
||||
assert sum(cells_map) == number_of_cells
|
||||
|
||||
extra = length - number_of_cells * cell_length - space_needed_for_decorations
|
||||
pos = start_at
|
||||
if not left_align:
|
||||
pos += extra // 2
|
||||
last_i = len(cells_map) - 1
|
||||
|
||||
for i, cells_per_window in enumerate(cells_map):
|
||||
before_dec, after_dec = decoration_pairs[i]
|
||||
pos += before_dec
|
||||
if i == 0:
|
||||
before_space = pos - start_at
|
||||
else:
|
||||
before_space = before_dec
|
||||
content_size = cells_per_window * cell_length
|
||||
if i == last_i:
|
||||
after_space = (start_at + length) - (pos + content_size)
|
||||
else:
|
||||
after_space = after_dec
|
||||
yield LayoutData(pos, cells_per_window, before_space, after_space, content_size)
|
||||
pos += content_size + after_space
|
||||
|
||||
|
||||
class Rect(NamedTuple):
|
||||
left: int
|
||||
top: int
|
||||
right: int
|
||||
bottom: int
|
||||
|
||||
|
||||
def blank_rects_for_window(wg: WindowGeometry) -> Generator[Rect, None, None]:
|
||||
left_width, right_width = wg.spaces.left, wg.spaces.right
|
||||
top_height, bottom_height = wg.spaces.top, wg.spaces.bottom
|
||||
if left_width > 0:
|
||||
yield Rect(wg.left - left_width, wg.top - top_height, wg.left, wg.bottom + bottom_height)
|
||||
if top_height > 0:
|
||||
yield Rect(wg.left, wg.top - top_height, wg.right + right_width, wg.top)
|
||||
if right_width > 0:
|
||||
yield Rect(wg.right, wg.top, wg.right + right_width, wg.bottom + bottom_height)
|
||||
if bottom_height > 0:
|
||||
yield Rect(wg.left, wg.bottom, wg.right, wg.bottom + bottom_height)
|
||||
|
||||
|
||||
def window_geometry(xstart: int, xnum: int, ystart: int, ynum: int, left: int, top: int, right: int, bottom: int) -> WindowGeometry:
|
||||
return WindowGeometry(
|
||||
left=xstart, top=ystart, xnum=xnum, ynum=ynum,
|
||||
right=xstart + lgd.cell_width * xnum, bottom=ystart + lgd.cell_height * ynum,
|
||||
spaces=Edges(left, top, right, bottom)
|
||||
)
|
||||
|
||||
|
||||
def window_geometry_from_layouts(x: LayoutData, y: LayoutData) -> WindowGeometry:
|
||||
return window_geometry(x.content_pos, x.cells_per_window, y.content_pos, y.cells_per_window, x.space_before, y.space_before, x.space_after, y.space_after)
|
||||
|
||||
|
||||
def layout_single_window(xdecoration_pairs: DecorationPairs, ydecoration_pairs: DecorationPairs, left_align: bool = False) -> WindowGeometry:
|
||||
x = next(layout_dimension(lgd.central.left, lgd.central.width, lgd.cell_width, xdecoration_pairs, left_align=lgd.align_top_left))
|
||||
y = next(layout_dimension(lgd.central.top, lgd.central.height, lgd.cell_height, ydecoration_pairs, left_align=lgd.align_top_left))
|
||||
return window_geometry_from_layouts(x, y)
|
||||
|
||||
|
||||
def safe_increment_bias(old_val: float, increment: float) -> float:
|
||||
return max(0.1, min(old_val + increment, 0.9))
|
||||
|
||||
|
||||
def normalize_biases(biases: List[float]) -> List[float]:
|
||||
s = sum(biases)
|
||||
if s == 1:
|
||||
return biases
|
||||
return [x/s for x in biases]
|
||||
|
||||
|
||||
def distribute_indexed_bias(base_bias: Sequence[float], index_bias_map: Dict[int, float]) -> Sequence[float]:
|
||||
if not index_bias_map:
|
||||
return base_bias
|
||||
ans = list(base_bias)
|
||||
limit = len(ans)
|
||||
for row, increment in index_bias_map.items():
|
||||
if row >= limit or not increment:
|
||||
continue
|
||||
other_increment = -increment / (limit - 1)
|
||||
ans = [safe_increment_bias(b, increment if i == row else other_increment) for i, b in enumerate(ans)]
|
||||
return normalize_biases(ans)
|
||||
|
||||
|
||||
def variable_bias(num_windows: int, candidate: Dict[int, float]) -> Sequence[float]:
|
||||
return distribute_indexed_bias(list(repeat(1/(num_windows), num_windows)), candidate)
|
||||
|
||||
|
||||
class Layout:
|
||||
|
||||
name: Optional[str] = None
|
||||
needs_window_borders = True
|
||||
must_draw_borders = False # can be overridden to customize behavior from kittens
|
||||
needs_all_windows = False
|
||||
layout_opts = LayoutOpts({})
|
||||
only_active_window_visible = False
|
||||
|
||||
def __init__(self, os_window_id: int, tab_id: int, layout_opts: str = '') -> None:
|
||||
self.os_window_id = os_window_id
|
||||
self.tab_id = tab_id
|
||||
self.set_active_window_in_os_window = partial(set_active_window, os_window_id, tab_id)
|
||||
self.swap_windows_in_os_window = partial(swap_windows, os_window_id, tab_id)
|
||||
# A set of rectangles corresponding to the blank spaces at the edges of
|
||||
# this layout, i.e. spaces that are not covered by any window
|
||||
self.blank_rects: List[Rect] = []
|
||||
self.layout_opts = self.parse_layout_opts(layout_opts)
|
||||
assert self.name is not None
|
||||
self.full_name = self.name + ((':' + layout_opts) if layout_opts else '')
|
||||
self.remove_all_biases()
|
||||
|
||||
def bias_increment_for_cell(self, is_horizontal: bool) -> float:
|
||||
self._set_dimensions()
|
||||
if is_horizontal:
|
||||
return (lgd.cell_width + 1) / lgd.central.width
|
||||
return (lgd.cell_height + 1) / lgd.central.height
|
||||
|
||||
def apply_bias(self, idx: int, increment: float, top_level_windows: ListOfWindows, is_horizontal: bool = True) -> bool:
|
||||
return False
|
||||
|
||||
def remove_all_biases(self) -> bool:
|
||||
return False
|
||||
|
||||
def modify_size_of_window(self, all_windows: WindowList, window_id: int, increment: float, is_horizontal: bool = True) -> bool:
|
||||
idx = all_windows.idx_for_window(window_id)
|
||||
if idx is None:
|
||||
return False
|
||||
return self.apply_bias(idx, increment, list(all_windows.iter_top_level_windows()), is_horizontal)
|
||||
|
||||
def parse_layout_opts(self, layout_opts: Optional[str] = None) -> LayoutOpts:
|
||||
data: Dict[str, str] = {}
|
||||
if layout_opts:
|
||||
for x in layout_opts.split(';'):
|
||||
k, v = x.partition('=')[::2]
|
||||
if k and v:
|
||||
data[k] = v
|
||||
return type(self.layout_opts)(data)
|
||||
|
||||
def nth_window(self, all_windows: WindowList, num: int) -> Optional[WindowType]:
|
||||
return all_windows.active_window_for_idx(num, clamp=True)
|
||||
|
||||
def activate_nth_window(self, all_windows: WindowList, num: int) -> int:
|
||||
w = self.nth_window(all_windows, num)
|
||||
assert w is not None
|
||||
active_window_idx = all_windows.idx_for_window(w)
|
||||
assert active_window_idx is not None
|
||||
return self.set_active_window(all_windows, active_window_idx)
|
||||
|
||||
def next_window(self, all_windows: WindowList, active_window_idx: int, delta: int = 1) -> int:
|
||||
w = self.nth_window(all_windows, active_window_idx)
|
||||
assert w is not None
|
||||
idx = all_windows.idx_for_window(w)
|
||||
assert idx is not None
|
||||
num_slots = all_windows.max_active_idx + 1
|
||||
aidx = (idx + num_slots + delta) % num_slots
|
||||
return self.set_active_window(all_windows, aidx)
|
||||
|
||||
def neighbors(self, all_windows: WindowList, active_window_idx: int) -> NeighborsMap:
|
||||
w = all_windows.active_window_for_idx(active_window_idx)
|
||||
assert w is not None
|
||||
n = self.neighbors_for_window(w, all_windows)
|
||||
|
||||
def as_indices(windows: Iterable[int]) -> Generator[int, None, None]:
|
||||
for w in windows:
|
||||
idx = all_windows.idx_for_window(w)
|
||||
if idx is not None:
|
||||
yield idx
|
||||
|
||||
ans: NeighborsMap = {
|
||||
'left': tuple(as_indices(n['left'])),
|
||||
'top': tuple(as_indices(n['top'])),
|
||||
'right': tuple(as_indices(n['right'])),
|
||||
'bottom': tuple(as_indices(n['bottom']))
|
||||
}
|
||||
return ans
|
||||
|
||||
def move_window(self, all_windows: WindowList, active_window_idx: int, delta: Union[str, int] = 1) -> int:
|
||||
# delta can be either a number or a string such as 'left', 'top', etc
|
||||
# for neighborhood moves
|
||||
if len(windows) < 2 or not delta:
|
||||
return active_window_idx
|
||||
wgd = WindowGroupingData(all_windows)
|
||||
w = wgd.base_window_for_idx(active_window_idx)
|
||||
if w is None:
|
||||
return active_window_idx
|
||||
|
||||
idx = idx_for_id(w.id, windows)
|
||||
if idx is None and w.overlay_window_id is not None:
|
||||
idx = idx_for_id(w.overlay_window_id, windows)
|
||||
assert idx is not None
|
||||
if isinstance(delta, int):
|
||||
nidx = (idx + len(windows) + delta) % len(windows)
|
||||
else:
|
||||
delta = delta.lower()
|
||||
delta = {'up': 'top', 'down': 'bottom'}.get(delta, delta)
|
||||
neighbors = self.neighbors_for_window(w, all_windows if self.needs_all_windows else windows)
|
||||
q = cast(WindowList, neighbors.get(cast(str, delta), ()))
|
||||
if not q:
|
||||
return active_window_idx
|
||||
w = q[0]
|
||||
qidx = idx_for_id(getattr(w, 'id', w), windows)
|
||||
assert qidx is not None
|
||||
nidx = qidx
|
||||
|
||||
nw = windows[nidx]
|
||||
qidx = idx_for_id(nw.id, all_windows)
|
||||
assert qidx is not None
|
||||
nidx = qidx
|
||||
idx = active_window_idx
|
||||
self.swap_windows_in_layout(all_windows, nidx, idx)
|
||||
self.swap_windows_in_os_window(nidx, idx)
|
||||
return self.set_active_window(all_windows, nidx)
|
||||
|
||||
def swap_windows_in_layout(self, all_windows: WindowList, a: int, b: int) -> None:
|
||||
all_windows[a], all_windows[b] = all_windows[b], all_windows[a]
|
||||
|
||||
def add_window(self, all_windows: WindowList, window: WindowType, current_active_window_idx: int, location: Optional[str] = None) -> int:
|
||||
active_window_idx = None
|
||||
if window.overlay_for is not None:
|
||||
i = idx_for_id(window.overlay_for, all_windows)
|
||||
if i is not None:
|
||||
# put the overlay window in the position occupied by the
|
||||
# overlaid window and move the overlaid window to the end
|
||||
self.swap_windows_in_os_window(len(all_windows), i)
|
||||
all_windows.append(all_windows[i])
|
||||
all_windows[i] = window
|
||||
active_window_idx = i
|
||||
if active_window_idx is None:
|
||||
if location == 'neighbor':
|
||||
location = 'after'
|
||||
active_window_idx = self.do_add_window(all_windows, window, current_active_window_idx, location)
|
||||
|
||||
self(all_windows, active_window_idx)
|
||||
self.set_active_window_in_os_window(active_window_idx)
|
||||
return active_window_idx
|
||||
|
||||
def do_add_window(self, all_windows: WindowList, window: WindowType, current_active_window_idx: Optional[int], location: Optional[str]) -> int:
|
||||
active_window_idx = None
|
||||
if location is not None:
|
||||
if location in ('after', 'vsplit', 'hsplit') and current_active_window_idx is not None and len(all_windows) > 1:
|
||||
active_window_idx = min(current_active_window_idx + 1, len(all_windows))
|
||||
elif location == 'before' and current_active_window_idx is not None and len(all_windows) > 1:
|
||||
active_window_idx = current_active_window_idx
|
||||
elif location == 'first':
|
||||
active_window_idx = 0
|
||||
if active_window_idx is not None:
|
||||
for i in range(len(all_windows), active_window_idx, -1):
|
||||
self.swap_windows_in_os_window(i, i - 1)
|
||||
all_windows.insert(active_window_idx, window)
|
||||
|
||||
if active_window_idx is None:
|
||||
active_window_idx = len(all_windows)
|
||||
all_windows.append(window)
|
||||
return active_window_idx
|
||||
|
||||
def remove_window(self, all_windows: WindowList, window: WindowType, current_active_window_idx: int, swapped: bool = False) -> int:
|
||||
try:
|
||||
active_window = all_windows[current_active_window_idx]
|
||||
except Exception:
|
||||
active_window = window
|
||||
if not swapped and window.overlay_for is not None:
|
||||
nidx = idx_for_id(window.overlay_for, all_windows)
|
||||
if nidx is not None:
|
||||
idx = all_windows.index(window)
|
||||
all_windows[nidx], all_windows[idx] = all_windows[idx], all_windows[nidx]
|
||||
self.swap_windows_in_os_window(nidx, idx)
|
||||
return self.remove_window(all_windows, window, current_active_window_idx, swapped=True)
|
||||
|
||||
position = all_windows.index(window)
|
||||
del all_windows[position]
|
||||
active_window_idx = None
|
||||
if window.overlay_for is not None:
|
||||
i = idx_for_id(window.overlay_for, all_windows)
|
||||
if i is not None:
|
||||
overlaid_window = all_windows[i]
|
||||
overlaid_window.overlay_window_id = None
|
||||
if active_window is window:
|
||||
active_window = overlaid_window
|
||||
active_window_idx = idx_for_id(active_window.id, all_windows)
|
||||
if active_window_idx is None:
|
||||
if active_window is window:
|
||||
active_window_idx = max(0, min(current_active_window_idx, len(all_windows) - 1))
|
||||
else:
|
||||
active_window_idx = idx_for_id(active_window.id, all_windows)
|
||||
assert active_window_idx is not None
|
||||
if all_windows:
|
||||
self(all_windows, active_window_idx)
|
||||
return self.set_active_window(all_windows, active_window_idx)
|
||||
|
||||
def update_visibility(self, all_windows: WindowList, active_window: WindowType, overlaid_windows: Optional[FrozenSet[WindowType]] = None) -> None:
|
||||
if overlaid_windows is None:
|
||||
overlaid_windows = process_overlaid_windows(all_windows)[0]
|
||||
for i, w in enumerate(all_windows):
|
||||
w.set_visible_in_layout(i, w is active_window or (not self.only_active_window_visible and w not in overlaid_windows))
|
||||
|
||||
def set_active_window(self, all_windows: WindowList, active_window_idx: int) -> int:
|
||||
if not all_windows:
|
||||
self.set_active_window_in_os_window(0)
|
||||
return 0
|
||||
w = all_windows[active_window_idx]
|
||||
if w.overlay_window_id is not None:
|
||||
i = idx_for_id(w.overlay_window_id, all_windows)
|
||||
if i is not None:
|
||||
active_window_idx = i
|
||||
self.update_visibility(all_windows, all_windows[active_window_idx])
|
||||
self.set_active_window_in_os_window(active_window_idx)
|
||||
return active_window_idx
|
||||
|
||||
def _set_dimensions(self) -> None:
|
||||
lgd.central, tab_bar, vw, vh, lgd.cell_width, lgd.cell_height = viewport_for_window(self.os_window_id)
|
||||
|
||||
def __call__(self, all_windows: WindowList, active_window_idx: int) -> int:
|
||||
self._set_dimensions()
|
||||
active_window = all_windows[active_window_idx]
|
||||
overlaid_windows, windows = process_overlaid_windows(all_windows)
|
||||
if overlaid_windows:
|
||||
windows = [w for w in all_windows if w not in overlaid_windows]
|
||||
q = idx_for_id(active_window.id, windows)
|
||||
if q is None:
|
||||
if active_window.overlay_window_id is not None:
|
||||
active_window_idx = idx_for_id(active_window.overlay_window_id, windows) or 0
|
||||
else:
|
||||
active_window_idx = 0
|
||||
else:
|
||||
active_window_idx = q
|
||||
active_window = windows[active_window_idx]
|
||||
else:
|
||||
windows = all_windows
|
||||
self.update_visibility(all_windows, active_window, overlaid_windows)
|
||||
self.blank_rects = []
|
||||
if self.needs_all_windows:
|
||||
self.do_layout_all_windows(windows, active_window_idx, all_windows)
|
||||
else:
|
||||
self.do_layout(windows, active_window_idx)
|
||||
return cast(int, idx_for_id(active_window.id, all_windows))
|
||||
|
||||
# Utils {{{
|
||||
|
||||
def layout_single_window(self, w: WindowType, return_geometry: bool = False, left_align: bool = False) -> Optional[WindowGeometry]:
|
||||
bw = w.effective_border() if self.must_draw_borders else 0
|
||||
xdecoration_pairs = ((
|
||||
w.effective_padding('left') + w.effective_margin('left', is_single_window=True) + bw,
|
||||
w.effective_padding('right') + w.effective_margin('right', is_single_window=True) + bw,
|
||||
),)
|
||||
ydecoration_pairs = ((
|
||||
w.effective_padding('top') + w.effective_margin('top', is_single_window=True) + bw,
|
||||
w.effective_padding('bottom') + w.effective_margin('bottom', is_single_window=True) + bw,
|
||||
),)
|
||||
wg = layout_single_window(xdecoration_pairs, ydecoration_pairs, left_align=left_align)
|
||||
if return_geometry:
|
||||
return wg
|
||||
w.set_geometry(0, wg)
|
||||
self.blank_rects = list(blank_rects_for_window(wg))
|
||||
return None
|
||||
|
||||
def xlayout(
|
||||
self,
|
||||
windows: WindowList,
|
||||
bias: Optional[Sequence[float]] = None,
|
||||
start: Optional[int] = None,
|
||||
size: Optional[int] = None
|
||||
) -> LayoutDimension:
|
||||
decoration_pairs = tuple(
|
||||
(
|
||||
w.effective_margin('left') + w.effective_border() + w.effective_padding('left'),
|
||||
w.effective_margin('right') + w.effective_border() + w.effective_padding('right'),
|
||||
) for w in windows
|
||||
)
|
||||
if start is None:
|
||||
start = lgd.central.left
|
||||
if size is None:
|
||||
size = lgd.central.width
|
||||
return layout_dimension(start, size, lgd.cell_width, decoration_pairs, bias=bias, left_align=lgd.align_top_left)
|
||||
|
||||
def ylayout(
|
||||
self,
|
||||
windows: WindowList,
|
||||
bias: Optional[Sequence[float]] = None,
|
||||
start: Optional[int] = None,
|
||||
size: Optional[int] = None
|
||||
) -> LayoutDimension:
|
||||
decoration_pairs = tuple(
|
||||
(
|
||||
w.effective_margin('top') + w.effective_border() + w.effective_padding('top'),
|
||||
w.effective_margin('bottom') + w.effective_border() + w.effective_padding('bottom'),
|
||||
) for w in windows
|
||||
)
|
||||
if start is None:
|
||||
start = lgd.central.top
|
||||
if size is None:
|
||||
size = lgd.central.height
|
||||
return layout_dimension(start, size, lgd.cell_height, decoration_pairs, bias=bias, left_align=lgd.align_top_left)
|
||||
|
||||
def set_window_geometry(self, w: WindowType, idx: int, xl: LayoutData, yl: LayoutData) -> None:
|
||||
wg = window_geometry_from_layouts(xl, yl)
|
||||
w.set_geometry(idx, wg)
|
||||
self.blank_rects.extend(blank_rects_for_window(wg))
|
||||
|
||||
# }}}
|
||||
|
||||
def do_layout(self, windows: WindowList, active_window_idx: int) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def do_layout_all_windows(self, windows: WindowList, active_window_idx: int, all_windows: WindowList) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def neighbors_for_window(self, window: WindowType, windows: WindowList) -> InternalNeighborsMap:
|
||||
return {'left': [], 'right': [], 'top': [], 'bottom': []}
|
||||
|
||||
def compute_needs_borders_map(self, windows: WindowList, active_window: Optional[WindowType]) -> Dict[int, bool]:
|
||||
return {w.id: ((w is active_window and lgd.draw_active_borders) or w.needs_attention) for w in windows}
|
||||
|
||||
def resolve_borders(self, windows: WindowList, active_window: Optional[WindowType]) -> Generator[Borders, None, None]:
|
||||
if lgd.draw_minimal_borders:
|
||||
needs_borders_map = self.compute_needs_borders_map(windows, active_window)
|
||||
yield from self.minimal_borders(windows, active_window, needs_borders_map)
|
||||
else:
|
||||
yield from Layout.minimal_borders(self, windows, active_window, {})
|
||||
|
||||
def window_independent_borders(self, windows: WindowList, active_window: Optional[WindowType] = None) -> Generator[Edges, None, None]:
|
||||
return
|
||||
yield Edges() # type: ignore
|
||||
|
||||
def minimal_borders(self, windows: WindowList, active_window: Optional[WindowType], needs_borders_map: Dict[int, bool]) -> Generator[Borders, None, None]:
|
||||
for w in windows:
|
||||
if w is not active_window or lgd.draw_active_borders or w.needs_attention:
|
||||
yield all_borders
|
||||
else:
|
||||
yield no_borders
|
||||
|
||||
def layout_action(self, action_name: str, args: Sequence[str], all_windows: WindowList, active_window_idx: int) -> Optional[Union[bool, int]]:
|
||||
pass
|
||||
253
kitty/layout/grid.py
Normal file
253
kitty/layout/grid.py
Normal file
@ -0,0 +1,253 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
from functools import lru_cache
|
||||
from itertools import repeat
|
||||
from typing import Callable, Dict, Generator, List, Optional, Sequence, Tuple
|
||||
|
||||
from kitty.constants import Edges
|
||||
from kitty.typing import WindowType
|
||||
from kitty.window_list import WindowList
|
||||
|
||||
from .base import (
|
||||
Borders, InternalNeighborsMap, Layout, LayoutData, LayoutDimension,
|
||||
ListOfWindows, all_borders, layout_dimension, lgd, no_borders,
|
||||
variable_bias
|
||||
)
|
||||
from .tall import neighbors_for_tall_window
|
||||
|
||||
|
||||
@lru_cache()
|
||||
def calc_grid_size(n: int) -> Tuple[int, int, int, int]:
|
||||
if n <= 5:
|
||||
ncols = 1 if n == 1 else 2
|
||||
else:
|
||||
for ncols in range(3, (n // 2) + 1):
|
||||
if ncols * ncols >= n:
|
||||
break
|
||||
nrows = n // ncols
|
||||
special_rows = n - (nrows * (ncols - 1))
|
||||
special_col = 0 if special_rows < nrows else ncols - 1
|
||||
return ncols, nrows, special_rows, special_col
|
||||
|
||||
|
||||
class Grid(Layout):
|
||||
|
||||
name = 'grid'
|
||||
|
||||
def remove_all_biases(self) -> bool:
|
||||
self.biased_rows: Dict[int, float] = {}
|
||||
self.biased_cols: Dict[int, float] = {}
|
||||
return True
|
||||
|
||||
def column_layout(
|
||||
self,
|
||||
num: int,
|
||||
bias: Optional[Sequence[float]] = None,
|
||||
) -> LayoutDimension:
|
||||
decoration_pairs = tuple(repeat((0, 0), num))
|
||||
return layout_dimension(lgd.central.left, lgd.central.width, lgd.cell_width, decoration_pairs, bias=bias, left_align=lgd.align_top_left)
|
||||
|
||||
def row_layout(
|
||||
self,
|
||||
num: int,
|
||||
bias: Optional[Sequence[float]] = None,
|
||||
) -> LayoutDimension:
|
||||
decoration_pairs = tuple(repeat((0, 0), num))
|
||||
return layout_dimension(lgd.central.top, lgd.central.height, lgd.cell_height, decoration_pairs, bias=bias, left_align=lgd.align_top_left)
|
||||
|
||||
def variable_layout(self, layout_func: Callable[..., LayoutDimension], num_windows: int, biased_map: Dict[int, float]) -> LayoutDimension:
|
||||
return layout_func(num_windows, bias=variable_bias(num_windows, biased_map) if num_windows > 1 else None)
|
||||
|
||||
def apply_bias(self, idx: int, increment: float, top_level_windows: ListOfWindows, is_horizontal: bool = True) -> bool:
|
||||
b = self.biased_cols if is_horizontal else self.biased_rows
|
||||
num_windows = len(top_level_windows)
|
||||
ncols, nrows, special_rows, special_col = calc_grid_size(num_windows)
|
||||
|
||||
def position_for_window_idx(idx: int) -> Tuple[int, int]:
|
||||
row_num = col_num = 0
|
||||
|
||||
def on_col_done(col_windows: List[int]) -> None:
|
||||
nonlocal col_num, row_num
|
||||
row_num = 0
|
||||
col_num += 1
|
||||
|
||||
for window_idx, xl, yl in self.layout_windows(
|
||||
num_windows, nrows, ncols, special_rows, special_col, on_col_done):
|
||||
if idx == window_idx:
|
||||
return row_num, col_num
|
||||
row_num += 1
|
||||
|
||||
row_num, col_num = position_for_window_idx(idx)
|
||||
|
||||
if is_horizontal:
|
||||
b = self.biased_cols
|
||||
if ncols < 2:
|
||||
return False
|
||||
bias_idx = col_num
|
||||
attr = 'biased_cols'
|
||||
|
||||
def layout_func(windows: ListOfWindows, bias: Optional[Sequence[float]] = None) -> LayoutDimension:
|
||||
return self.column_layout(num_windows, bias=bias)
|
||||
|
||||
else:
|
||||
b = self.biased_rows
|
||||
if max(nrows, special_rows) < 2:
|
||||
return False
|
||||
bias_idx = row_num
|
||||
attr = 'biased_rows'
|
||||
|
||||
def layout_func(windows: ListOfWindows, bias: Optional[Sequence[float]] = None) -> LayoutDimension:
|
||||
return self.row_layout(num_windows, bias=bias)
|
||||
|
||||
before_layout = list(self.variable_layout(layout_func, num_windows, b))
|
||||
candidate = b.copy()
|
||||
before = candidate.get(bias_idx, 0)
|
||||
candidate[bias_idx] = before + increment
|
||||
if before_layout == list(self.variable_layout(layout_func, num_windows, candidate)):
|
||||
return False
|
||||
setattr(self, attr, candidate)
|
||||
return True
|
||||
|
||||
def layout_windows(
|
||||
self,
|
||||
num_windows: int,
|
||||
nrows: int, ncols: int,
|
||||
special_rows: int, special_col: int,
|
||||
on_col_done: Callable[[List[int]], None] = lambda col_windows: None
|
||||
) -> Generator[Tuple[int, LayoutData, LayoutData], None, None]:
|
||||
# Distribute windows top-to-bottom, left-to-right (i.e. in columns)
|
||||
xlayout = self.variable_layout(self.column_layout, ncols, self.biased_cols)
|
||||
yvals_normal = tuple(self.variable_layout(self.row_layout, nrows, self.biased_rows))
|
||||
yvals_special = yvals_normal if special_rows == nrows else tuple(self.variable_layout(self.row_layout, special_rows, self.biased_rows))
|
||||
pos = 0
|
||||
for col in range(ncols):
|
||||
rows = special_rows if col == special_col else nrows
|
||||
yls = yvals_special if col == special_col else yvals_normal
|
||||
xl = next(xlayout)
|
||||
col_windows = []
|
||||
for i, yl in enumerate(yls):
|
||||
window_idx = pos + i
|
||||
yield window_idx, xl, yl
|
||||
col_windows.append(window_idx)
|
||||
pos += rows
|
||||
on_col_done(col_windows)
|
||||
|
||||
def do_layout(self, windows: WindowList, active_window_idx: int) -> None:
|
||||
n = len(windows)
|
||||
if n == 1:
|
||||
self.layout_single_window(windows[0])
|
||||
return
|
||||
ncols, nrows, special_rows, special_col = calc_grid_size(n)
|
||||
|
||||
win_col_map = []
|
||||
|
||||
def on_col_done(col_windows: List[int]) -> None:
|
||||
col_windows_w = [windows[i] for i in col_windows]
|
||||
win_col_map.append(col_windows_w)
|
||||
|
||||
def extents(ld: LayoutData) -> Tuple[int, int]:
|
||||
start = ld.content_pos - ld.space_before
|
||||
size = ld.space_before + ld.space_after + ld.content_size
|
||||
return start, size
|
||||
|
||||
def layout(ld: LayoutData, cell_length: int, before_dec: int, after_dec: int) -> LayoutData:
|
||||
start, size = extents(ld)
|
||||
space_needed_for_decorations = before_dec + after_dec
|
||||
content_size = size - space_needed_for_decorations
|
||||
number_of_cells = content_size // cell_length
|
||||
cell_area = number_of_cells * cell_length
|
||||
extra = content_size - cell_area
|
||||
if extra > 0 and not lgd.align_top_left:
|
||||
before_dec += extra // 2
|
||||
return LayoutData(start + before_dec, number_of_cells, before_dec, size - cell_area - before_dec, cell_area)
|
||||
|
||||
def position_window_in_grid_cell(window_idx: int, xl: LayoutData, yl: LayoutData) -> None:
|
||||
w = windows[window_idx]
|
||||
bw = w.effective_border()
|
||||
edges = Edges(
|
||||
w.effective_margin('left') + w.effective_padding('left') + bw,
|
||||
w.effective_margin('right') + w.effective_padding('right') + bw,
|
||||
w.effective_margin('top') + w.effective_padding('top') + bw,
|
||||
w.effective_margin('bottom') + w.effective_padding('bottom') + bw,
|
||||
)
|
||||
xl = layout(xl, lgd.cell_width, edges.left, edges.right)
|
||||
yl = layout(yl, lgd.cell_height, edges.top, edges.bottom)
|
||||
self.set_window_geometry(w, window_idx, xl, yl)
|
||||
|
||||
for window_idx, xl, yl in self.layout_windows(
|
||||
n, nrows, ncols, special_rows, special_col, on_col_done):
|
||||
position_window_in_grid_cell(window_idx, xl, yl)
|
||||
|
||||
def minimal_borders(self, windows: WindowList, active_window: Optional[WindowType], needs_borders_map: Dict[int, bool]) -> Generator[Borders, None, None]:
|
||||
for w in windows:
|
||||
if needs_borders_map[w.id]:
|
||||
yield all_borders
|
||||
else:
|
||||
yield no_borders
|
||||
|
||||
def window_independent_borders(self, windows: WindowList, active_window: Optional[WindowType] = None) -> Generator[Edges, None, None]:
|
||||
n = len(windows)
|
||||
if not lgd.draw_minimal_borders or n < 2:
|
||||
return
|
||||
ncols, nrows, special_rows, special_col = calc_grid_size(n)
|
||||
row_borders: List[List[Edges]] = [[]]
|
||||
col_borders: List[Edges] = []
|
||||
bw = windows[0].effective_border()
|
||||
|
||||
def on_col_done(col_windows: List[int]) -> None:
|
||||
left = xl.content_pos + xl.content_size + xl.space_after - bw // 2
|
||||
col_borders.append(Edges(left, lgd.central.top, left + bw, lgd.central.bottom))
|
||||
row_borders.append([])
|
||||
|
||||
for window_idx, xl, yl in self.layout_windows(n, nrows, ncols, special_rows, special_col, on_col_done):
|
||||
top = yl.content_pos + yl.content_size + yl.space_after - bw // 2
|
||||
right = xl.content_pos + xl.content_size + xl.space_after
|
||||
row_borders[-1].append(Edges(xl.content_pos - xl.space_before, top, right, top + bw))
|
||||
|
||||
for border in col_borders[:-1]:
|
||||
yield border
|
||||
|
||||
for rows in row_borders:
|
||||
for border in rows[:-1]:
|
||||
yield border
|
||||
|
||||
def neighbors_for_window(self, window: WindowType, windows: WindowList) -> InternalNeighborsMap:
|
||||
n = len(windows)
|
||||
if n < 4:
|
||||
return neighbors_for_tall_window(1, window, windows)
|
||||
ncols, nrows, special_rows, special_col = calc_grid_size(n)
|
||||
blank_row: List[Optional[int]] = [None for i in range(ncols)]
|
||||
matrix = tuple(blank_row[:] for j in range(max(nrows, special_rows)))
|
||||
wi = iter(windows)
|
||||
pos_map: Dict[int, Tuple[int, int]] = {}
|
||||
col_counts: List[int] = []
|
||||
for col in range(ncols):
|
||||
rows = special_rows if col == special_col else nrows
|
||||
for row in range(rows):
|
||||
w = next(wi)
|
||||
matrix[row][col] = wid = w.id
|
||||
pos_map[wid] = row, col
|
||||
col_counts.append(rows)
|
||||
row, col = pos_map[window.id]
|
||||
|
||||
def neighbors(row: int, col: int) -> List[int]:
|
||||
try:
|
||||
ans = matrix[row][col]
|
||||
except IndexError:
|
||||
ans = None
|
||||
return [] if ans is None else [ans]
|
||||
|
||||
def side(row: int, col: int, delta: int) -> List[int]:
|
||||
neighbor_col = col + delta
|
||||
if col_counts[neighbor_col] == col_counts[col]:
|
||||
return neighbors(row, neighbor_col)
|
||||
return neighbors(min(row, col_counts[neighbor_col] - 1), neighbor_col)
|
||||
|
||||
return {
|
||||
'top': neighbors(row-1, col) if row else [],
|
||||
'bottom': neighbors(row + 1, col),
|
||||
'left': side(row, col, -1) if col else [],
|
||||
'right': side(row, col, 1) if col < ncols - 1 else [],
|
||||
}
|
||||
50
kitty/layout/interface.py
Normal file
50
kitty/layout/interface.py
Normal file
@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
from typing import Dict, Tuple, Type
|
||||
|
||||
from .base import Layout
|
||||
from .grid import Grid
|
||||
from .splits import Splits
|
||||
from .stack import Stack
|
||||
from .tall import Fat, Tall
|
||||
from .vertical import Horizontal, Vertical
|
||||
|
||||
all_layouts: Dict[str, Type[Layout]] = {
|
||||
Stack.name: Stack,
|
||||
Tall.name: Tall,
|
||||
Fat.name: Fat,
|
||||
Vertical.name: Vertical,
|
||||
Horizontal.name: Horizontal,
|
||||
Grid.name: Grid,
|
||||
Splits.name: Splits,
|
||||
}
|
||||
|
||||
|
||||
class CreateLayoutObjectFor:
|
||||
cache: Dict[Tuple, Layout] = {}
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
name: str,
|
||||
os_window_id: int,
|
||||
tab_id: int,
|
||||
layout_opts: str = ''
|
||||
) -> Layout:
|
||||
key = name, os_window_id, tab_id, layout_opts
|
||||
ans = create_layout_object_for.cache.get(key)
|
||||
if ans is None:
|
||||
name, layout_opts = name.partition(':')[::2]
|
||||
ans = create_layout_object_for.cache[key] = all_layouts[name](
|
||||
os_window_id, tab_id, layout_opts)
|
||||
return ans
|
||||
|
||||
|
||||
create_layout_object_for = CreateLayoutObjectFor()
|
||||
|
||||
|
||||
def evict_cached_layouts(tab_id: int) -> None:
|
||||
remove = [key for key in create_layout_object_for.cache if key[2] == tab_id]
|
||||
for key in remove:
|
||||
del create_layout_object_for.cache[key]
|
||||
465
kitty/layout/splits.py
Normal file
465
kitty/layout/splits.py
Normal file
@ -0,0 +1,465 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
from typing import (
|
||||
Collection, Dict, Generator, Optional, Sequence, Tuple, Union
|
||||
)
|
||||
|
||||
from kitty.constants import Edges, WindowGeometry
|
||||
from kitty.typing import EdgeLiteral, WindowType
|
||||
from kitty.window_list import WindowList
|
||||
|
||||
from .base import (
|
||||
Borders, InternalNeighborsMap, Layout, LayoutOpts, all_borders,
|
||||
blank_rects_for_window, lgd, no_borders, window_geometry_from_layouts
|
||||
)
|
||||
|
||||
|
||||
class Pair:
|
||||
|
||||
def __init__(self, horizontal: bool = True):
|
||||
self.horizontal = horizontal
|
||||
self.one: Optional[Union[Pair, int]] = None
|
||||
self.two: Optional[Union[Pair, int]] = None
|
||||
self.bias = 0.5
|
||||
self.between_border: Optional[Edges] = None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return 'Pair(horizontal={}, bias={:.2f}, one={}, two={}, between_border={})'.format(
|
||||
self.horizontal, self.bias, self.one, self.two, self.between_border)
|
||||
|
||||
def all_window_ids(self) -> Generator[int, None, None]:
|
||||
if self.one is not None:
|
||||
if isinstance(self.one, Pair):
|
||||
yield from self.one.all_window_ids()
|
||||
else:
|
||||
yield self.one
|
||||
if self.two is not None:
|
||||
if isinstance(self.two, Pair):
|
||||
yield from self.two.all_window_ids()
|
||||
else:
|
||||
yield self.two
|
||||
|
||||
def self_and_descendants(self) -> Generator['Pair', None, None]:
|
||||
yield self
|
||||
if isinstance(self.one, Pair):
|
||||
yield from self.one.self_and_descendants()
|
||||
if isinstance(self.two, Pair):
|
||||
yield from self.two.self_and_descendants()
|
||||
|
||||
def pair_for_window(self, window_id: int) -> Optional['Pair']:
|
||||
if self.one == window_id or self.two == window_id:
|
||||
return self
|
||||
ans = None
|
||||
if isinstance(self.one, Pair):
|
||||
ans = self.one.pair_for_window(window_id)
|
||||
if ans is None and isinstance(self.two, Pair):
|
||||
ans = self.two.pair_for_window(window_id)
|
||||
return ans
|
||||
|
||||
def parent(self, root: 'Pair') -> Optional['Pair']:
|
||||
for q in root.self_and_descendants():
|
||||
if q.one is self or q.two is self:
|
||||
return q
|
||||
|
||||
def remove_windows(self, window_ids: Collection[int]) -> None:
|
||||
if isinstance(self.one, int) and self.one in window_ids:
|
||||
self.one = None
|
||||
if isinstance(self.two, int) and self.two in window_ids:
|
||||
self.two = None
|
||||
if self.one is None and self.two is not None:
|
||||
self.one, self.two = self.two, None
|
||||
|
||||
@property
|
||||
def is_redundant(self) -> bool:
|
||||
return self.one is None or self.two is None
|
||||
|
||||
def collapse_redundant_pairs(self) -> None:
|
||||
while isinstance(self.one, Pair) and self.one.is_redundant:
|
||||
self.one = self.one.one or self.one.two
|
||||
while isinstance(self.two, Pair) and self.two.is_redundant:
|
||||
self.two = self.two.one or self.two.two
|
||||
if isinstance(self.one, Pair):
|
||||
self.one.collapse_redundant_pairs()
|
||||
if isinstance(self.two, Pair):
|
||||
self.two.collapse_redundant_pairs()
|
||||
|
||||
def balanced_add(self, window_id: int) -> 'Pair':
|
||||
if self.one is None or self.two is None:
|
||||
if self.one is None:
|
||||
if self.two is None:
|
||||
self.one = window_id
|
||||
return self
|
||||
self.one, self.two = self.two, self.one
|
||||
self.two = window_id
|
||||
return self
|
||||
if isinstance(self.one, Pair) and isinstance(self.two, Pair):
|
||||
one_count = sum(1 for _ in self.one.all_window_ids())
|
||||
two_count = sum(1 for _ in self.two.all_window_ids())
|
||||
q = self.one if one_count < two_count else self.two
|
||||
return q.balanced_add(window_id)
|
||||
if not isinstance(self.one, Pair) and not isinstance(self.two, Pair):
|
||||
pair = Pair(horizontal=self.horizontal)
|
||||
pair.balanced_add(self.one)
|
||||
pair.balanced_add(self.two)
|
||||
self.one, self.two = pair, window_id
|
||||
return self
|
||||
if isinstance(self.one, Pair):
|
||||
window_to_be_split = self.two
|
||||
self.two = pair = Pair(horizontal=self.horizontal)
|
||||
else:
|
||||
window_to_be_split = self.one
|
||||
self.one = pair = Pair(horizontal=self.horizontal)
|
||||
assert isinstance(window_to_be_split, int)
|
||||
pair.balanced_add(window_to_be_split)
|
||||
pair.balanced_add(window_id)
|
||||
return pair
|
||||
|
||||
def split_and_add(self, existing_window_id: int, new_window_id: int, horizontal: bool, after: bool) -> 'Pair':
|
||||
q = (existing_window_id, new_window_id) if after else (new_window_id, existing_window_id)
|
||||
if self.is_redundant:
|
||||
pair = self
|
||||
pair.horizontal = horizontal
|
||||
self.one, self.two = q
|
||||
else:
|
||||
pair = Pair(horizontal=horizontal)
|
||||
if self.one == existing_window_id:
|
||||
self.one = pair
|
||||
else:
|
||||
self.two = pair
|
||||
tuple(map(pair.balanced_add, q))
|
||||
return pair
|
||||
|
||||
def apply_window_geometry(
|
||||
self, window_id: int,
|
||||
window_geometry: WindowGeometry,
|
||||
id_window_map: Dict[int, WindowType],
|
||||
id_idx_map: Dict[int, int],
|
||||
layout_object: Layout
|
||||
) -> None:
|
||||
w = id_window_map[window_id]
|
||||
w.set_geometry(id_idx_map[window_id], window_geometry)
|
||||
if w.overlay_window_id is not None:
|
||||
q = id_window_map.get(w.overlay_window_id)
|
||||
if q is not None:
|
||||
q.set_geometry(id_idx_map[q.id], window_geometry)
|
||||
layout_object.blank_rects.extend(blank_rects_for_window(window_geometry))
|
||||
|
||||
def effective_border(self, id_window_map: Dict[int, WindowType]) -> int:
|
||||
for wid in self.all_window_ids():
|
||||
return id_window_map[wid].effective_border()
|
||||
return 0
|
||||
|
||||
def layout_pair(
|
||||
self,
|
||||
left: int, top: int, width: int, height: int,
|
||||
id_window_map: Dict[int, WindowType],
|
||||
id_idx_map: Dict[int, int],
|
||||
layout_object: Layout
|
||||
) -> None:
|
||||
self.between_border = None
|
||||
if self.one is None or self.two is None:
|
||||
q = self.one or self.two
|
||||
if isinstance(q, Pair):
|
||||
return q.layout_pair(left, top, width, height, id_window_map, id_idx_map, layout_object)
|
||||
if q is None:
|
||||
return
|
||||
w = id_window_map[q]
|
||||
xl = next(layout_object.xlayout([w], start=left, size=width))
|
||||
yl = next(layout_object.ylayout([w], start=top, size=height))
|
||||
geom = window_geometry_from_layouts(xl, yl)
|
||||
self.apply_window_geometry(q, geom, id_window_map, id_idx_map, layout_object)
|
||||
return
|
||||
bw = self.effective_border(id_window_map) if lgd.draw_minimal_borders else 0
|
||||
b1 = bw // 2
|
||||
b2 = bw - b1
|
||||
if self.horizontal:
|
||||
w1 = max(2*lgd.cell_width + 1, int(self.bias * width) - b1)
|
||||
w2 = max(2*lgd.cell_width + 1, width - w1 - b1 - b2)
|
||||
if isinstance(self.one, Pair):
|
||||
self.one.layout_pair(left, top, w1, height, id_window_map, id_idx_map, layout_object)
|
||||
else:
|
||||
w = id_window_map[self.one]
|
||||
yl = next(layout_object.ylayout([w], start=top, size=height))
|
||||
xl = next(layout_object.xlayout([w], start=left, size=w1))
|
||||
geom = window_geometry_from_layouts(xl, yl)
|
||||
self.apply_window_geometry(self.one, geom, id_window_map, id_idx_map, layout_object)
|
||||
if b1 + b2:
|
||||
self.between_border = Edges(left + w1, top, left + w1 + b1 + b2, top + height)
|
||||
left += b1 + b2
|
||||
if isinstance(self.two, Pair):
|
||||
self.two.layout_pair(left + w1, top, w2, height, id_window_map, id_idx_map, layout_object)
|
||||
else:
|
||||
w = id_window_map[self.two]
|
||||
xl = next(layout_object.xlayout([w], start=left + w1, size=w2))
|
||||
yl = next(layout_object.ylayout([w], start=top, size=height))
|
||||
geom = window_geometry_from_layouts(xl, yl)
|
||||
self.apply_window_geometry(self.two, geom, id_window_map, id_idx_map, layout_object)
|
||||
else:
|
||||
h1 = max(2*lgd.cell_height + 1, int(self.bias * height) - b1)
|
||||
h2 = max(2*lgd.cell_height + 1, height - h1 - b1 - b2)
|
||||
if isinstance(self.one, Pair):
|
||||
self.one.layout_pair(left, top, width, h1, id_window_map, id_idx_map, layout_object)
|
||||
else:
|
||||
w = id_window_map[self.one]
|
||||
xl = next(layout_object.xlayout([w], start=left, size=width))
|
||||
yl = next(layout_object.ylayout([w], start=top, size=h1))
|
||||
geom = window_geometry_from_layouts(xl, yl)
|
||||
self.apply_window_geometry(self.one, geom, id_window_map, id_idx_map, layout_object)
|
||||
if b1 + b2:
|
||||
self.between_border = Edges(left, top + h1, left + width, top + h1 + b1 + b2)
|
||||
top += b1 + b2
|
||||
if isinstance(self.two, Pair):
|
||||
self.two.layout_pair(left, top + h1, width, h2, id_window_map, id_idx_map, layout_object)
|
||||
else:
|
||||
w = id_window_map[self.two]
|
||||
xl = next(layout_object.xlayout([w], start=left, size=width))
|
||||
yl = next(layout_object.ylayout([w], start=top + h1, size=h2))
|
||||
geom = window_geometry_from_layouts(xl, yl)
|
||||
self.apply_window_geometry(self.two, geom, id_window_map, id_idx_map, layout_object)
|
||||
|
||||
def modify_size_of_child(self, which: int, increment: float, is_horizontal: bool, layout_object: 'Splits') -> bool:
|
||||
if is_horizontal == self.horizontal and not self.is_redundant:
|
||||
if which == 2:
|
||||
increment *= -1
|
||||
new_bias = max(0.1, min(self.bias + increment, 0.9))
|
||||
if new_bias != self.bias:
|
||||
self.bias = new_bias
|
||||
return True
|
||||
return False
|
||||
parent = self.parent(layout_object.pairs_root)
|
||||
if parent is not None:
|
||||
which = 1 if parent.one is self else 2
|
||||
return parent.modify_size_of_child(which, increment, is_horizontal, layout_object)
|
||||
return False
|
||||
|
||||
def neighbors_for_window(self, window_id: int, ans: InternalNeighborsMap, layout_object: 'Splits') -> None:
|
||||
|
||||
def quadrant(is_horizontal: bool, is_first: bool) -> Tuple[EdgeLiteral, EdgeLiteral]:
|
||||
if is_horizontal:
|
||||
if is_first:
|
||||
return 'left', 'right'
|
||||
return 'right', 'left'
|
||||
if is_first:
|
||||
return 'top', 'bottom'
|
||||
return 'bottom', 'top'
|
||||
|
||||
def extend(other: Union[int, 'Pair', None], edge: EdgeLiteral, which: EdgeLiteral) -> None:
|
||||
if not ans[which] and other:
|
||||
if isinstance(other, Pair):
|
||||
ans[which].extend(other.edge_windows(edge))
|
||||
else:
|
||||
ans[which].append(other)
|
||||
|
||||
other = self.two if self.one == window_id else self.one
|
||||
extend(other, *quadrant(self.horizontal, self.one == window_id))
|
||||
|
||||
child = self
|
||||
while True:
|
||||
parent = child.parent(layout_object.pairs_root)
|
||||
if parent is None:
|
||||
break
|
||||
other = parent.two if child is parent.one else parent.one
|
||||
extend(other, *quadrant(parent.horizontal, child is parent.one))
|
||||
child = parent
|
||||
|
||||
def edge_windows(self, edge: str) -> Generator[int, None, None]:
|
||||
if self.is_redundant:
|
||||
q = self.one or self.two
|
||||
if q:
|
||||
if isinstance(q, Pair):
|
||||
yield from q.edge_windows(edge)
|
||||
else:
|
||||
yield q
|
||||
edges = ('left', 'right') if self.horizontal else ('top', 'bottom')
|
||||
if edge in edges:
|
||||
q = self.one if edge in ('left', 'top') else self.two
|
||||
if q:
|
||||
if isinstance(q, Pair):
|
||||
yield from q.edge_windows(edge)
|
||||
else:
|
||||
yield q
|
||||
else:
|
||||
for q in (self.one, self.two):
|
||||
if q:
|
||||
if isinstance(q, Pair):
|
||||
yield from q.edge_windows(edge)
|
||||
else:
|
||||
yield q
|
||||
|
||||
|
||||
class SplitsLayoutOpts(LayoutOpts):
|
||||
|
||||
default_axis_is_horizontal: bool = True
|
||||
|
||||
def __init__(self, data: Dict[str, str]):
|
||||
self.default_axis_is_horizontal = data.get('split_axis', 'horizontal') == 'horizontal'
|
||||
|
||||
|
||||
class Splits(Layout):
|
||||
name = 'splits'
|
||||
needs_all_windows = True
|
||||
layout_opts = SplitsLayoutOpts({})
|
||||
|
||||
@property
|
||||
def default_axis_is_horizontal(self) -> bool:
|
||||
return self.layout_opts.default_axis_is_horizontal
|
||||
|
||||
@property
|
||||
def pairs_root(self) -> Pair:
|
||||
root: Optional[Pair] = getattr(self, '_pairs_root', None)
|
||||
if root is None:
|
||||
self._pairs_root = root = Pair(horizontal=self.default_axis_is_horizontal)
|
||||
return root
|
||||
|
||||
@pairs_root.setter
|
||||
def pairs_root(self, root: Pair) -> None:
|
||||
self._pairs_root = root
|
||||
|
||||
def do_layout_all_windows(self, windows: WindowList, active_window_idx: int, all_windows: WindowList) -> None:
|
||||
window_count = len(windows)
|
||||
root = self.pairs_root
|
||||
all_present_window_ids = frozenset(w.overlay_for or w.id for w in windows)
|
||||
already_placed_window_ids = frozenset(root.all_window_ids())
|
||||
windows_to_remove = already_placed_window_ids - all_present_window_ids
|
||||
|
||||
if windows_to_remove:
|
||||
for pair in root.self_and_descendants():
|
||||
pair.remove_windows(windows_to_remove)
|
||||
root.collapse_redundant_pairs()
|
||||
if root.one is None or root.two is None:
|
||||
q = root.one or root.two
|
||||
if isinstance(q, Pair):
|
||||
root = self.pairs_root = q
|
||||
id_window_map = {w.id: w for w in all_windows}
|
||||
id_idx_map = {w.id: i for i, w in enumerate(all_windows)}
|
||||
windows_to_add = all_present_window_ids - already_placed_window_ids
|
||||
if windows_to_add:
|
||||
for wid in sorted(windows_to_add, key=id_idx_map.__getitem__):
|
||||
root.balanced_add(wid)
|
||||
|
||||
if window_count == 1:
|
||||
self.layout_single_window(windows[0])
|
||||
else:
|
||||
root.layout_pair(lgd.central.left, lgd.central.top, lgd.central.width, lgd.central.height, id_window_map, id_idx_map, self)
|
||||
|
||||
def do_add_window(
|
||||
self,
|
||||
all_windows: WindowList,
|
||||
window: WindowType,
|
||||
current_active_window_idx: Optional[int],
|
||||
location: Optional[str]
|
||||
) -> int:
|
||||
horizontal = self.default_axis_is_horizontal
|
||||
after = True
|
||||
if location is not None:
|
||||
if location == 'vsplit':
|
||||
horizontal = True
|
||||
elif location == 'hsplit':
|
||||
horizontal = False
|
||||
if location in ('before', 'first'):
|
||||
after = False
|
||||
active_window_idx = None
|
||||
if current_active_window_idx is not None and 0 <= current_active_window_idx < len(all_windows):
|
||||
cw = all_windows[current_active_window_idx]
|
||||
window_id = cw.overlay_for or cw.id
|
||||
pair = self.pairs_root.pair_for_window(window_id)
|
||||
if pair is not None:
|
||||
pair.split_and_add(window_id, window.id, horizontal, after)
|
||||
active_window_idx = current_active_window_idx
|
||||
if after:
|
||||
active_window_idx += 1
|
||||
for i in range(len(all_windows), active_window_idx, -1):
|
||||
self.swap_windows_in_os_window(i, i - 1)
|
||||
all_windows.insert(active_window_idx, window)
|
||||
if active_window_idx is None:
|
||||
active_window_idx = len(all_windows)
|
||||
all_windows.append(window)
|
||||
return active_window_idx
|
||||
|
||||
def modify_size_of_window(
|
||||
self,
|
||||
all_windows: WindowList,
|
||||
window_id: int,
|
||||
increment: float,
|
||||
is_horizontal: bool = True
|
||||
) -> bool:
|
||||
idx = idx_for_id(window_id, all_windows)
|
||||
if idx is None:
|
||||
return False
|
||||
w = all_windows[idx]
|
||||
window_id = w.overlay_for or w.id
|
||||
pair = self.pairs_root.pair_for_window(window_id)
|
||||
if pair is None:
|
||||
return False
|
||||
which = 1 if pair.one == window_id else 2
|
||||
return pair.modify_size_of_child(which, increment, is_horizontal, self)
|
||||
|
||||
def remove_all_biases(self) -> bool:
|
||||
for pair in self.pairs_root.self_and_descendants():
|
||||
pair.bias = 0.5
|
||||
return True
|
||||
|
||||
def window_independent_borders(self, windows: WindowList, active_window: Optional[WindowType] = None) -> Generator[Edges, None, None]:
|
||||
if not lgd.draw_minimal_borders:
|
||||
return
|
||||
for pair in self.pairs_root.self_and_descendants():
|
||||
if pair.between_border is not None:
|
||||
yield pair.between_border
|
||||
|
||||
def neighbors_for_window(self, window: WindowType, windows: WindowList) -> InternalNeighborsMap:
|
||||
window_id = window.overlay_for or window.id
|
||||
pair = self.pairs_root.pair_for_window(window_id)
|
||||
ans: InternalNeighborsMap = {'left': [], 'right': [], 'top': [], 'bottom': []}
|
||||
if pair is not None:
|
||||
pair.neighbors_for_window(window_id, ans, self)
|
||||
return ans
|
||||
|
||||
def swap_windows_in_layout(self, all_windows: WindowList, a: int, b: int) -> None:
|
||||
w1_, w2_ = all_windows[a], all_windows[b]
|
||||
super().swap_windows_in_layout(all_windows, a, b)
|
||||
w1 = w1_.overlay_for or w1_.id
|
||||
w2 = w2_.overlay_for or w2_.id
|
||||
p1 = self.pairs_root.pair_for_window(w1)
|
||||
p2 = self.pairs_root.pair_for_window(w2)
|
||||
if p1 and p2:
|
||||
if p1 is p2:
|
||||
p1.one, p1.two = p1.two, p1.one
|
||||
else:
|
||||
if p1.one == w1:
|
||||
p1.one = w2
|
||||
else:
|
||||
p1.two = w2
|
||||
if p2.one == w2:
|
||||
p2.one = w1
|
||||
else:
|
||||
p2.two = w1
|
||||
|
||||
def minimal_borders(self, windows: WindowList, active_window: Optional[WindowType], needs_borders_map: Dict[int, bool]) -> Generator[Borders, None, None]:
|
||||
for w in windows:
|
||||
if (w is active_window and lgd.draw_active_borders) or w.needs_attention:
|
||||
yield all_borders
|
||||
else:
|
||||
yield no_borders
|
||||
|
||||
def layout_action(self, action_name: str, args: Sequence[str], all_windows: WindowList, active_window_idx: int) -> Optional[Union[bool, int]]:
|
||||
if action_name == 'rotate':
|
||||
args = args or ('90',)
|
||||
try:
|
||||
amt = int(args[0])
|
||||
except Exception:
|
||||
amt = 90
|
||||
if amt not in (90, 180, 270):
|
||||
amt = 90
|
||||
rotate = amt in (90, 270)
|
||||
swap = amt in (180, 270)
|
||||
w = all_windows[active_window_idx]
|
||||
wid = w.overlay_for or w.id
|
||||
pair = self.pairs_root.pair_for_window(wid)
|
||||
if pair is not None and not pair.is_redundant:
|
||||
if rotate:
|
||||
pair.horizontal = not pair.horizontal
|
||||
if swap:
|
||||
pair.one, pair.two = pair.two, pair.one
|
||||
return True
|
||||
22
kitty/layout/stack.py
Normal file
22
kitty/layout/stack.py
Normal file
@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
from kitty.window_list import WindowList
|
||||
|
||||
from .base import Layout, blank_rects_for_window, lgd
|
||||
|
||||
|
||||
class Stack(Layout):
|
||||
|
||||
name = 'stack'
|
||||
needs_window_borders = False
|
||||
only_active_window_visible = True
|
||||
|
||||
def do_layout(self, windows: WindowList, active_window_idx: int) -> None:
|
||||
for i, w in enumerate(windows):
|
||||
wg = self.layout_single_window(w, left_align=lgd.align_top_left, return_geometry=True)
|
||||
if wg is not None:
|
||||
w.set_geometry(i, wg)
|
||||
if w.is_visible_in_layout:
|
||||
self.blank_rects = list(blank_rects_for_window(wg))
|
||||
191
kitty/layout/tall.py
Normal file
191
kitty/layout/tall.py
Normal file
@ -0,0 +1,191 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
from itertools import repeat
|
||||
from typing import Dict, Generator, List, Optional, Tuple
|
||||
|
||||
from kitty.typing import EdgeLiteral, WindowType
|
||||
from kitty.window_list import WindowList
|
||||
|
||||
from .base import (
|
||||
Borders, InternalNeighborsMap, Layout, LayoutDimension, LayoutOpts,
|
||||
ListOfWindows, all_borders, lgd, no_borders, normalize_biases,
|
||||
safe_increment_bias, variable_bias
|
||||
)
|
||||
|
||||
|
||||
def neighbors_for_tall_window(num_full_size_windows: int, window: WindowType, windows: WindowList) -> InternalNeighborsMap:
|
||||
idx = windows.index(window)
|
||||
prev = None if idx == 0 else windows[idx-1]
|
||||
nxt = None if idx == len(windows) - 1 else windows[idx+1]
|
||||
ans: InternalNeighborsMap = {'left': [prev.id] if prev is not None else [], 'right': [], 'top': [], 'bottom': []}
|
||||
if idx < num_full_size_windows - 1:
|
||||
if nxt is not None:
|
||||
ans['right'] = [nxt.id]
|
||||
elif idx == num_full_size_windows - 1:
|
||||
ans['right'] = [w.id for w in windows[idx+1:]]
|
||||
else:
|
||||
ans['left'] = [windows[num_full_size_windows - 1].id]
|
||||
if idx > num_full_size_windows and prev is not None:
|
||||
ans['top'] = [prev.id]
|
||||
if nxt is not None:
|
||||
ans['bottom'] = [nxt.id]
|
||||
return ans
|
||||
|
||||
|
||||
class TallLayoutOpts(LayoutOpts):
|
||||
bias: Tuple[float, ...] = ()
|
||||
full_size: int = 1
|
||||
|
||||
def __init__(self, data: Dict[str, str]):
|
||||
|
||||
try:
|
||||
self.full_size = int(data.get('full_size', 1))
|
||||
except Exception:
|
||||
self.full_size = 1
|
||||
self.full_size = fs = max(1, min(self.full_size, 100))
|
||||
try:
|
||||
b = int(data.get('bias', 50)) / 100
|
||||
except Exception:
|
||||
b = 0.5
|
||||
b = max(0.1, min(b, 0.9))
|
||||
self.bias = tuple(repeat(b / fs, fs)) + (1.0 - b,)
|
||||
|
||||
|
||||
class Tall(Layout):
|
||||
|
||||
name = 'tall'
|
||||
main_is_horizontal = True
|
||||
only_between_border = Borders(False, False, False, True)
|
||||
only_main_border = Borders(False, False, True, False)
|
||||
layout_opts = TallLayoutOpts({})
|
||||
main_axis_layout = Layout.xlayout
|
||||
perp_axis_layout = Layout.ylayout
|
||||
|
||||
@property
|
||||
def num_full_size_windows(self) -> int:
|
||||
return self.layout_opts.full_size
|
||||
|
||||
def remove_all_biases(self) -> bool:
|
||||
self.main_bias: List[float] = list(self.layout_opts.bias)
|
||||
self.biased_map: Dict[int, float] = {}
|
||||
return True
|
||||
|
||||
def variable_layout(self, windows: ListOfWindows, biased_map: Dict[int, float]) -> LayoutDimension:
|
||||
windows = windows[self.num_full_size_windows:]
|
||||
bias = variable_bias(len(windows), biased_map) if len(windows) > 1 else None
|
||||
return self.perp_axis_layout(windows, bias=bias)
|
||||
|
||||
def apply_bias(self, idx: int, increment: float, top_level_windows: ListOfWindows, is_horizontal: bool = True) -> bool:
|
||||
num_windows = len(top_level_windows)
|
||||
if self.main_is_horizontal == is_horizontal:
|
||||
before_main_bias = self.main_bias
|
||||
ncols = self.num_full_size_windows + 1
|
||||
biased_col = idx if idx < self.num_full_size_windows else (ncols - 1)
|
||||
self.main_bias = [
|
||||
safe_increment_bias(self.main_bias[i], increment * (1 if i == biased_col else -1)) for i in range(ncols)
|
||||
]
|
||||
self.main_bias = normalize_biases(self.main_bias)
|
||||
return self.main_bias != before_main_bias
|
||||
|
||||
num_of_short_windows = num_windows - self.num_full_size_windows
|
||||
if idx < self.num_full_size_windows or num_of_short_windows < 2:
|
||||
return False
|
||||
idx -= self.num_full_size_windows
|
||||
before_layout = list(self.variable_layout(top_level_windows, self.biased_map))
|
||||
before = self.biased_map.get(idx, 0.)
|
||||
candidate = self.biased_map.copy()
|
||||
candidate[idx] = after = before + increment
|
||||
if before_layout == list(self.variable_layout(top_level_windows, candidate)):
|
||||
return False
|
||||
self.biased_map = candidate
|
||||
return before != after
|
||||
|
||||
def do_layout(self, windows: WindowList, active_window_idx: int) -> None:
|
||||
if len(windows) == 1:
|
||||
self.layout_single_window(windows[0])
|
||||
return
|
||||
is_fat = not self.main_is_horizontal
|
||||
if len(windows) <= self.num_full_size_windows + 1:
|
||||
xlayout = self.main_axis_layout(windows, bias=self.main_bias)
|
||||
for i, (w, xl) in enumerate(zip(windows, xlayout)):
|
||||
yl = next(self.perp_axis_layout([w]))
|
||||
if is_fat:
|
||||
xl, yl = yl, xl
|
||||
self.set_window_geometry(w, i, xl, yl)
|
||||
return
|
||||
|
||||
xlayout = self.main_axis_layout(windows[:self.num_full_size_windows + 1], bias=self.main_bias)
|
||||
attr: EdgeLiteral = 'bottom' if is_fat else 'right'
|
||||
start = lgd.central.top if is_fat else lgd.central.left
|
||||
for i, w in enumerate(windows):
|
||||
if i >= self.num_full_size_windows:
|
||||
break
|
||||
xl = next(xlayout)
|
||||
yl = next(self.perp_axis_layout([w]))
|
||||
if is_fat:
|
||||
xl, yl = yl, xl
|
||||
self.set_window_geometry(w, i, xl, yl)
|
||||
start = getattr(w.geometry, attr) + w.effective_border() + w.effective_margin(attr) + w.effective_padding(attr)
|
||||
ylayout = self.variable_layout(windows, self.biased_map)
|
||||
size = (lgd.central.height if is_fat else lgd.central.width) - start
|
||||
for i, w in enumerate(windows):
|
||||
if i < self.num_full_size_windows:
|
||||
continue
|
||||
yl = next(ylayout)
|
||||
xl = next(self.main_axis_layout([w], start=start, size=size))
|
||||
if is_fat:
|
||||
xl, yl = yl, xl
|
||||
self.set_window_geometry(w, i, xl, yl)
|
||||
|
||||
def neighbors_for_window(self, window: WindowType, windows: WindowList) -> InternalNeighborsMap:
|
||||
return neighbors_for_tall_window(self.num_full_size_windows, window, windows)
|
||||
|
||||
def minimal_borders(self, windows: WindowList, active_window: Optional[WindowType], needs_borders_map: Dict[int, bool]) -> Generator[Borders, None, None]:
|
||||
last_i = len(windows) - 1
|
||||
for i, w in enumerate(windows):
|
||||
if needs_borders_map[w.id]:
|
||||
yield all_borders
|
||||
continue
|
||||
if i < self.num_full_size_windows:
|
||||
if (last_i == i+1 or i+1 < self.num_full_size_windows) and needs_borders_map[windows[i+1].id]:
|
||||
yield no_borders
|
||||
else:
|
||||
yield no_borders if i == last_i else self.only_main_border
|
||||
continue
|
||||
if i == last_i:
|
||||
yield no_borders
|
||||
break
|
||||
if needs_borders_map[windows[i+1].id]:
|
||||
yield no_borders
|
||||
else:
|
||||
yield self.only_between_border
|
||||
|
||||
|
||||
class Fat(Tall):
|
||||
|
||||
name = 'fat'
|
||||
main_is_horizontal = False
|
||||
only_between_border = Borders(False, False, True, False)
|
||||
only_main_border = Borders(False, False, False, True)
|
||||
main_axis_layout = Layout.ylayout
|
||||
perp_axis_layout = Layout.xlayout
|
||||
|
||||
def neighbors_for_window(self, window: WindowType, windows: WindowList) -> InternalNeighborsMap:
|
||||
idx = windows.index(window)
|
||||
prev = None if idx == 0 else windows[idx-1]
|
||||
nxt = None if idx == len(windows) - 1 else windows[idx+1]
|
||||
ans: InternalNeighborsMap = {'left': [], 'right': [], 'top': [] if prev is None else [prev.id], 'bottom': []}
|
||||
if idx < self.num_full_size_windows - 1:
|
||||
if nxt is not None:
|
||||
ans['bottom'] = [nxt.id]
|
||||
elif idx == self.num_full_size_windows - 1:
|
||||
ans['bottom'] = [w.id for w in windows[idx+1:]]
|
||||
else:
|
||||
ans['top'] = [windows[self.num_full_size_windows - 1].id]
|
||||
if idx > self.num_full_size_windows and prev is not None:
|
||||
ans['left'] = [prev.id]
|
||||
if nxt is not None:
|
||||
ans['right'] = [nxt.id]
|
||||
return ans
|
||||
90
kitty/layout/vertical.py
Normal file
90
kitty/layout/vertical.py
Normal file
@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
from typing import Dict, Generator, Optional
|
||||
|
||||
from kitty.typing import WindowType
|
||||
from kitty.window_list import WindowList
|
||||
|
||||
from .base import (
|
||||
Borders, InternalNeighborsMap, Layout, LayoutDimension, ListOfWindows,
|
||||
all_borders, no_borders, variable_bias
|
||||
)
|
||||
|
||||
|
||||
class Vertical(Layout):
|
||||
|
||||
name = 'vertical'
|
||||
main_is_horizontal = False
|
||||
only_between_border = Borders(False, False, False, True)
|
||||
main_axis_layout = Layout.ylayout
|
||||
perp_axis_layout = Layout.xlayout
|
||||
|
||||
def variable_layout(self, windows: ListOfWindows, biased_map: Dict[int, float]) -> LayoutDimension:
|
||||
num_windows = len(windows)
|
||||
bias = variable_bias(num_windows, biased_map) if num_windows else None
|
||||
return self.main_axis_layout(windows, bias=bias)
|
||||
|
||||
def remove_all_biases(self) -> bool:
|
||||
self.biased_map: Dict[int, float] = {}
|
||||
return True
|
||||
|
||||
def apply_bias(self, idx: int, increment: float, top_level_windows: ListOfWindows, is_horizontal: bool = True) -> bool:
|
||||
if self.main_is_horizontal != is_horizontal:
|
||||
return False
|
||||
num_windows = len(top_level_windows)
|
||||
if num_windows < 2:
|
||||
return False
|
||||
before_layout = list(self.variable_layout(top_level_windows, self.biased_map))
|
||||
candidate = self.biased_map.copy()
|
||||
before = candidate.get(idx, 0)
|
||||
candidate[idx] = before + increment
|
||||
if before_layout == list(self.variable_layout(top_level_windows, candidate)):
|
||||
return False
|
||||
self.biased_map = candidate
|
||||
return True
|
||||
|
||||
def do_layout(self, windows: WindowList, active_window_idx: int) -> None:
|
||||
window_count = len(windows)
|
||||
if window_count == 1:
|
||||
self.layout_single_window(windows[0])
|
||||
return
|
||||
|
||||
ylayout = self.variable_layout(windows, self.biased_map)
|
||||
for i, (w, yl) in enumerate(zip(windows, ylayout)):
|
||||
xl = next(self.perp_axis_layout([w]))
|
||||
if self.main_is_horizontal:
|
||||
xl, yl = yl, xl
|
||||
self.set_window_geometry(w, i, xl, yl)
|
||||
|
||||
def minimal_borders(self, windows: WindowList, active_window: Optional[WindowType], needs_borders_map: Dict[int, bool]) -> Generator[Borders, None, None]:
|
||||
last_i = len(windows) - 1
|
||||
for i, w in enumerate(windows):
|
||||
if needs_borders_map[w.id]:
|
||||
yield all_borders
|
||||
continue
|
||||
if i == last_i:
|
||||
yield no_borders
|
||||
break
|
||||
if needs_borders_map[windows[i+1].id]:
|
||||
yield no_borders
|
||||
else:
|
||||
yield self.only_between_border
|
||||
|
||||
def neighbors_for_window(self, window: WindowType, windows: WindowList) -> InternalNeighborsMap:
|
||||
idx = windows.index(window)
|
||||
before = [] if window is windows[0] else [windows[idx-1].id]
|
||||
after = [] if window is windows[-1] else [windows[idx+1].id]
|
||||
if self.main_is_horizontal:
|
||||
return {'left': before, 'right': after, 'top': [], 'bottom': []}
|
||||
return {'top': before, 'bottom': after, 'left': [], 'right': []}
|
||||
|
||||
|
||||
class Horizontal(Vertical):
|
||||
|
||||
name = 'horizontal'
|
||||
main_is_horizontal = True
|
||||
only_between_border = Borders(False, False, True, False)
|
||||
main_axis_layout = Layout.xlayout
|
||||
perp_axis_layout = Layout.ylayout
|
||||
@ -9,7 +9,7 @@ from typing import Generator, List, NamedTuple, Optional, Tuple, Union
|
||||
from .cli_stub import CLIOptions
|
||||
from .config_data import to_layout_names
|
||||
from .constants import FloatEdges, kitty_exe
|
||||
from .layout import all_layouts
|
||||
from .layout.interface import all_layouts
|
||||
from .options_stub import Options
|
||||
from .typing import SpecialWindowInstance
|
||||
from .utils import log_error, resolved_shell
|
||||
|
||||
@ -10,7 +10,7 @@ from .fast_data_types import (
|
||||
DECAWM, Screen, cell_size_for_window, pt_to_px, set_tab_bar_render_data,
|
||||
viewport_for_window
|
||||
)
|
||||
from .layout import Rect
|
||||
from .layout.base import Rect
|
||||
from .options_stub import Options
|
||||
from .rgb import Color, alpha_blend, color_from_int
|
||||
from .utils import color_as_int, log_error
|
||||
|
||||
113
kitty/tabs.py
113
kitty/tabs.py
@ -6,6 +6,7 @@ import weakref
|
||||
from collections import deque
|
||||
from contextlib import suppress
|
||||
from functools import partial
|
||||
from operator import attrgetter
|
||||
from typing import (
|
||||
Any, Deque, Dict, Generator, Iterator, List, NamedTuple, Optional, Pattern,
|
||||
Sequence, Tuple, cast
|
||||
@ -20,14 +21,16 @@ from .fast_data_types import (
|
||||
next_window_id, remove_tab, remove_window, ring_bell, set_active_tab,
|
||||
swap_tabs, sync_os_window_title, x11_window_id
|
||||
)
|
||||
from .layout import (
|
||||
Layout, Rect, create_layout_object_for, evict_cached_layouts
|
||||
from .layout.base import Layout, Rect
|
||||
from .layout.interface import (
|
||||
create_layout_object_for, evict_cached_layouts
|
||||
)
|
||||
from .options_stub import Options
|
||||
from .tab_bar import TabBar, TabBarData
|
||||
from .typing import SessionTab, SessionType, TypedDict
|
||||
from .utils import log_error, resolved_shell
|
||||
from .window import Watchers, Window, WindowDict
|
||||
from .window_list import WindowList
|
||||
|
||||
|
||||
class TabDict(TypedDict):
|
||||
@ -90,7 +93,7 @@ class Tab: # {{{
|
||||
self.name = getattr(session_tab, 'name', '')
|
||||
self.enabled_layouts = [x.lower() for x in getattr(session_tab, 'enabled_layouts', None) or self.opts.enabled_layouts]
|
||||
self.borders = Borders(self.os_window_id, self.id, self.opts)
|
||||
self.windows: List[Window] = []
|
||||
self.windows = WindowList()
|
||||
for i, which in enumerate('first second third fourth fifth sixth seventh eighth ninth tenth'.split()):
|
||||
setattr(self, which + '_window', partial(self.nth_window, num=i))
|
||||
self._last_used_layout: Optional[str] = None
|
||||
@ -118,16 +121,14 @@ class Tab: # {{{
|
||||
if other_tab._current_layout_name:
|
||||
self._set_current_layout(other_tab._current_layout_name)
|
||||
self._last_used_layout = other_tab._last_used_layout
|
||||
|
||||
orig_windows = list(other_tab.windows)
|
||||
orig_history = deque(other_tab.active_window_history)
|
||||
orig_active = other_tab._active_window_idx
|
||||
for window in other_tab.windows:
|
||||
detach_window(other_tab.os_window_id, other_tab.id, window.id)
|
||||
other_tab.windows = []
|
||||
other_tab._active_window_idx = 0
|
||||
self.active_window_history = orig_history
|
||||
self.windows = orig_windows
|
||||
self.windows = other_tab.windows
|
||||
other_tab.windows = WindowList()
|
||||
self._active_window_idx = orig_active
|
||||
for window in self.windows:
|
||||
window.change_tab(self)
|
||||
@ -166,19 +167,11 @@ class Tab: # {{{
|
||||
|
||||
@active_window_idx.setter
|
||||
def active_window_idx(self, val: int) -> None:
|
||||
try:
|
||||
old_active_window: Optional[Window] = self.windows[self._active_window_idx]
|
||||
except Exception:
|
||||
old_active_window = None
|
||||
else:
|
||||
assert old_active_window is not None
|
||||
wid = old_active_window.id if old_active_window.overlay_for is None else old_active_window.overlay_for
|
||||
add_active_id_to_history(self.active_window_history, wid)
|
||||
self._active_window_idx = max(0, min(val, len(self.windows) - 1))
|
||||
try:
|
||||
new_active_window: Optional[Window] = self.windows[self._active_window_idx]
|
||||
except Exception:
|
||||
new_active_window = None
|
||||
old_active_window = self.windows.active_window_for_idx(self._active_window_idx)
|
||||
if old_active_window is not None:
|
||||
add_active_id_to_history(self.active_window_history, self.windows.overlaid_window_for(old_active_window))
|
||||
self._active_window_idx = max(0, min(val, self.windows.max_active_idx))
|
||||
new_active_window = self.windows.active_window_for_idx(self._active_window_idx)
|
||||
if old_active_window is not new_active_window:
|
||||
if old_active_window is not None:
|
||||
old_active_window.focus_changed(False)
|
||||
@ -194,7 +187,7 @@ class Tab: # {{{
|
||||
|
||||
@property
|
||||
def active_window(self) -> Optional[Window]:
|
||||
return self.windows[self.active_window_idx] if self.windows else None
|
||||
return self.windows.active_window_for_idx(self.active_window_idx)
|
||||
|
||||
@property
|
||||
def title(self) -> str:
|
||||
@ -231,7 +224,7 @@ class Tab: # {{{
|
||||
def relayout_borders(self) -> None:
|
||||
tm = self.tab_manager_ref()
|
||||
if tm is not None:
|
||||
visible_windows = [w for w in self.windows if w.is_visible_in_layout]
|
||||
visible_windows = list(self.visible_windows())
|
||||
w = self.active_window
|
||||
ly = self.current_layout
|
||||
self.borders(
|
||||
@ -284,9 +277,9 @@ class Tab: # {{{
|
||||
raise ValueError(increment)
|
||||
is_horizontal = quality in ('wider', 'narrower')
|
||||
increment *= 1 if quality in ('wider', 'taller') else -1
|
||||
if self.resize_window_by(
|
||||
self.windows[self.active_window_idx].id,
|
||||
increment, is_horizontal) is not None:
|
||||
w = self.active_window
|
||||
if w is not None and self.resize_window_by(
|
||||
w.id, increment, is_horizontal) is not None:
|
||||
ring_bell()
|
||||
|
||||
def reset_window_sizes(self) -> None:
|
||||
@ -359,9 +352,7 @@ class Tab: # {{{
|
||||
copy_colors_from=copy_colors_from, watchers=watchers
|
||||
)
|
||||
if overlay_for is not None:
|
||||
overlaid = next(w for w in self.windows if w.id == overlay_for)
|
||||
window.overlay_for = overlay_for
|
||||
overlaid.overlay_window_id = window.id
|
||||
# Must add child before laying out so that resize_pty succeeds
|
||||
get_boss().add_child(window)
|
||||
self._add_window(window, location=location)
|
||||
@ -389,12 +380,13 @@ class Tab: # {{{
|
||||
)
|
||||
|
||||
def close_window(self) -> None:
|
||||
if self.windows:
|
||||
self.remove_window(self.windows[self.active_window_idx])
|
||||
w = self.active_window
|
||||
if w is not None:
|
||||
self.remove_window(w)
|
||||
|
||||
def close_other_windows_in_tab(self) -> None:
|
||||
if len(self.windows) > 1:
|
||||
active_window = self.windows[self.active_window_idx]
|
||||
active_window = self.active_window
|
||||
for window in tuple(self.windows):
|
||||
if window is not active_window:
|
||||
self.remove_window(window)
|
||||
@ -404,28 +396,25 @@ class Tab: # {{{
|
||||
old_window_id = self.active_window_history[-num]
|
||||
except IndexError:
|
||||
return None
|
||||
for idx, w in enumerate(self.windows):
|
||||
if w.id == old_window_id:
|
||||
return idx
|
||||
return self.windows.idx_for_window(old_window_id)
|
||||
|
||||
def remove_window(self, window: Window, destroy: bool = True) -> None:
|
||||
idx = self.previous_active_window_idx(1)
|
||||
next_window_id = None
|
||||
if idx is not None:
|
||||
next_window_id = self.windows[idx].id
|
||||
next_window_id = self.windows.next_id_in_stack_on_remove(window)
|
||||
if next_window_id is None:
|
||||
try:
|
||||
next_window_id = self.active_window_history[-1]
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
active_window_idx = self.current_layout.remove_window(self.windows, window, self.active_window_idx)
|
||||
if destroy:
|
||||
remove_window(self.os_window_id, self.id, window.id)
|
||||
else:
|
||||
detach_window(self.os_window_id, self.id, window.id)
|
||||
if window.overlay_for is not None:
|
||||
for idx, q in enumerate(self.windows):
|
||||
if q.id == window.overlay_for:
|
||||
active_window_idx = idx
|
||||
next_window_id = q.id
|
||||
break
|
||||
if next_window_id is None and len(self.windows) > active_window_idx:
|
||||
next_window_id = self.windows[active_window_idx].id
|
||||
if next_window_id is None:
|
||||
w = self.windows.active_window_for_idx(active_window_idx)
|
||||
if w is not None:
|
||||
next_window_id = w.id
|
||||
if next_window_id is not None:
|
||||
for idx, window in enumerate(self.windows):
|
||||
if window.id == next_window_id:
|
||||
@ -441,26 +430,12 @@ class Tab: # {{{
|
||||
if active_window:
|
||||
self.title_changed(active_window)
|
||||
|
||||
def detach_window(self, window: Window) -> Tuple[Optional[Window], Optional[Window]]:
|
||||
underlaid_window: Optional[Window] = None
|
||||
overlaid_window: Optional[Window] = window
|
||||
if window.overlay_for:
|
||||
for x in self.windows:
|
||||
if x.id == window.overlay_for:
|
||||
underlaid_window = x
|
||||
break
|
||||
elif window.overlay_window_id:
|
||||
underlaid_window = window
|
||||
overlaid_window = None
|
||||
for x in self.windows:
|
||||
if x.id == window.overlay_window_id:
|
||||
overlaid_window = x
|
||||
break
|
||||
if overlaid_window is not None:
|
||||
self.remove_window(overlaid_window, destroy=False)
|
||||
if underlaid_window is not None:
|
||||
self.remove_window(underlaid_window, destroy=False)
|
||||
return underlaid_window, overlaid_window
|
||||
def detach_window(self, window: Window) -> Tuple[Window, ...]:
|
||||
windows = list(self.windows.iter_stack_for_window(window))
|
||||
windows.sort(key=attrgetter('id')) # since ids increase in order of creation
|
||||
for w in reversed(windows):
|
||||
self.remove_window(w, destroy=False)
|
||||
return tuple(windows)
|
||||
|
||||
def attach_window(self, window: Window) -> None:
|
||||
window.change_tab(self)
|
||||
@ -473,10 +448,8 @@ class Tab: # {{{
|
||||
self.relayout_borders()
|
||||
|
||||
def set_active_window(self, window: Window) -> None:
|
||||
try:
|
||||
idx = self.windows.index(window)
|
||||
except ValueError:
|
||||
return
|
||||
idx = self.windows.idx_for_window(window)
|
||||
if idx is not None:
|
||||
self.set_active_window_idx(idx)
|
||||
|
||||
def get_nth_window(self, n: int) -> Optional[Window]:
|
||||
@ -551,7 +524,7 @@ class Tab: # {{{
|
||||
evict_cached_layouts(self.id)
|
||||
for w in self.windows:
|
||||
w.destroy()
|
||||
self.windows = []
|
||||
self.windows = WindowList()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return 'Tab(title={}, id={})'.format(self.name or self.title, hex(id(self)))
|
||||
|
||||
@ -224,13 +224,12 @@ class Window:
|
||||
self.pty_resized_once = False
|
||||
self.needs_attention = False
|
||||
self.override_title = override_title
|
||||
self.overlay_window_id: Optional[int] = None
|
||||
self.overlay_for: Optional[int] = None
|
||||
self.default_title = os.path.basename(child.argv[0] or appname)
|
||||
self.child_title = self.default_title
|
||||
self.title_stack: Deque[str] = deque(maxlen=10)
|
||||
self.allow_remote_control = child.allow_remote_control
|
||||
self.id = add_window(tab.os_window_id, tab.id, self.title)
|
||||
self.id: int = add_window(tab.os_window_id, tab.id, self.title)
|
||||
self.margin = EdgeWidths()
|
||||
self.padding = EdgeWidths()
|
||||
if not self.id:
|
||||
@ -297,8 +296,8 @@ class Window:
|
||||
return self.override_title or self.child_title
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return 'Window(title={}, id={}, overlay_for={}, overlay_window_id={})'.format(
|
||||
self.title, self.id, self.overlay_for, self.overlay_window_id)
|
||||
return 'Window(title={}, id={}, overlay_for={})'.format(
|
||||
self.title, self.id, self.overlay_for)
|
||||
|
||||
def as_dict(self, is_focused: bool = False) -> WindowDict:
|
||||
return dict(
|
||||
@ -321,7 +320,6 @@ class Window:
|
||||
'default_title': self.default_title,
|
||||
'title_stack': list(self.title_stack),
|
||||
'allow_remote_control': self.allow_remote_control,
|
||||
'overlay_window_id': self.overlay_window_id,
|
||||
'overlay_for': self.overlay_for,
|
||||
'cwd': self.child.current_cwd or self.child.cwd,
|
||||
'env': self.child.environ,
|
||||
|
||||
90
kitty/window_list.py
Normal file
90
kitty/window_list.py
Normal file
@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
from typing import Dict, Generator, Iterator, List, Optional, Union
|
||||
|
||||
from .typing import WindowType
|
||||
|
||||
|
||||
class WindowList:
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.all_windows: List[WindowType] = []
|
||||
self.id_map: Dict[int, WindowType] = {}
|
||||
self.overlay_stacks: Dict[int, List[int]] = {}
|
||||
self.id_to_idx_map: Dict[int, int] = {}
|
||||
self.idx_to_base_id_map: Dict[int, int] = {}
|
||||
self.max_active_idx = 0
|
||||
|
||||
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: WindowType) -> bool:
|
||||
return window.id in self.id_map
|
||||
|
||||
def stack_for_window_id(self, q: int) -> List[int]:
|
||||
' The stack of overlaid windows this window belongs to '
|
||||
w = self.id_map[q]
|
||||
if w.overlay_for is not None and w.overlay_for in self.id_map:
|
||||
q = self.id_map[w.overlay_for].id
|
||||
return self.overlay_stacks[q]
|
||||
|
||||
def iter_top_level_windows(self) -> Generator[WindowType, None, None]:
|
||||
' Iterator over all top level windows '
|
||||
for stack in self.overlay_stacks.values():
|
||||
yield self.id_map[stack[-1]]
|
||||
|
||||
def iter_stack_for_window(self, x: Union[WindowType, int], reverse: bool = False) -> Generator[WindowType, None, None]:
|
||||
' Iterator over all windows in the stack for this window '
|
||||
q = x if isinstance(x, int) else x.id
|
||||
stack = self.stack_for_window_id(q)
|
||||
y = reversed(stack) if reverse else iter(stack)
|
||||
for wid in y:
|
||||
yield self.id_map[wid]
|
||||
|
||||
def overlay_for(self, x: Union[WindowType, int]) -> int:
|
||||
' id of the top-most window overlaying this window, same as this window id if not overlaid '
|
||||
q = x if isinstance(x, int) else x.id
|
||||
return self.stack_for_window_id(q)[-1]
|
||||
|
||||
def overlaid_window_for(self, x: Union[WindowType, int]) -> int:
|
||||
' id of the bottom-most window in this windows overlay stack '
|
||||
q = x if isinstance(x, int) else x.id
|
||||
return self.stack_for_window_id(q)[0]
|
||||
|
||||
def is_overlaid(self, x: Union[WindowType, int]) -> bool:
|
||||
' Return False if there is a window overlaying this one '
|
||||
q = x if isinstance(x, int) else x.id
|
||||
return self.overlay_for(q) != q
|
||||
|
||||
def idx_for_window(self, x: Union[WindowType, int]) -> Optional[int]:
|
||||
' Return the index of the window in the list of top-level windows '
|
||||
q = x if isinstance(x, int) else x.id
|
||||
return self.id_to_idx_map[q]
|
||||
|
||||
def active_window_for_idx(self, idx: int, clamp: bool = False) -> Optional[WindowType]:
|
||||
' Return the active window at the specified index '
|
||||
if clamp:
|
||||
idx = max(0, min(idx, self.max_active_idx))
|
||||
q = self.idx_to_base_id_map.get(idx)
|
||||
if q is not None:
|
||||
return self.id_map[self.overlay_stacks[q][-1]]
|
||||
return None
|
||||
|
||||
def next_id_in_stack_on_remove(self, x: Union[WindowType, int]) -> Optional[int]:
|
||||
' The id of the window that should become active when this window is removed, or None if there is no other window in the stack '
|
||||
q = x if isinstance(x, int) else x.id
|
||||
stack = self.stack_for_window_id(q)
|
||||
idx = stack.index(q)
|
||||
if idx < len(stack) - 1:
|
||||
return stack[idx + 1]
|
||||
if idx > 0:
|
||||
return stack[idx - 1]
|
||||
return None
|
||||
Loading…
x
Reference in New Issue
Block a user