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