• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2021 The Pigweed Authors
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may not
4# use this file except in compliance with the License. You may obtain a copy of
5# the License at
6#
7#     https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations under
13# the License.
14"""WindowManager"""
15
16import collections
17import copy
18import functools
19from itertools import chain
20import logging
21import operator
22from typing import Any, Dict, Iterable, List, Optional
23
24from prompt_toolkit.key_binding import KeyBindings
25from prompt_toolkit.layout import (
26    Dimension,
27    HSplit,
28    VSplit,
29    FormattedTextControl,
30    Window,
31    WindowAlign,
32)
33from prompt_toolkit.mouse_events import MouseEvent, MouseEventType, MouseButton
34from prompt_toolkit.widgets import MenuItem
35
36from pw_console.console_prefs import ConsolePrefs, error_unknown_window
37from pw_console.log_pane import LogPane
38from pw_console.widgets import (
39    WindowPaneToolbar,
40    to_checkbox_text,
41)
42from pw_console.widgets import mouse_handlers as pw_console_mouse_handlers
43from pw_console.window_list import WindowList, DisplayMode
44
45_LOG = logging.getLogger(__package__)
46
47# Amount for adjusting window dimensions when enlarging and shrinking.
48_WINDOW_SPLIT_ADJUST = 1
49
50
51class WindowListResizeHandle(FormattedTextControl):
52    """Button to initiate window list resize drag events."""
53
54    def __init__(
55        self, window_manager, window_list: Any, *args, **kwargs
56    ) -> None:
57        self.window_manager = window_manager
58        self.window_list = window_list
59        super().__init__(*args, **kwargs)
60
61    def mouse_handler(self, mouse_event: MouseEvent):
62        """Mouse handler for this control."""
63        # Start resize mouse drag event
64        if mouse_event.event_type == MouseEventType.MOUSE_DOWN:
65            self.window_manager.start_resize(self.window_list)
66            # Mouse event handled, return None.
67            return None
68
69        # Mouse event not handled, return NotImplemented.
70        return NotImplemented
71
72
73class WindowManagerVSplit(VSplit):
74    """PromptToolkit VSplit class with some additions for size and mouse resize.
75
76    This VSplit has a write_to_screen function that saves the width and height
77    of the container for the current render pass. It also handles overriding
78    mouse handlers for triggering window resize adjustments.
79    """
80
81    def __init__(self, parent_window_manager, *args, **kwargs):
82        # Save a reference to the parent window pane.
83        self.parent_window_manager = parent_window_manager
84        super().__init__(*args, **kwargs)
85
86    def write_to_screen(
87        self,
88        screen,
89        mouse_handlers,
90        write_position,
91        parent_style: str,
92        erase_bg: bool,
93        z_index: Optional[int],
94    ) -> None:
95        new_mouse_handlers = mouse_handlers
96        # Is resize mode active?
97        if self.parent_window_manager.resize_mode:
98            # Ignore future mouse_handler updates.
99            new_mouse_handlers = pw_console_mouse_handlers.EmptyMouseHandler()
100            # Set existing mouse_handlers to the parent_window_managers's
101            # mouse_handler. This will handle triggering resize events.
102            mouse_handlers.set_mouse_handler_for_range(
103                write_position.xpos,
104                write_position.xpos + write_position.width,
105                write_position.ypos,
106                write_position.ypos + write_position.height,
107                self.parent_window_manager.mouse_handler,
108            )
109
110        # Save the width and height for the current render pass.
111        self.parent_window_manager.update_window_manager_size(
112            write_position.width, write_position.height
113        )
114        # Continue writing content to the screen.
115        super().write_to_screen(
116            screen,
117            new_mouse_handlers,
118            write_position,
119            parent_style,
120            erase_bg,
121            z_index,
122        )
123
124
125class WindowManagerHSplit(HSplit):
126    """PromptToolkit HSplit class with some additions for size and mouse resize.
127
128    This HSplit has a write_to_screen function that saves the width and height
129    of the container for the current render pass. It also handles overriding
130    mouse handlers for triggering window resize adjustments.
131    """
132
133    def __init__(self, parent_window_manager, *args, **kwargs):
134        # Save a reference to the parent window pane.
135        self.parent_window_manager = parent_window_manager
136        super().__init__(*args, **kwargs)
137
138    def write_to_screen(
139        self,
140        screen,
141        mouse_handlers,
142        write_position,
143        parent_style: str,
144        erase_bg: bool,
145        z_index: Optional[int],
146    ) -> None:
147        new_mouse_handlers = mouse_handlers
148        # Is resize mode active?
149        if self.parent_window_manager.resize_mode:
150            # Ignore future mouse_handler updates.
151            new_mouse_handlers = pw_console_mouse_handlers.EmptyMouseHandler()
152            # Set existing mouse_handlers to the parent_window_managers's
153            # mouse_handler. This will handle triggering resize events.
154            mouse_handlers.set_mouse_handler_for_range(
155                write_position.xpos,
156                write_position.xpos + write_position.width,
157                write_position.ypos,
158                write_position.ypos + write_position.height,
159                self.parent_window_manager.mouse_handler,
160            )
161
162        # Save the width and height for the current render pass.
163        self.parent_window_manager.update_window_manager_size(
164            write_position.width, write_position.height
165        )
166        # Continue writing content to the screen.
167        super().write_to_screen(
168            screen,
169            new_mouse_handlers,
170            write_position,
171            parent_style,
172            erase_bg,
173            z_index,
174        )
175
176
177class WindowManager:
178    """WindowManager class
179
180    This class handles adding/removing/resizing windows and rendering the
181    prompt_toolkit split layout."""
182
183    # pylint: disable=too-many-public-methods,too-many-instance-attributes
184
185    def __init__(
186        self,
187        application: Any,
188    ):
189        self.application = application
190        self.window_lists: collections.deque = collections.deque()
191        self.window_lists.append(WindowList(self))
192        self.key_bindings = self._create_key_bindings()
193        self.top_toolbars: List[WindowPaneToolbar] = []
194        self.bottom_toolbars: List[WindowPaneToolbar] = []
195
196        self.resize_mode: bool = False
197        self.resize_target_window_list_index: Optional[int] = None
198        self.resize_target_window_list: Optional[int] = None
199        self.resize_current_row: int = 0
200        self.resize_current_column: int = 0
201
202        self.current_window_manager_width: int = 0
203        self.current_window_manager_height: int = 0
204        self.last_window_manager_width: int = 0
205        self.last_window_manager_height: int = 0
206
207    def update_window_manager_size(self, width, height):
208        """Save width and height for the current UI render pass."""
209        if width:
210            self.last_window_manager_width = self.current_window_manager_width
211            self.current_window_manager_width = width
212        if height:
213            self.last_window_manager_height = self.current_window_manager_height
214            self.current_window_manager_height = height
215
216        if (
217            self.current_window_manager_width != self.last_window_manager_width
218            or self.current_window_manager_height
219            != self.last_window_manager_height
220        ):
221            self.rebalance_window_list_sizes()
222
223    def _set_window_list_sizes(
224        self, new_heights: List[int], new_widths: List[int]
225    ) -> None:
226        for window_list in self.window_lists:
227            window_list.height = Dimension(preferred=new_heights[0])
228            new_heights = new_heights[1:]
229            window_list.width = Dimension(preferred=new_widths[0])
230            new_widths = new_widths[1:]
231
232    def vertical_window_list_spliting(self) -> bool:
233        return self.application.prefs.window_column_split_method == 'vertical'
234
235    def rebalance_window_list_sizes(self) -> None:
236        """Adjust relative split sizes to fill available space."""
237        available_height = self.current_window_manager_height
238        available_width = self.current_window_manager_width
239
240        old_heights = [w.height.preferred for w in self.window_lists]
241        old_widths = [w.width.preferred for w in self.window_lists]
242
243        # Make sure the old totals are not zero.
244        old_height_total = max(sum(old_heights), 1)
245        old_width_total = max(sum(old_widths), 1)
246
247        height_percentages = [value / old_height_total for value in old_heights]
248        width_percentages = [value / old_width_total for value in old_widths]
249
250        new_heights = [
251            int(available_height * percentage)
252            for percentage in height_percentages
253        ]
254        new_widths = [
255            int(available_width * percentage)
256            for percentage in width_percentages
257        ]
258
259        if self.vertical_window_list_spliting():
260            new_heights = [
261                self.current_window_manager_height for h in new_heights
262            ]
263        else:
264            new_widths = [self.current_window_manager_width for h in new_widths]
265
266        self._set_window_list_sizes(new_heights, new_widths)
267
268    def _create_key_bindings(self) -> KeyBindings:
269        key_bindings = KeyBindings()
270        register = self.application.prefs.register_keybinding
271
272        @register('window-manager.move-pane-left', key_bindings)
273        def move_pane_left(_event):
274            """Move window pane left."""
275            self.move_pane_left()
276
277        @register('window-manager.move-pane-right', key_bindings)
278        def move_pane_right(_event):
279            """Move window pane right."""
280            self.move_pane_right()
281
282        @register('window-manager.move-pane-down', key_bindings)
283        def move_pane_down(_event):
284            """Move window pane down."""
285            self.move_pane_down()
286
287        @register('window-manager.move-pane-up', key_bindings)
288        def move_pane_up(_event):
289            """Move window pane up."""
290            self.move_pane_up()
291
292        @register('window-manager.enlarge-pane', key_bindings)
293        def enlarge_pane(_event):
294            """Enlarge the active window pane."""
295            self.enlarge_pane()
296
297        @register('window-manager.shrink-pane', key_bindings)
298        def shrink_pane(_event):
299            """Shrink the active window pane."""
300            self.shrink_pane()
301
302        @register('window-manager.shrink-split', key_bindings)
303        def shrink_split(_event):
304            """Shrink the current window split."""
305            self.shrink_split()
306
307        @register('window-manager.enlarge-split', key_bindings)
308        def enlarge_split(_event):
309            """Enlarge the current window split."""
310            self.enlarge_split()
311
312        @register('window-manager.focus-prev-pane', key_bindings)
313        def focus_prev_pane(_event):
314            """Switch focus to the previous window pane or tab."""
315            self.focus_previous_pane()
316
317        @register('window-manager.focus-next-pane', key_bindings)
318        def focus_next_pane(_event):
319            """Switch focus to the next window pane or tab."""
320            self.focus_next_pane()
321
322        @register('window-manager.balance-window-panes', key_bindings)
323        def balance_window_panes(_event):
324            """Balance all window sizes."""
325            self.balance_window_sizes()
326
327        return key_bindings
328
329    def delete_empty_window_lists(self):
330        empty_lists = [
331            window_list
332            for window_list in self.window_lists
333            if window_list.empty()
334        ]
335        for empty_list in empty_lists:
336            self.window_lists.remove(empty_list)
337
338    def add_top_toolbar(self, toolbar: WindowPaneToolbar) -> None:
339        self.top_toolbars.append(toolbar)
340
341    def add_bottom_toolbar(self, toolbar: WindowPaneToolbar) -> None:
342        self.bottom_toolbars.append(toolbar)
343
344    def create_root_container(self):
345        """Create vertical or horizontal splits for all active panes."""
346        self.delete_empty_window_lists()
347
348        for window_list in self.window_lists:
349            window_list.update_container()
350
351        vertical_split = self.vertical_window_list_spliting()
352
353        window_containers = []
354        for i, window_list in enumerate(self.window_lists):
355            window_containers.append(window_list.container)
356            if (i + 1) >= len(self.window_lists):
357                continue
358
359            if vertical_split:
360                separator_padding = Window(
361                    content=WindowListResizeHandle(self, window_list, "│"),
362                    char='│',
363                    width=1,
364                    dont_extend_height=False,
365                )
366                resize_separator = HSplit(
367                    [
368                        separator_padding,
369                        Window(
370                            content=WindowListResizeHandle(
371                                self, window_list, "║\n║\n║"
372                            ),
373                            char='│',
374                            width=1,
375                            dont_extend_height=True,
376                        ),
377                        separator_padding,
378                    ],
379                    style='class:pane_separator',
380                )
381            else:
382                resize_separator = Window(
383                    content=WindowListResizeHandle(self, window_list, "════"),
384                    char='─',
385                    height=1,
386                    align=WindowAlign.CENTER,
387                    dont_extend_width=False,
388                    style='class:pane_separator',
389                )
390            window_containers.append(resize_separator)
391
392        if vertical_split:
393            split = WindowManagerVSplit(self, window_containers)
394        else:
395            split = WindowManagerHSplit(self, window_containers)
396
397        split_items = []
398        split_items.extend(self.top_toolbars)
399        split_items.append(split)
400        split_items.extend(self.bottom_toolbars)
401        return HSplit(split_items)
402
403    def update_root_container_body(self):
404        # Replace the root MenuContainer body with the new split.
405        self.application.root_container.container.content.children[
406            1
407        ] = self.create_root_container()
408
409    def _get_active_window_list_and_pane(self):
410        active_pane = None
411        active_window_list = None
412        for window_list in self.window_lists:
413            active_pane = window_list.get_current_active_pane()
414            if active_pane:
415                active_window_list = window_list
416                break
417        return active_window_list, active_pane
418
419    def window_list_index(self, window_list: WindowList) -> Optional[int]:
420        index = None
421        try:
422            index = self.window_lists.index(window_list)
423        except ValueError:
424            # Ignore ValueError which can be raised by the self.window_lists
425            # deque if the window_list can't be found.
426            pass
427        return index
428
429    def run_action_on_active_pane(self, function_name):
430        (
431            _active_window_list,
432            active_pane,
433        ) = self._get_active_window_list_and_pane()
434        if not hasattr(active_pane, function_name):
435            return
436        method_to_call = getattr(active_pane, function_name)
437        method_to_call()
438        return
439
440    def focus_previous_pane(self) -> None:
441        """Focus on the previous visible window pane or tab."""
442        self.focus_next_pane(reverse_order=True)
443
444    def focus_next_pane(self, reverse_order=False) -> None:
445        """Focus on the next visible window pane or tab."""
446        (
447            active_window_list,
448            active_pane,
449        ) = self._get_active_window_list_and_pane()
450        if active_window_list is None:
451            return
452
453        # Total count of window lists and panes
454        window_list_count = len(self.window_lists)
455        pane_count = len(active_window_list.active_panes)
456
457        # Get currently focused indices
458        active_window_list_index = self.window_list_index(active_window_list)
459        if active_window_list_index is None:
460            return
461        active_pane_index = active_window_list.pane_index(active_pane)
462
463        increment = -1 if reverse_order else 1
464        # Assume we can switch to the next pane in the current window_list
465        next_pane_index = active_pane_index + increment
466
467        # Case 1: next_pane_index does not exist in this window list.
468        # Action: Switch to the first pane of the next window list.
469        if next_pane_index >= pane_count or next_pane_index < 0:
470            # Get the next window_list
471            next_window_list_index = (
472                active_window_list_index + increment
473            ) % window_list_count
474            next_window_list = self.window_lists[next_window_list_index]
475
476            # If tabbed window mode is enabled, switch to the first tab.
477            if next_window_list.display_mode == DisplayMode.TABBED:
478                if reverse_order:
479                    next_window_list.switch_to_tab(
480                        len(next_window_list.active_panes) - 1
481                    )
482                else:
483                    next_window_list.switch_to_tab(0)
484                return
485
486            # Otherwise switch to the first visible window pane.
487            pane_list = next_window_list.active_panes
488            if reverse_order:
489                pane_list = reversed(pane_list)
490            for pane in pane_list:
491                if pane.show_pane:
492                    self.application.focus_on_container(pane)
493                    return
494
495        # Case 2: next_pane_index does exist and display mode is tabs.
496        # Action: Switch to the next tab of the current window list.
497        if active_window_list.display_mode == DisplayMode.TABBED:
498            active_window_list.switch_to_tab(next_pane_index)
499            return
500
501        # Case 3: next_pane_index does exist and display mode is stacked.
502        # Action: Switch to the next visible window pane.
503        index_range = range(1, pane_count)
504        if reverse_order:
505            index_range = range(pane_count - 1, 0, -1)
506        for i in index_range:
507            next_pane_index = (active_pane_index + i) % pane_count
508            next_pane = active_window_list.active_panes[next_pane_index]
509            if next_pane.show_pane:
510                self.application.focus_on_container(next_pane)
511                return
512        return
513
514    def move_pane_left(self):
515        (
516            active_window_list,
517            active_pane,
518        ) = self._get_active_window_list_and_pane()
519        if not active_window_list:
520            return
521
522        window_list_index = self.window_list_index(active_window_list)
523        # Move left should pick the previous window_list
524        target_window_list_index = window_list_index - 1
525
526        # Check if a new WindowList should be created on the left
527        if target_window_list_index == -1:
528            # Add the new WindowList
529            target_window_list = WindowList(self)
530            self.window_lists.appendleft(target_window_list)
531            self.reset_split_sizes()
532            # New index is 0
533            target_window_list_index = 0
534
535        # Get the destination window_list
536        target_window_list = self.window_lists[target_window_list_index]
537
538        # Move the pane
539        active_window_list.remove_pane_no_checks(active_pane)
540        target_window_list.add_pane(active_pane, add_at_beginning=True)
541        target_window_list.reset_pane_sizes()
542        self.delete_empty_window_lists()
543
544    def move_pane_right(self):
545        (
546            active_window_list,
547            active_pane,
548        ) = self._get_active_window_list_and_pane()
549        if not active_window_list:
550            return
551
552        window_list_index = self.window_list_index(active_window_list)
553        # Move right should pick the next window_list
554        target_window_list_index = window_list_index + 1
555
556        # Check if a new WindowList should be created
557        if target_window_list_index == len(self.window_lists):
558            # Add a new WindowList
559            target_window_list = WindowList(self)
560            self.window_lists.append(target_window_list)
561            self.reset_split_sizes()
562
563        # Get the destination window_list
564        target_window_list = self.window_lists[target_window_list_index]
565
566        # Move the pane
567        active_window_list.remove_pane_no_checks(active_pane)
568        target_window_list.add_pane(active_pane, add_at_beginning=True)
569        target_window_list.reset_pane_sizes()
570        self.delete_empty_window_lists()
571
572    def move_pane_up(self):
573        (
574            active_window_list,
575            _active_pane,
576        ) = self._get_active_window_list_and_pane()
577        if not active_window_list:
578            return
579
580        active_window_list.move_pane_up()
581
582    def move_pane_down(self):
583        (
584            active_window_list,
585            _active_pane,
586        ) = self._get_active_window_list_and_pane()
587        if not active_window_list:
588            return
589
590        active_window_list.move_pane_down()
591
592    def shrink_pane(self):
593        (
594            active_window_list,
595            _active_pane,
596        ) = self._get_active_window_list_and_pane()
597        if not active_window_list:
598            return
599
600        active_window_list.shrink_pane()
601
602    def enlarge_pane(self):
603        (
604            active_window_list,
605            _active_pane,
606        ) = self._get_active_window_list_and_pane()
607        if not active_window_list:
608            return
609
610        active_window_list.enlarge_pane()
611
612    def shrink_split(self):
613        if len(self.window_lists) < 2:
614            return
615
616        (
617            active_window_list,
618            _active_pane,
619        ) = self._get_active_window_list_and_pane()
620        if not active_window_list:
621            return
622
623        self.adjust_split_size(active_window_list, -_WINDOW_SPLIT_ADJUST)
624
625    def enlarge_split(self):
626        (
627            active_window_list,
628            _active_pane,
629        ) = self._get_active_window_list_and_pane()
630        if not active_window_list:
631            return
632
633        self.adjust_split_size(active_window_list, _WINDOW_SPLIT_ADJUST)
634
635    def balance_window_sizes(self):
636        """Reset all splits and pane sizes."""
637        self.reset_pane_sizes()
638        self.reset_split_sizes()
639
640    def reset_split_sizes(self):
641        """Reset all active pane width and height to defaults"""
642        available_height = self.current_window_manager_height
643        available_width = self.current_window_manager_width
644        old_heights = [w.height.preferred for w in self.window_lists]
645        old_widths = [w.width.preferred for w in self.window_lists]
646        new_heights = [int(available_height / len(old_heights))] * len(
647            old_heights
648        )
649        new_widths = [int(available_width / len(old_widths))] * len(old_widths)
650
651        self._set_window_list_sizes(new_heights, new_widths)
652
653    def _get_next_window_list_for_resizing(
654        self, window_list: WindowList
655    ) -> Optional[WindowList]:
656        window_list_index = self.window_list_index(window_list)
657        if window_list_index is None:
658            return None
659
660        next_window_list_index = (window_list_index + 1) % len(
661            self.window_lists
662        )
663
664        # Use the previous window if we are on the last split
665        if window_list_index == len(self.window_lists) - 1:
666            next_window_list_index = window_list_index - 1
667
668        next_window_list = self.window_lists[next_window_list_index]
669        return next_window_list
670
671    def adjust_split_size(
672        self, window_list: WindowList, diff: int = _WINDOW_SPLIT_ADJUST
673    ) -> None:
674        """Increase or decrease a given window_list's vertical split width."""
675        # No need to resize if only one split.
676        if len(self.window_lists) < 2:
677            return
678
679        # Get the next split to subtract from.
680        next_window_list = self._get_next_window_list_for_resizing(window_list)
681        if not next_window_list:
682            return
683
684        if self.vertical_window_list_spliting():
685            # Get current width
686            old_value = window_list.width.preferred
687            next_old_value = next_window_list.width.preferred  # type: ignore
688        else:
689            # Get current height
690            old_value = window_list.height.preferred
691            next_old_value = next_window_list.height.preferred  # type: ignore
692
693        # Add to the current split
694        new_value = old_value + diff
695        if new_value <= 0:
696            new_value = old_value
697
698        # Subtract from the next split
699        next_new_value = next_old_value - diff
700        if next_new_value <= 0:
701            next_new_value = next_old_value
702
703        # If new height is too small or no change, make no adjustments.
704        if new_value < 3 or next_new_value < 3 or old_value == new_value:
705            return
706
707        if self.vertical_window_list_spliting():
708            # Set new width
709            window_list.width.preferred = new_value
710            next_window_list.width.preferred = next_new_value  # type: ignore
711        else:
712            # Set new height
713            window_list.height.preferred = new_value
714            next_window_list.height.preferred = next_new_value  # type: ignore
715            window_list.rebalance_window_heights()
716            next_window_list.rebalance_window_heights()
717
718    def toggle_pane(self, pane):
719        """Toggle a pane on or off."""
720        window_list, _pane_index = self.find_window_list_and_pane_index(pane)
721
722        # Don't hide the window if tabbed mode is enabled. Switching to a
723        # separate tab is preffered.
724        if window_list.display_mode == DisplayMode.TABBED:
725            return
726        pane.show_pane = not pane.show_pane
727        self.update_root_container_body()
728        self.application.update_menu_items()
729
730        # Set focus to the top level menu. This has the effect of keeping the
731        # menu open if it's already open.
732        self.application.focus_main_menu()
733
734    def focus_first_visible_pane(self):
735        """Focus on the first visible container."""
736        for pane in self.active_panes():
737            if pane.show_pane:
738                self.application.application.layout.focus(pane)
739                break
740
741    def check_for_all_hidden_panes_and_unhide(self) -> None:
742        """Scan for window_lists containing only hidden panes."""
743        for window_list in self.window_lists:
744            all_hidden = all(
745                not pane.show_pane for pane in window_list.active_panes
746            )
747            if all_hidden:
748                # Unhide the first pane
749                self.toggle_pane(window_list.active_panes[0])
750
751    def add_pane_no_checks(self, pane: Any):
752        self.window_lists[0].add_pane_no_checks(pane)
753
754    def add_pane(self, pane: Any):
755        self.window_lists[0].add_pane(pane, add_at_beginning=True)
756
757    def first_window_list(self):
758        return self.window_lists[0]
759
760    def active_panes(self):
761        """Return all active panes from all window lists."""
762        return chain.from_iterable(
763            map(operator.attrgetter('active_panes'), self.window_lists)
764        )
765
766    def start_resize_pane(self, pane):
767        window_list, pane_index = self.find_window_list_and_pane_index(pane)
768        window_list.start_resize(pane, pane_index)
769
770    def mouse_resize(self, xpos, ypos):
771        if self.resize_target_window_list_index is None:
772            return
773        target_window_list = self.window_lists[
774            self.resize_target_window_list_index
775        ]
776
777        diff = ypos - self.resize_current_row
778        if self.vertical_window_list_spliting():
779            diff = xpos - self.resize_current_column
780        if diff == 0:
781            return
782
783        self.adjust_split_size(target_window_list, diff)
784        self._resize_update_current_row_column()
785        self.application.redraw_ui()
786
787    def mouse_handler(self, mouse_event: MouseEvent):
788        """MouseHandler used when resize_mode == True."""
789        mouse_position = mouse_event.position
790
791        if (
792            mouse_event.event_type == MouseEventType.MOUSE_MOVE
793            and mouse_event.button == MouseButton.LEFT
794        ):
795            self.mouse_resize(mouse_position.x, mouse_position.y)
796        elif mouse_event.event_type == MouseEventType.MOUSE_UP:
797            self.stop_resize()
798            # Mouse event handled, return None.
799            return None
800        else:
801            self.stop_resize()
802
803        # Mouse event not handled, return NotImplemented.
804        return NotImplemented
805
806    def _calculate_actual_widths(self) -> List[int]:
807        widths = [w.width.preferred for w in self.window_lists]
808
809        available_width = self.current_window_manager_width
810        # Subtract 1 for each separator
811        available_width -= len(self.window_lists) - 1
812        remaining_rows = available_width - sum(widths)
813        window_list_index = 0
814        # Distribute remaining unaccounted columns to each window in turn.
815        while remaining_rows > 0:
816            widths[window_list_index] += 1
817            remaining_rows -= 1
818            window_list_index = (window_list_index + 1) % len(widths)
819
820        return widths
821
822    def _calculate_actual_heights(self) -> List[int]:
823        heights = [w.height.preferred for w in self.window_lists]
824
825        available_height = self.current_window_manager_height
826        # Subtract 1 for each vertical separator
827        available_height -= len(self.window_lists) - 1
828        remaining_rows = available_height - sum(heights)
829        window_list_index = 0
830        # Distribute remaining unaccounted columns to each window in turn.
831        while remaining_rows > 0:
832            heights[window_list_index] += 1
833            remaining_rows -= 1
834            window_list_index = (window_list_index + 1) % len(heights)
835
836        return heights
837
838    def _resize_update_current_row_column(self) -> None:
839        if self.resize_target_window_list_index is None:
840            return
841
842        widths = self._calculate_actual_widths()
843        heights = self._calculate_actual_heights()
844
845        start_column = 0
846        start_row = 0
847
848        # Find the starting column
849        for i in range(self.resize_target_window_list_index + 1):
850            # If we are past the target window_list, exit the loop.
851            if i > self.resize_target_window_list_index:
852                break
853            start_column += widths[i]
854            start_row += heights[i]
855            if i < self.resize_target_window_list_index - 1:
856                start_column += 1
857                start_row += 1
858
859        self.resize_current_column = start_column
860        self.resize_current_row = start_row
861
862    def start_resize(self, window_list):
863        # Check the target window_list isn't the last one.
864        if window_list == self.window_lists[-1]:
865            return
866
867        list_index = self.window_list_index(window_list)
868        if list_index is None:
869            return
870
871        self.resize_mode = True
872        self.resize_target_window_list = window_list
873        self.resize_target_window_list_index = list_index
874        self._resize_update_current_row_column()
875
876    def stop_resize(self):
877        self.resize_mode = False
878        self.resize_target_window_list = None
879        self.resize_target_window_list_index = None
880        self.resize_current_row = 0
881        self.resize_current_column = 0
882
883    def find_window_list_and_pane_index(self, pane: Any):
884        pane_index = None
885        parent_window_list = None
886        for window_list in self.window_lists:
887            pane_index = window_list.pane_index(pane)
888            if pane_index is not None:
889                parent_window_list = window_list
890                break
891        return parent_window_list, pane_index
892
893    def remove_pane(self, existing_pane: Any):
894        window_list, _pane_index = self.find_window_list_and_pane_index(
895            existing_pane
896        )
897        if window_list:
898            window_list.remove_pane(existing_pane)
899            # Reset focus if this list is empty
900            if len(window_list.active_panes) == 0:
901                self.application.focus_main_menu()
902
903    def reset_pane_sizes(self):
904        for window_list in self.window_lists:
905            window_list.reset_pane_sizes()
906
907    def _remove_panes_from_layout(
908        self, pane_titles: Iterable[str]
909    ) -> Dict[str, Any]:
910        # Gather pane objects and remove them from the window layout.
911        collected_panes = {}
912
913        for window_list in self.window_lists:
914            # Make a copy of active_panes to prevent mutating the while
915            # iterating.
916            for pane in copy.copy(window_list.active_panes):
917                if pane.pane_title() in pane_titles:
918                    collected_panes[
919                        pane.pane_title()
920                    ] = window_list.remove_pane_no_checks(pane)
921        return collected_panes
922
923    def _set_pane_options(  # pylint: disable=no-self-use
924        self, pane, options: dict
925    ) -> None:
926        if options.get('hidden', False):
927            # Hide this pane
928            pane.show_pane = False
929        if options.get('height', False):
930            # Apply new height
931            new_height = options['height']
932            assert isinstance(new_height, int)
933            pane.height.preferred = new_height
934
935    def _set_window_list_display_modes(self, prefs: ConsolePrefs) -> None:
936        # Set column display modes
937        for column_index, column_type in enumerate(prefs.window_column_modes):
938            mode = DisplayMode.STACK
939            if 'tabbed' in column_type:
940                mode = DisplayMode.TABBED
941            self.window_lists[column_index].set_display_mode(mode)
942
943    def _create_new_log_pane_with_loggers(
944        self, window_title, window_options, existing_pane_titles
945    ) -> LogPane:
946        if 'loggers' not in window_options:
947            error_unknown_window(window_title, existing_pane_titles)
948
949        new_pane = LogPane(
950            application=self.application, pane_title=window_title
951        )
952        # Add logger handlers
953        for logger_name, logger_options in window_options.get(
954            'loggers', {}
955        ).items():
956            log_level_name = logger_options.get('level', None)
957            new_pane.add_log_handler(logger_name, level_name=log_level_name)
958        return new_pane
959
960    # TODO(tonymd): Split this large function up.
961    def apply_config(self, prefs: ConsolePrefs) -> None:
962        """Apply window configuration from loaded ConsolePrefs."""
963        if not prefs.windows:
964            return
965
966        unique_titles = prefs.unique_window_titles
967        collected_panes = self._remove_panes_from_layout(unique_titles)
968        existing_pane_titles = [
969            p.pane_title()
970            for p in collected_panes.values()
971            if isinstance(p, LogPane)
972        ]
973
974        # Keep track of original non-duplicated pane titles
975        already_added_panes = []
976
977        for column_index, column in enumerate(
978            prefs.windows.items()
979        ):  # pylint: disable=too-many-nested-blocks
980            _column_type, windows = column
981            # Add a new window_list if needed
982            if column_index >= len(self.window_lists):
983                self.window_lists.append(WindowList(self))
984
985            # Set column display mode to stacked by default.
986            self.window_lists[column_index].display_mode = DisplayMode.STACK
987
988            # Add windows to the this column (window_list)
989            for window_title, window_dict in windows.items():
990                window_options = window_dict if window_dict else {}
991                new_pane = None
992                desired_window_title = window_title
993                # Check for duplicate_of: Title value
994                window_title = window_options.get('duplicate_of', window_title)
995
996                # Check if this pane is brand new, ready to be added, or should
997                # be duplicated.
998                if (
999                    window_title not in already_added_panes
1000                    and window_title not in collected_panes
1001                ):
1002                    # New pane entirely
1003                    new_pane = self._create_new_log_pane_with_loggers(
1004                        window_title, window_options, existing_pane_titles
1005                    )
1006
1007                elif window_title not in already_added_panes:
1008                    # First time adding this pane
1009                    already_added_panes.append(window_title)
1010                    new_pane = collected_panes[window_title]
1011
1012                elif window_title in collected_panes:
1013                    # Pane added once, duplicate it
1014                    new_pane = collected_panes[window_title].create_duplicate()
1015                    # Rename this duplicate pane
1016                    assert isinstance(new_pane, LogPane)
1017                    new_pane.set_pane_title(desired_window_title)
1018
1019                if new_pane:
1020                    # Set window size and visibility
1021                    self._set_pane_options(new_pane, window_options)
1022                    # Add the new pane
1023                    self.window_lists[column_index].add_pane_no_checks(new_pane)
1024                    # Apply log pane options
1025                    if isinstance(new_pane, LogPane):
1026                        new_pane.apply_filters_from_config(window_options)
1027                        # Auto-start the websocket log server if requested.
1028                        if window_options.get('view_in_web', False):
1029                            new_pane.toggle_websocket_server()
1030
1031        # Update column display modes.
1032        self._set_window_list_display_modes(prefs)
1033        # Check for columns where all panes are hidden and unhide at least one.
1034        self.check_for_all_hidden_panes_and_unhide()
1035
1036        # Update prompt_toolkit containers.
1037        self.update_root_container_body()
1038        self.application.update_menu_items()
1039
1040        # Focus on the first visible pane.
1041        self.focus_first_visible_pane()
1042
1043    def create_window_menu_items(self) -> List[MenuItem]:
1044        """Build the [Window] menu for the current set of window lists."""
1045        root_menu_items = []
1046        for window_list_index, window_list in enumerate(self.window_lists):
1047            menu_items = []
1048            menu_items.append(
1049                MenuItem(
1050                    'Column {index} View Modes'.format(
1051                        index=window_list_index + 1
1052                    ),
1053                    children=[
1054                        MenuItem(
1055                            '{check} {display_mode} Windows'.format(
1056                                display_mode=display_mode.value,
1057                                check=to_checkbox_text(
1058                                    window_list.display_mode == display_mode,
1059                                    end='',
1060                                ),
1061                            ),
1062                            handler=functools.partial(
1063                                window_list.set_display_mode, display_mode
1064                            ),
1065                        )
1066                        for display_mode in DisplayMode
1067                    ],
1068                )
1069            )
1070            menu_items.extend(
1071                MenuItem(
1072                    '{index}: {title}'.format(
1073                        index=pane_index + 1,
1074                        title=pane.menu_title(),
1075                    ),
1076                    children=[
1077                        MenuItem(
1078                            '{check} Show/Hide Window'.format(
1079                                check=to_checkbox_text(pane.show_pane, end='')
1080                            ),
1081                            handler=functools.partial(self.toggle_pane, pane),
1082                        ),
1083                    ]
1084                    + [
1085                        MenuItem(
1086                            text,
1087                            handler=functools.partial(
1088                                self.application.run_pane_menu_option, handler
1089                            ),
1090                        )
1091                        for text, handler in pane.get_window_menu_options()
1092                    ],
1093                )
1094                for pane_index, pane in enumerate(window_list.active_panes)
1095            )
1096            if window_list_index + 1 < len(self.window_lists):
1097                menu_items.append(MenuItem('-'))
1098            root_menu_items.extend(menu_items)
1099
1100        return root_menu_items
1101