#!/usr/bin/env python3 # vim:fileencoding=utf-8 # License: GPL v3 Copyright: 2016, Kovid Goyal from functools import lru_cache, partial from itertools import islice, repeat from typing import ( TYPE_CHECKING, Callable, Collection, Deque, Dict, FrozenSet, Generator, Iterable, List, NamedTuple, Optional, Sequence, Tuple, Union, cast ) from .constants import WindowGeometry from .fast_data_types import ( Region, set_active_window, swap_windows, viewport_for_window ) from .options_stub import Options try: from typing import TypedDict, Literal EdgeLiteral = Literal['left', 'top', 'right', 'bottom'] except ImportError: TypedDict = dict EdgeLiteral = str # type: ignore if TYPE_CHECKING: from .window import Window Window else: Window = object class Borders(NamedTuple): left: bool top: bool right: bool bottom: bool class LayoutOpts: def __init__(self, data: Dict[str, str]): pass # Utils {{{ central = Region((0, 0, 199, 199, 200, 200)) cell_width = cell_height = 20 all_borders = Borders(True, True, True, True) no_borders = Borders(False, False, False, False) draw_minimal_borders = False draw_active_borders = True align_top_left = False LayoutDimension = Generator[Tuple[int, int], None, None] DecorationPairs = Sequence[Tuple[int, int]] WindowList = Union[List[Window], Deque[Window]] 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, ...] def idx_for_id(win_id: int, windows: Iterable[Window]) -> Optional[int]: for i, w in enumerate(windows): if w.id == win_id: return i def set_layout_options(opts: Options) -> None: global draw_minimal_borders, draw_active_borders, align_top_left draw_minimal_borders = opts.draw_minimal_borders and opts.window_margin_width == 0 draw_active_borders = opts.active_border_color is not None align_top_left = opts.placement_strategy == 'top-left' 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_per_window = number_of_cells // number_of_windows extra -= space_needed_for_decorations pos = start_at if not left_align: pos += extra // 2 def calc_window_geom(i: int, cells_in_window: int) -> int: nonlocal pos pos += decoration_pairs[i][0] inner_length = cells_in_window * cell_length return inner_length + decoration_pairs[i][1] 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 for i, cells_per_window in enumerate(cells_map): window_length = calc_window_geom(i, cells_per_window) yield pos, cells_per_window pos += window_length class Rect(NamedTuple): left: int top: int right: int bottom: int def process_overlaid_windows(all_windows: WindowList) -> Tuple[FrozenSet[Window], WindowList]: id_map = {w.id: w for w in all_windows} overlaid_windows = frozenset(w for w in all_windows if w.overlay_window_id is not None and w.overlay_window_id in id_map) windows = [w for w in all_windows if w not in overlaid_windows] return overlaid_windows, windows def window_geometry(xstart: int, xnum: int, ystart: int, ynum: int) -> WindowGeometry: return WindowGeometry(left=xstart, top=ystart, xnum=xnum, ynum=ynum, right=xstart + cell_width * xnum, bottom=ystart + cell_height * ynum) def layout_single_window(xdecoration_pairs: DecorationPairs, ydecoration_pairs: DecorationPairs, left_align: bool = False) -> WindowGeometry: xstart, xnum = next(layout_dimension(central.left, central.width, cell_width, xdecoration_pairs, left_align=align_top_left)) ystart, ynum = next(layout_dimension(central.top, central.height, cell_height, ydecoration_pairs, left_align=align_top_left)) return window_geometry(xstart, xnum, ystart, ynum) def left_blank_rect(w: Window, rects: List[Rect]) -> None: lt = w.geometry.left if lt > central.left: rects.append(Rect(central.left, central.top, lt, central.bottom + 1)) def right_blank_rect(w: Window, rects: List[Rect]) -> None: r = w.geometry.right if r < central.right: rects.append(Rect(r, central.top, central.right + 1, central.bottom + 1)) def top_blank_rect(w: Window, rects: List[Rect]) -> None: t = w.geometry.top if t > central.top: rects.append(Rect(central.left, central.top, central.right + 1, t)) def bottom_blank_rect(w: Window, rects: List[Rect]) -> None: b = w.geometry.bottom # Need to use <= here as otherwise a single pixel row at the bottom of the # window is sometimes not covered. See https://github.com/kovidgoyal/kitty/issues/506 if b <= central.bottom: rects.append(Rect(central.left, b, central.right + 1, central.bottom + 1)) def blank_rects_for_window(w: Window) -> List[Rect]: ans: List[Rect] = [] left_blank_rect(w, ans) top_blank_rect(w, ans) right_blank_rect(w, ans) bottom_blank_rect(w, ans) return ans 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 needs_all_windows = False layout_opts = LayoutOpts({}) only_active_window_visible = False def __init__( self, os_window_id: int, tab_id: int, margin_width: int, single_window_margin_width: int, padding_width: int, border_width: 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) self.border_width = border_width self.margin_width = margin_width self.single_window_margin_width = single_window_margin_width self.padding_width = padding_width # 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 update_sizes(self, margin_width: int, single_window_margin_width: int, padding_width: int, border_width: int) -> None: self.border_width = border_width self.margin_width = margin_width self.single_window_margin_width = single_window_margin_width self.padding_width = padding_width def bias_increment_for_cell(self, is_horizontal: bool) -> float: self._set_dimensions() if is_horizontal: return (cell_width + 1) / central.width return (cell_height + 1) / central.height def apply_bias(self, idx: int, increment: float, num_windows: int, 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 = idx_for_id(window_id, all_windows) if idx is None: return False w = all_windows[idx] windows = process_overlaid_windows(all_windows)[1] 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) if idx is not None: return self.apply_bias(idx, increment, len(windows), is_horizontal) return False 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) -> Window: windows = process_overlaid_windows(all_windows)[1] w = windows[min(num, len(windows) - 1)] return w def activate_nth_window(self, all_windows: WindowList, num: int) -> int: w = self.nth_window(all_windows, num) active_window_idx = idx_for_id(w.id, all_windows) 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 = all_windows[active_window_idx] windows = process_overlaid_windows(all_windows)[1] 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 aidx = (idx + len(windows) + delta) % len(windows) ans = idx_for_id(windows[aidx].id, all_windows) assert ans is not None return self.set_active_window(all_windows, ans) def neighbors(self, all_windows: WindowList, active_window_idx: int) -> NeighborsMap: w = all_windows[active_window_idx] if self.needs_all_windows: windows = all_windows else: windows = process_overlaid_windows(all_windows)[1] n = self.neighbors_for_window(w, windows) def as_indices(windows: Iterable[int]) -> Generator[int, None, None]: for w in windows: idx = idx_for_id(w, all_windows) 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 w = all_windows[active_window_idx] windows = process_overlaid_windows(all_windows)[1] if len(windows) < 2 or not delta: 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: Window, 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: Window, 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: Window, 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: Window, overlaid_windows: Optional[FrozenSet[Window]] = 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: global central, cell_width, cell_height central, tab_bar, vw, vh, cell_width, 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: Window) -> None: mw = self.margin_width if self.single_window_margin_width < 0 else self.single_window_margin_width decoration_pairs = ((self.padding_width + mw, self.padding_width + mw),) wg = layout_single_window(decoration_pairs, decoration_pairs) w.set_geometry(0, wg) self.blank_rects = blank_rects_for_window(w) def xlayout( self, num: int, bias: Optional[Sequence[float]] = None, left: Optional[int] = None, width: Optional[int] = None ) -> LayoutDimension: decoration = self.margin_width + self.border_width + self.padding_width decoration_pairs = tuple(repeat((decoration, decoration), num)) if left is None: left = central.left if width is None: width = central.width return layout_dimension(left, width, cell_width, decoration_pairs, bias=bias, left_align=align_top_left) def ylayout( self, num: int, left_align: bool = True, bias: Optional[Sequence[float]] = None, top: Optional[int] = None, height: Optional[int] = None ) -> LayoutDimension: decoration = self.margin_width + self.border_width + self.padding_width decoration_pairs = tuple(repeat((decoration, decoration), num)) if top is None: top = central.top if height is None: height = central.height return layout_dimension(top, height, cell_height, decoration_pairs, bias=bias, left_align=align_top_left) def simple_blank_rects(self, first_window: Window, last_window: Window) -> None: br = self.blank_rects left_blank_rect(first_window, br) top_blank_rect(first_window, br) right_blank_rect(last_window, br) def between_blank_rect(self, left_window: Window, right_window: Window, vertical: bool = True) -> None: if vertical: self.blank_rects.append(Rect(left_window.geometry.right, central.top, right_window.geometry.left, central.bottom + 1)) else: self.blank_rects.append(Rect(central.left, left_window.geometry.top, central.right + 1, right_window.geometry.bottom)) def bottom_blank_rect(self, window: Window) -> None: self.blank_rects.append(Rect(window.geometry.left, window.geometry.bottom, window.geometry.right, central.bottom + 1)) # }}} 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: Window, windows: WindowList) -> InternalNeighborsMap: return {'left': [], 'right': [], 'top': [], 'bottom': []} def compute_needs_borders_map(self, windows: WindowList, active_window: Optional[Window]) -> Dict[int, bool]: return {w.id: ((w is active_window and draw_active_borders) or w.needs_attention) for w in windows} def resolve_borders(self, windows: WindowList, active_window: Optional[Window]) -> Generator[Borders, None, None]: if 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[Window] = None) -> Generator[Tuple[int, int, int, int], None, None]: return yield (0, 0, 0, 0) # type: ignore def minimal_borders(self, windows: WindowList, active_window: Optional[Window], needs_borders_map: Dict[int, bool]) -> Generator[Borders, None, None]: for w in windows: if (w is active_window and 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 # }}} 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: mw = self.margin_width if self.single_window_margin_width < 0 else self.single_window_margin_width decoration_pairs = ((mw + self.padding_width, mw + self.padding_width),) wg = layout_single_window(decoration_pairs, decoration_pairs, left_align=align_top_left) for i, w in enumerate(windows): w.set_geometry(i, wg) if w.is_visible_in_layout: self.blank_rects = blank_rects_for_window(w) # }}} # Tall {{{ def neighbors_for_tall_window(num_full_size_windows: int, window: Window, 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({}) @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, num_windows: int, biased_map: Dict[int, float]) -> LayoutDimension: num_windows -= self.num_full_size_windows bias = variable_bias(num_windows, biased_map) if num_windows > 1 else None if self.main_is_horizontal: return self.ylayout(num_windows, bias=bias) return self.xlayout(num_windows, bias=bias) def apply_bias(self, idx: int, increment: float, num_windows: int, is_horizontal: bool = True) -> bool: 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(num_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(num_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: return self.layout_single_window(windows[0]) y, ynum = next(self.ylayout(1)) if len(windows) <= self.num_full_size_windows: bias = normalize_biases(self.main_bias[:-1]) xlayout = self.xlayout(self.num_full_size_windows, bias=bias) for i, (w, (x, xnum)) in enumerate(zip(windows, xlayout)): w.set_geometry(i, window_geometry(x, xnum, y, ynum)) if i > 0: self.between_blank_rect(windows[i-1], windows[i]) # bottom blank rect self.bottom_blank_rect(windows[i]) # left, top and right blank rects self.simple_blank_rects(windows[0], windows[-1]) return xlayout = self.xlayout(self.num_full_size_windows + 1, bias=self.main_bias) for i in range(self.num_full_size_windows): w = windows[i] x, xnum = next(xlayout) w.set_geometry(i, window_geometry(x, xnum, y, ynum)) self.between_blank_rect(windows[i], windows[i+1]) # bottom blank rect self.bottom_blank_rect(windows[i]) x, xnum = next(xlayout) ylayout = self.variable_layout(len(windows), self.biased_map) for i, (w, (ystart, ynum)) in enumerate(zip(islice(windows, self.num_full_size_windows, None), ylayout)): w.set_geometry(i + self.num_full_size_windows, window_geometry(x, xnum, ystart, ynum)) # right bottom blank rect self.bottom_blank_rect(windows[-1]) # left, top and right blank rects self.simple_blank_rects(windows[0], windows[-1]) def neighbors_for_window(self, window: Window, windows: WindowList) -> InternalNeighborsMap: return neighbors_for_tall_window(self.num_full_size_windows, window, windows) def minimal_borders(self, windows: WindowList, active_window: Optional[Window], 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) def do_layout(self, windows: WindowList, active_window_idx: int) -> None: if len(windows) == 1: return self.layout_single_window(windows[0]) x, xnum = next(self.xlayout(1)) if len(windows) <= self.num_full_size_windows: bias = normalize_biases(self.main_bias[:-1]) ylayout = self.ylayout(self.num_full_size_windows, bias=bias) for i, (w, (y, ynum)) in enumerate(zip(windows, ylayout)): w.set_geometry(i, window_geometry(x, xnum, y, ynum)) if i > 0: self.between_blank_rect(windows[i-1], windows[i], vertical=False) # bottom blank rect self.bottom_blank_rect(windows[-1]) # left, top and right blank rects self.simple_blank_rects(windows[0], windows[-1]) return ylayout = self.ylayout(self.num_full_size_windows + 1, bias=self.main_bias) for i in range(self.num_full_size_windows): w = windows[i] y, ynum = next(ylayout) w.set_geometry(i, window_geometry(x, xnum, y, ynum)) self.between_blank_rect(windows[i], windows[i+1], vertical=False) y, ynum = next(ylayout) xlayout = self.variable_layout(len(windows), self.biased_map) for i, (w, (x, xnum)) in enumerate(zip(islice(windows, self.num_full_size_windows, None), xlayout)): w.set_geometry(i + self.num_full_size_windows, window_geometry(x, xnum, y, ynum)) # bottom blank rect self.bottom_blank_rect(windows[i]) # left, top and right blank rects self.simple_blank_rects(windows[0], windows[-1]) def neighbors_for_window(self, window: Window, 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 # }}} # Grid {{{ @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 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, num_windows: int, is_horizontal: bool = True) -> bool: b = self.biased_cols if is_horizontal else self.biased_rows 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, xstart, xnum, ystart, ynum 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(num_windows: int, bias: Optional[Sequence[float]] = None) -> LayoutDimension: return self.xlayout(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(num_windows: int, bias: Optional[Sequence[float]] = None) -> LayoutDimension: return self.xlayout(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, int, int, int, int], None, None]: # Distribute windows top-to-bottom, left-to-right (i.e. in columns) xlayout = self.variable_layout(self.xlayout, ncols, self.biased_cols) yvals_normal = tuple(self.variable_layout(self.ylayout, nrows, self.biased_rows)) yvals_special = yvals_normal if special_rows == nrows else tuple(self.variable_layout(self.ylayout, special_rows, self.biased_rows)) pos = 0 for col in range(ncols): rows = special_rows if col == special_col else nrows yl = yvals_special if col == special_col else yvals_normal xstart, xnum = next(xlayout) col_windows = [] for i, (ystart, ynum) in enumerate(yl): window_idx = pos + i yield window_idx, xstart, xnum, ystart, ynum 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: return self.layout_single_window(windows[0]) 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) # bottom blank rect self.bottom_blank_rect(col_windows_w[-1]) for window_idx, xstart, xnum, ystart, ynum in self.layout_windows( len(windows), nrows, ncols, special_rows, special_col, on_col_done): w = windows[window_idx] w.set_geometry(window_idx, window_geometry(xstart, xnum, ystart, ynum)) # left, top and right blank rects self.simple_blank_rects(windows[0], windows[-1]) # the in-between columns blank rects for i in range(ncols - 1): self.between_blank_rect(win_col_map[i][0], win_col_map[i + 1][0]) def minimal_borders(self, windows: WindowList, active_window: Optional[Window], needs_borders_map: Dict[int, bool]) -> Generator[Borders, None, None]: n = len(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 = {} col_counts = [] for col in range(ncols): rows = special_rows if col == special_col else nrows for row in range(rows): matrix[row][col] = wid = next(wi).id pos_map[wid] = row, col col_counts.append(rows) for w in windows: wid = w.id if needs_borders_map[wid]: yield all_borders continue row, col = pos_map[wid] if col + 1 < ncols: next_col_has_different_count = col_counts[col + 1] != col_counts[col] right_neighbor_id: Optional[int] = matrix[row][col+1] else: right_neighbor_id = None next_col_has_different_count = False try: bottom_neighbor_id: Optional[int] = matrix[row+1][col] except IndexError: bottom_neighbor_id = None yield Borders( False, False, (right_neighbor_id is not None and not needs_borders_map[right_neighbor_id]) or next_col_has_different_count, bottom_neighbor_id is not None and not needs_borders_map[bottom_neighbor_id] ) def neighbors_for_window(self, window: Window, 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 [], } # }}} class Vertical(Layout): # {{{ name = 'vertical' main_is_horizontal = False only_between_border = Borders(False, False, False, True) def variable_layout(self, num_windows: int, biased_map: Dict[int, float]) -> LayoutDimension: bias = variable_bias(num_windows, biased_map) if num_windows else None if self.main_is_horizontal: return self.xlayout(num_windows, bias=bias) return self.ylayout(num_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, num_windows: int, is_horizontal: bool = True) -> bool: if self.main_is_horizontal != is_horizontal: return False if num_windows < 2: return False before_layout = list(self.variable_layout(num_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(num_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: return self.layout_single_window(windows[0]) xlayout = self.xlayout(1) xstart, xnum = next(xlayout) ylayout = self.variable_layout(window_count, self.biased_map) for i, (w, (ystart, ynum)) in enumerate(zip(windows, ylayout)): w.set_geometry(i, window_geometry(xstart, xnum, ystart, ynum)) # bottom blank rect self.bottom_blank_rect(windows[i]) # left, top and right blank rects self.simple_blank_rects(windows[0], windows[-1]) def minimal_borders(self, windows: WindowList, active_window: Optional[Window], 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: Window, 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) def do_layout(self, windows: WindowList, active_window_idx: int) -> None: window_count = len(windows) if window_count == 1: return self.layout_single_window(windows[0]) xlayout = self.variable_layout(window_count, self.biased_map) ylayout = self.ylayout(1) ystart, ynum = next(ylayout) for i, (w, (xstart, xnum)) in enumerate(zip(windows, xlayout)): w.set_geometry(i, window_geometry(xstart, xnum, ystart, ynum)) if i > 0: # between blank rect self.between_blank_rect(windows[i - 1], windows[i]) # left, top and right blank rects self.simple_blank_rects(windows[0], windows[-1]) # bottom blank rect self.blank_rects.append(Rect(windows[0].geometry.left, windows[0].geometry.bottom, windows[-1].geometry.right, central.bottom + 1)) # }}} # Splits {{{ 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[Tuple[int, int, int, int]] = 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, Window], id_idx_map: Dict[int, int]) -> 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) def blank_rects_for_window(self, layout_object: Layout, window: Window, left: int, top: int, width: int, height: int) -> None: right = left + width - 1 bottom = top + height - 1 g = window.geometry rects = layout_object.blank_rects lt = g.left if lt > left: rects.append(Rect(left, top, lt, bottom + 1)) r = g.right if r <= right: rects.append(Rect(r, top, right + 1, bottom + 1)) t = g.top if t > top: rects.append(Rect(left, top, right + 1, t)) b = g.bottom if b <= bottom: rects.append(Rect(left, b, right + 1, bottom + 1)) def layout_pair( self, left: int, top: int, width: int, height: int, id_window_map: Dict[int, Window], 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 xstart, xnum = next(layout_object.xlayout(1, left=left, width=width)) ystart, ynum = next(layout_object.ylayout(1, top=top, height=height)) geom = window_geometry(xstart, xnum, ystart, ynum) self.apply_window_geometry(q, geom, id_window_map, id_idx_map) self.blank_rects_for_window(layout_object, id_window_map[q], left, top, width, height) return bw = layout_object.border_width if draw_minimal_borders else 0 b1 = bw // 2 b2 = bw - b1 if self.horizontal: ystart, ynum = next(layout_object.ylayout(1, top=top, height=height)) w1 = max(2*cell_width + 1, int(self.bias * width) - b1) w2 = max(2*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: xstart, xnum = next(layout_object.xlayout(1, left=left, width=w1)) self.apply_window_geometry(self.one, window_geometry(xstart, xnum, ystart, ynum), id_window_map, id_idx_map) self.blank_rects_for_window(layout_object, id_window_map[self.one], left, top, w1, height) if b1 + b2: self.between_border = (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: xstart, xnum = next(layout_object.xlayout(1, left=left + w1, width=w2)) self.apply_window_geometry(self.two, window_geometry(xstart, xnum, ystart, ynum), id_window_map, id_idx_map) self.blank_rects_for_window(layout_object, id_window_map[self.two], left + w1, top, w2, height) else: xstart, xnum = next(layout_object.xlayout(1, left=left, width=width)) h1 = max(2*cell_height + 1, int(self.bias * height) - b1) h2 = max(2*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: ystart, ynum = next(layout_object.ylayout(1, top=top, height=h1)) self.apply_window_geometry(self.one, window_geometry(xstart, xnum, ystart, ynum), id_window_map, id_idx_map) self.blank_rects_for_window(layout_object, id_window_map[self.one], left, top, width, h1) if b1 + b2: self.between_border = (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: ystart, ynum = next(layout_object.ylayout(1, top=top + h1, height=h2)) self.apply_window_geometry(self.two, window_geometry(xstart, xnum, ystart, ynum), id_window_map, id_idx_map) self.blank_rects_for_window(layout_object, id_window_map[self.two], left, top + h1, width, h2) 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(central.left, central.top, central.width, central.height, id_window_map, id_idx_map, self) def do_add_window( self, all_windows: WindowList, window: Window, 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[Window] = None) -> Generator[Tuple[int, int, int, int], None, None]: if not 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: Window, 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 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 # }}} # Instantiation {{{ all_layouts = {cast(str, o.name): o for o in globals().values() if isinstance(o, type) and issubclass(o, Layout) and o is not Layout} class CreateLayoutObjectFor: cache: Dict[Tuple, Layout] = {} def __call__( self, name: str, os_window_id: int, tab_id: int, margin_width: int, single_window_margin_width: int, padding_width: int, border_width: int, layout_opts: str = '' ) -> Layout: key = name, os_window_id, tab_id, margin_width, single_window_margin_width, padding_width, border_width, 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, margin_width, single_window_margin_width, padding_width, border_width, 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] # }}}