#!/usr/bin/env python
# Copyright 2022 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
""" Prompt toolkit application for pw watch. """

import asyncio
import functools
import logging
import os
import re
import time
from typing import Callable, Iterable, NoReturn

from prompt_toolkit.application import Application
from prompt_toolkit.clipboard.pyperclip import PyperclipClipboard
from prompt_toolkit.filters import Condition
from prompt_toolkit.history import (
    InMemoryHistory,
    History,
    ThreadedHistory,
)
from prompt_toolkit.key_binding import (
    KeyBindings,
    KeyBindingsBase,
    merge_key_bindings,
)
from prompt_toolkit.layout import (
    DynamicContainer,
    Float,
    FloatContainer,
    FormattedTextControl,
    HSplit,
    Layout,
    Window,
)
from prompt_toolkit.layout.controls import BufferControl
from prompt_toolkit.styles import (
    ConditionalStyleTransformation,
    DynamicStyle,
    SwapLightAndDarkStyleTransformation,
    merge_style_transformations,
    merge_styles,
    style_from_pygments_cls,
)
from prompt_toolkit.formatted_text import StyleAndTextTuples
from prompt_toolkit.lexers import PygmentsLexer
from pygments.lexers.markup import MarkdownLexer  # type: ignore

from pw_config_loader import yaml_config_loader_mixin

from pw_console.console_app import get_default_colordepth, MIN_REDRAW_INTERVAL
from pw_console.get_pw_console_app import PW_CONSOLE_APP_CONTEXTVAR
from pw_console.help_window import HelpWindow
from pw_console.key_bindings import DEFAULT_KEY_BINDINGS
from pw_console.log_pane import LogPane
from pw_console.plugin_mixin import PluginMixin
import pw_console.python_logging
from pw_console.quit_dialog import QuitDialog
from pw_console.style import generate_styles, get_theme_colors
from pw_console.pigweed_code_style import PigweedCodeStyle
from pw_console.widgets import (
    FloatingWindowPane,
    ToolbarButton,
    WindowPaneToolbar,
    create_border,
    mouse_handlers,
    to_checkbox,
)
from pw_console.window_list import DisplayMode
from pw_console.window_manager import WindowManager

from pw_build.project_builder_prefs import ProjectBuilderPrefs
from pw_build.project_builder_context import get_project_builder_context


_LOG = logging.getLogger('pw_build.watch')

BUILDER_CONTEXT = get_project_builder_context()

_HELP_TEXT = """
Mouse Keys
==========

- Click on a line in the bottom progress bar to switch to that tab.
- Click on any tab, or button to activate.
- Scroll wheel in the the log windows moves back through the history.


Global Keys
===========

Quit with confirmation dialog. --------------------  Ctrl-D
Quit without confirmation. ------------------------  Ctrl-X Ctrl-C
Toggle user guide window. -------------------------  F1
Trigger a rebuild. --------------------------------  Enter


Window Management Keys
======================

Switch focus to the next window pane or tab. ------  Ctrl-Alt-N
Switch focus to the previous window pane or tab. --  Ctrl-Alt-P
Move window pane left. ----------------------------  Ctrl-Alt-Left
Move window pane right. ---------------------------  Ctrl-Alt-Right
Move window pane down. ----------------------------  Ctrl-Alt-Down
Move window pane up. ------------------------------  Ctrl-Alt-Up
Balance all window sizes. -------------------------  Ctrl-U


Bottom Toolbar Controls
=======================

Rebuild Enter --------------- Click or press Enter to trigger a rebuild.
[x] Auto Rebuild ------------ Click to globaly enable or disable automatic
                              rebuilding when files change.
Help F1 --------------------- Click or press F1 to open this help window.
Quit Ctrl-d ----------------- Click or press Ctrl-d to quit pw_watch.
Next Tab Ctrl-Alt-n --------- Switch to the next log tab.
Previous Tab Ctrl-Alt-p ----- Switch to the previous log tab.


Build Status Bar
================

The build status bar shows the current status of all build directories outlined
in a colored frame.

  ┏━━ BUILDING ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
  ┃ [✓] out_directory  Building  Last line of standard out.                ┃
  ┃ [✓] out_dir2       Waiting   Last line of standard out.                ┃
  ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

Each checkbox on the far left controls whether that directory is built when
files change and manual builds are run.


Copying Text
============

- Click drag will select whole lines in the log windows.
- `Ctrl-c` will copy selected lines to your system clipboard.

If running over SSH you will need to use your terminal's built in text
selection.

Linux
-----

- Holding `Shift` and dragging the mouse in most terminals.

Mac
---

- Apple Terminal:

  Hold `Fn` and drag the mouse

- iTerm2:

  Hold `Cmd+Option` and drag the mouse

Windows
-------

- Git CMD (included in `Git for Windows)

  1. Click on the Git window icon in the upper left of the title bar
  2. Click `Edit` then `Mark`
  3. Drag the mouse to select text and press Enter to copy.

- Windows Terminal

  1. Hold `Shift` and drag the mouse to select text
  2. Press `Ctrl-Shift-C` to copy.

"""


class WatchAppPrefs(ProjectBuilderPrefs):
    """Add pw_console specific prefs standard ProjectBuilderPrefs."""

    def __init__(self, *args, **kwargs) -> None:
        super().__init__(*args, **kwargs)

        self.registered_commands = DEFAULT_KEY_BINDINGS
        self.registered_commands.update(self.user_key_bindings)

        new_config_settings = {
            'key_bindings': DEFAULT_KEY_BINDINGS,
            'show_python_logger': True,
        }
        self.default_config.update(new_config_settings)
        self._update_config(
            new_config_settings,
            yaml_config_loader_mixin.Stage.DEFAULT,
        )

    # Required pw_console preferences for key bindings and themes
    @property
    def user_key_bindings(self) -> dict[str, list[str]]:
        return self._config.get('key_bindings', {})

    @property
    def ui_theme(self) -> str:
        return self._config.get('ui_theme', '')

    @ui_theme.setter
    def ui_theme(self, new_ui_theme: str) -> None:
        self._config['ui_theme'] = new_ui_theme

    @property
    def theme_colors(self):
        return get_theme_colors(self.ui_theme)

    @property
    def swap_light_and_dark(self) -> bool:
        return self._config.get('swap_light_and_dark', False)

    def get_function_keys(self, name: str) -> list:
        """Return the keys for the named function."""
        try:
            return self.registered_commands[name]
        except KeyError as error:
            raise KeyError('Unbound key function: {}'.format(name)) from error

    def register_named_key_function(
        self, name: str, default_bindings: list[str]
    ) -> None:
        self.registered_commands[name] = default_bindings

    def register_keybinding(
        self, name: str, key_bindings: KeyBindings, **kwargs
    ) -> Callable:
        """Apply registered keys for the given named function."""

        def decorator(handler: Callable) -> Callable:
            "`handler` is a callable or Binding."
            for keys in self.get_function_keys(name):
                key_bindings.add(*keys.split(' '), **kwargs)(handler)
            return handler

        return decorator

    # Required pw_console preferences for using a log window pane.
    @property
    def spaces_between_columns(self) -> int:
        return 2

    @property
    def window_column_split_method(self) -> str:
        return 'vertical'

    @property
    def hide_date_from_log_time(self) -> bool:
        return True

    @property
    def column_order(self) -> list:
        return []

    def column_style(  # pylint: disable=no-self-use
        self,
        _column_name: str,
        _column_value: str,
        default='',
    ) -> str:
        return default

    @property
    def show_python_file(self) -> bool:
        return self._config.get('show_python_file', False)

    @property
    def show_source_file(self) -> bool:
        return self._config.get('show_source_file', False)

    @property
    def show_python_logger(self) -> bool:
        return self._config.get('show_python_logger', False)


class WatchWindowManager(WindowManager):
    def update_root_container_body(self):
        self.application.window_manager_container = self.create_root_container()


class WatchApp(PluginMixin):
    """Pigweed Watch main window application."""

    # pylint: disable=too-many-instance-attributes

    NINJA_FAILURE_TEXT = '\033[31mFAILED: '

    NINJA_BUILD_STEP = re.compile(
        r"^\[(?P<step>[0-9]+)/(?P<total_steps>[0-9]+)\] (?P<action>.*)$"
    )

    def __init__(
        self,
        event_handler,
        prefs: WatchAppPrefs,
    ):
        self.event_handler = event_handler

        self.color_depth = get_default_colordepth()

        # Necessary for some of pw_console's window manager features to work
        # such as mouse drag resizing.
        PW_CONSOLE_APP_CONTEXTVAR.set(self)  # type: ignore

        self.prefs = prefs

        self.quit_dialog = QuitDialog(self, self.exit)  # type: ignore

        self.search_history: History = ThreadedHistory(InMemoryHistory())

        self.window_manager = WatchWindowManager(self)

        self._build_error_count = 0
        self._errors_in_output = False

        self.log_ui_update_frequency = 0.1  # 10 FPS
        self._last_ui_update_time = time.time()

        self.recipe_name_to_log_pane: dict[str, LogPane] = {}
        self.recipe_index_to_log_pane: dict[int, LogPane] = {}

        debug_logging = (
            event_handler.project_builder.default_log_level == logging.DEBUG
        )
        level_name = 'DEBUG' if debug_logging else 'INFO'

        no_propagation_loggers = []

        if event_handler.separate_logfiles:
            pane_index = len(event_handler.project_builder.build_recipes) - 1
            for recipe in reversed(event_handler.project_builder.build_recipes):
                log_pane = self.add_build_log_pane(
                    recipe.display_name,
                    loggers=[recipe.log],
                    level_name=level_name,
                )
                if recipe.log.propagate is False:
                    no_propagation_loggers.append(recipe.log)

                self.recipe_name_to_log_pane[recipe.display_name] = log_pane
                self.recipe_index_to_log_pane[pane_index] = log_pane
                pane_index -= 1

        pw_console.python_logging.setup_python_logging(
            loggers_with_no_propagation=no_propagation_loggers
        )

        self.root_log_pane = self.add_build_log_pane(
            'Root Log',
            loggers=[
                logging.getLogger('pw_build'),
            ],
            level_name=level_name,
        )
        # Repeat the Attaching filesystem watcher message for the full screen
        # interface. The original log in watch.py will be hidden from view.
        _LOG.info('Attaching filesystem watcher...')

        self.window_manager.window_lists[0].display_mode = DisplayMode.TABBED

        self.window_manager_container = (
            self.window_manager.create_root_container()
        )

        self.status_bar_border_style = 'class:command-runner-border'

        self.status_bar_control = FormattedTextControl(self.get_status_bar_text)

        self.status_bar_container = create_border(
            HSplit(
                [
                    # Result Toolbar.
                    Window(
                        content=self.status_bar_control,
                        height=len(self.event_handler.project_builder),
                        wrap_lines=False,
                        style='class:pane_active',
                    ),
                ]
            ),
            content_height=len(self.event_handler.project_builder),
            title=BUILDER_CONTEXT.get_title_bar_text,
            border_style=(BUILDER_CONTEXT.get_title_style),
            base_style='class:pane_active',
            left_margin_columns=1,
            right_margin_columns=1,
        )

        self.floating_window_plugins: list[FloatingWindowPane] = []

        self.user_guide_window = HelpWindow(
            self,  # type: ignore
            title='Pigweed Watch',
            disable_ctrl_c=True,
        )
        self.user_guide_window.set_help_text(
            _HELP_TEXT, lexer=PygmentsLexer(MarkdownLexer)
        )

        self.help_toolbar = WindowPaneToolbar(
            title='Pigweed Watch',
            include_resize_handle=False,
            focus_action_callable=self.switch_to_root_log,
            click_to_focus_text='',
        )
        self.help_toolbar.add_button(
            ToolbarButton('Enter', 'Rebuild', self.run_build)
        )
        self.help_toolbar.add_button(
            ToolbarButton(
                description='Auto Rebuild',
                mouse_handler=self.toggle_restart_on_filechange,
                is_checkbox=True,
                checked=lambda: self.restart_on_changes,
            )
        )
        self.help_toolbar.add_button(
            ToolbarButton('F1', 'Help', self.user_guide_window.toggle_display)
        )
        self.help_toolbar.add_button(ToolbarButton('Ctrl-d', 'Quit', self.exit))
        self.help_toolbar.add_button(
            ToolbarButton(
                'Ctrl-Alt-n', 'Next Tab', self.window_manager.focus_next_pane
            )
        )
        self.help_toolbar.add_button(
            ToolbarButton(
                'Ctrl-Alt-p',
                'Previous Tab',
                self.window_manager.focus_previous_pane,
            )
        )

        self.root_container = FloatContainer(
            HSplit(
                [
                    # Window pane content:
                    DynamicContainer(lambda: self.window_manager_container),
                    self.status_bar_container,
                    self.help_toolbar,
                ]
            ),
            floats=[
                Float(
                    content=self.user_guide_window,
                    top=2,
                    left=4,
                    bottom=4,
                    width=self.user_guide_window.content_width,
                ),
                Float(
                    content=self.quit_dialog,
                    top=2,
                    left=2,
                ),
            ],
        )

        key_bindings = KeyBindings()

        @key_bindings.add('enter', filter=self.input_box_not_focused())
        def _run_build(_event):
            "Rebuild."
            self.run_build()

        register = self.prefs.register_keybinding

        @register('global.exit-no-confirmation', key_bindings)
        def _quit_no_confirm(_event):
            """Quit without confirmation."""
            _LOG.info('Got quit signal; exiting...')
            self.exit(0)

        @register('global.exit-with-confirmation', key_bindings)
        def _quit_with_confirm(_event):
            """Quit with confirmation dialog."""
            self.quit_dialog.open_dialog()

        @register(
            'global.open-user-guide',
            key_bindings,
            filter=Condition(lambda: not self.modal_window_is_open()),
        )
        def _show_help(_event):
            """Toggle user guide window."""
            self.user_guide_window.toggle_display()

        self.key_bindings = merge_key_bindings(
            [
                self.window_manager.key_bindings,
                key_bindings,
            ]
        )

        self.current_theme = generate_styles(self.prefs.ui_theme)

        self.style_transformation = merge_style_transformations(
            [
                ConditionalStyleTransformation(
                    SwapLightAndDarkStyleTransformation(),
                    filter=Condition(lambda: self.prefs.swap_light_and_dark),
                ),
            ]
        )

        self.code_theme = style_from_pygments_cls(PigweedCodeStyle)

        self.layout = Layout(
            self.root_container,
            focused_element=self.root_log_pane,
        )

        self.application: Application = Application(
            layout=self.layout,
            key_bindings=self.key_bindings,
            mouse_support=True,
            color_depth=self.color_depth,
            clipboard=PyperclipClipboard(),
            style=DynamicStyle(
                lambda: merge_styles(
                    [
                        self.current_theme,
                        self.code_theme,
                    ]
                )
            ),
            style_transformation=self.style_transformation,
            full_screen=True,
            min_redraw_interval=MIN_REDRAW_INTERVAL,
        )

        self.plugin_init(
            plugin_callback=self.check_build_status,
            plugin_callback_frequency=0.5,
            plugin_logger_name='pw_watch_stdout_checker',
        )

    def add_build_log_pane(
        self, title: str, loggers: list[logging.Logger], level_name: str
    ) -> LogPane:
        """Setup a new build log pane."""
        new_log_pane = LogPane(application=self, pane_title=title)
        for logger in loggers:
            new_log_pane.add_log_handler(logger, level_name=level_name)

        # Set python log format to just the message itself.
        new_log_pane.log_view.log_store.formatter = logging.Formatter(
            '%(message)s'
        )

        new_log_pane.table_view = False

        # Disable line wrapping for improved error visibility.
        if new_log_pane.wrap_lines:
            new_log_pane.toggle_wrap_lines()

        # Blank right side toolbar text
        new_log_pane._pane_subtitle = ' '  # pylint: disable=protected-access

        # Make tab and shift-tab search for next and previous error
        next_error_bindings = KeyBindings()

        @next_error_bindings.add('s-tab')
        def _previous_error(_event):
            self.jump_to_error(backwards=True)

        @next_error_bindings.add('tab')
        def _next_error(_event):
            self.jump_to_error()

        existing_log_bindings: (
            KeyBindingsBase | None
        ) = new_log_pane.log_content_control.key_bindings

        key_binding_list: list[KeyBindingsBase] = []
        if existing_log_bindings:
            key_binding_list.append(existing_log_bindings)
        key_binding_list.append(next_error_bindings)
        new_log_pane.log_content_control.key_bindings = merge_key_bindings(
            key_binding_list
        )

        # Only show a few buttons in the log pane toolbars.
        new_buttons = []
        for button in new_log_pane.bottom_toolbar.buttons:
            if button.description in [
                'Search',
                'Save',
                'Follow',
                'Wrap',
                'Clear',
            ]:
                new_buttons.append(button)
        new_log_pane.bottom_toolbar.buttons = new_buttons

        self.window_manager.add_pane(new_log_pane)
        return new_log_pane

    def logs_redraw(self):
        emit_time = time.time()
        # Has enough time passed since last UI redraw due to new logs?
        if emit_time > self._last_ui_update_time + self.log_ui_update_frequency:
            # Update last log time
            self._last_ui_update_time = emit_time

            # Trigger Prompt Toolkit UI redraw.
            self.redraw_ui()

    def jump_to_error(self, backwards: bool = False) -> None:
        if not self.root_log_pane.log_view.search_text:
            self.root_log_pane.log_view.set_search_regex(
                '^FAILED: ', False, None
            )
        if backwards:
            self.root_log_pane.log_view.search_backwards()
        else:
            self.root_log_pane.log_view.search_forwards()
        self.root_log_pane.log_view.log_screen.reset_logs(
            log_index=self.root_log_pane.log_view.log_index
        )

        self.root_log_pane.log_view.move_selected_line_to_top()

    def refresh_layout(self) -> None:
        self.window_manager.update_root_container_body()

    def update_menu_items(self):
        """Required by the Window Manager Class."""

    def redraw_ui(self):
        """Redraw the prompt_toolkit UI."""
        if hasattr(self, 'application'):
            self.application.invalidate()

    def focus_on_container(self, pane):
        """Set application focus to a specific container."""
        # Try to focus on the given pane
        try:
            self.application.layout.focus(pane)
        except ValueError:
            # If the container can't be focused, focus on the first visible
            # window pane.
            self.window_manager.focus_first_visible_pane()

    def focused_window(self):
        """Return the currently focused window."""
        return self.application.layout.current_window

    def focus_main_menu(self):
        """Focus on the main menu.

        Currently pw_watch has no main menu so focus on the first visible pane
        instead."""
        self.window_manager.focus_first_visible_pane()

    def switch_to_root_log(self) -> None:
        (
            window_list,
            pane_index,
        ) = self.window_manager.find_window_list_and_pane_index(
            self.root_log_pane
        )
        window_list.switch_to_tab(pane_index)

    def switch_to_build_log(self, log_index: int) -> None:
        pane = self.recipe_index_to_log_pane.get(log_index, None)
        if not pane:
            return

        (
            window_list,
            pane_index,
        ) = self.window_manager.find_window_list_and_pane_index(pane)
        window_list.switch_to_tab(pane_index)

    def command_runner_is_open(self) -> bool:
        # pylint: disable=no-self-use
        return False

    def all_log_panes(self) -> Iterable[LogPane]:
        for pane in self.window_manager.active_panes():
            if isinstance(pane, LogPane):
                yield pane

    def clear_log_panes(self) -> None:
        """Erase all log pane content and turn on follow.

        This is called whenever rebuilds occur. Either a manual build from
        self.run_build or on file changes called from
        pw_watch._handle_matched_event."""
        for pane in self.all_log_panes():
            pane.log_view.clear_visual_selection()
            pane.log_view.clear_filters()
            pane.log_view.log_store.clear_logs()
            pane.log_view.view_mode_changed()
            # Re-enable follow if needed
            if not pane.log_view.follow:
                pane.log_view.toggle_follow()

    def run_build(self) -> None:
        """Manually trigger a rebuild from the UI."""
        self.clear_log_panes()
        self.event_handler.rebuild()

    @property
    def restart_on_changes(self) -> bool:
        return self.event_handler.restart_on_changes

    def toggle_restart_on_filechange(self) -> None:
        self.event_handler.restart_on_changes = (
            not self.event_handler.restart_on_changes
        )

    def get_status_bar_text(self) -> StyleAndTextTuples:
        """Return formatted text for build status bar."""
        formatted_text: StyleAndTextTuples = []

        separator = ('', ' ')
        name_width = self.event_handler.project_builder.max_name_width

        # pylint: disable=protected-access
        (
            _window_list,
            pane,
        ) = self.window_manager._get_active_window_list_and_pane()
        # pylint: enable=protected-access
        restarting = BUILDER_CONTEXT.restart_flag

        for i, cfg in enumerate(self.event_handler.project_builder):
            # The build directory
            name_style = ''
            if not pane:
                formatted_text.append(('', '\n'))
                continue

            # Dim the build name if disabled
            if not cfg.enabled:
                name_style = 'class:theme-fg-inactive'

            # If this build tab is selected, highlight with cyan.
            if pane.pane_title() == cfg.display_name:
                name_style = 'class:theme-fg-cyan'

            formatted_text.append(
                to_checkbox(
                    cfg.enabled,
                    functools.partial(
                        mouse_handlers.on_click,
                        cfg.toggle_enabled,
                    ),
                    end=' ',
                    unchecked_style='class:checkbox',
                    checked_style='class:checkbox-checked',
                )
            )
            formatted_text.append(
                (
                    name_style,
                    f'{cfg.display_name}'.ljust(name_width),
                    functools.partial(
                        mouse_handlers.on_click,
                        functools.partial(self.switch_to_build_log, i),
                    ),
                )
            )
            formatted_text.append(separator)
            # Status
            formatted_text.append(cfg.status.status_slug(restarting=restarting))
            formatted_text.append(separator)
            # Current stdout line
            formatted_text.extend(cfg.status.current_step_formatted())
            formatted_text.append(('', '\n'))

        if not formatted_text:
            formatted_text = [('', 'Loading...')]

        self.set_tab_bar_colors()

        return formatted_text

    def set_tab_bar_colors(self) -> None:
        restarting = BUILDER_CONTEXT.restart_flag

        for cfg in BUILDER_CONTEXT.recipes:
            pane = self.recipe_name_to_log_pane.get(cfg.display_name, None)
            if not pane:
                continue

            pane.extra_tab_style = None
            if not restarting and cfg.status.failed():
                pane.extra_tab_style = 'class:theme-fg-red'

    def exit(
        self,
        exit_code: int = 1,
        log_after_shutdown: Callable[[], None] | None = None,
    ) -> None:
        _LOG.info('Exiting...')
        BUILDER_CONTEXT.ctrl_c_pressed = True

        # Shut everything down after the prompt_toolkit app exits.
        def _really_exit(future: asyncio.Future) -> NoReturn:
            BUILDER_CONTEXT.restore_logging_and_shutdown(log_after_shutdown)
            os._exit(future.result())  # pylint: disable=protected-access

        if self.application.future:
            self.application.future.add_done_callback(_really_exit)
        self.application.exit(result=exit_code)

    def check_build_status(self) -> bool:
        if not self.event_handler.current_stdout:
            return False

        if self._errors_in_output:
            return True

        if self.event_handler.current_build_errors > self._build_error_count:
            self._errors_in_output = True
            self.jump_to_error()

        return True

    def run(self):
        self.plugin_start()
        # Run the prompt_toolkit application
        self.application.run(set_exception_handler=True)

    def input_box_not_focused(self) -> Condition:
        """Condition checking the focused control is not a text input field."""

        @Condition
        def _test() -> bool:
            """Check if the currently focused control is an input buffer.

            Returns:
                bool: True if the currently focused control is not a text input
                    box. For example if the user presses enter when typing in
                    the search box, return False.
            """
            return not isinstance(
                self.application.layout.current_control, BufferControl
            )

        return _test

    def modal_window_is_open(self):
        """Return true if any modal window or dialog is open."""
        floating_window_is_open = (
            self.user_guide_window.show_window or self.quit_dialog.show_dialog
        )

        floating_plugin_is_open = any(
            plugin.show_pane for plugin in self.floating_window_plugins
        )

        return floating_window_is_open or floating_plugin_is_open
