prompt-toolkit / python-prompt-toolkit

Library for building powerful interactive command line applications in Python
https://python-prompt-toolkit.readthedocs.io/
BSD 3-Clause "New" or "Revised" License
9.28k stars 715 forks source link

Opening multiple dialogs at once #1130

Open lighth7015 opened 4 years ago

lighth7015 commented 4 years ago

Is it possible to open multiple dialogs open at once? (E.g. a setup process)? image

Thanks again!

jonathanslenders commented 4 years ago

This is possible by creating multiple Dialog objects and wrapping them in a prompt_toolkit.layout.Float in a prompt_toolkit.layout.FloatContainer.

lighth7015 commented 4 years ago

Ah! I see!

lighth7015 commented 4 years ago

So, something like this (borrowed from the text editor example)?

class Modal(Float):
    """ Modal wrapper for dialogs """

    def __init__(self, container: AnyContainer, dialog: Dialog):
        parent = super(Modal, self)
        parent.__init__(content = dialog)

        container.floats.insert(0, self)
        self._instance = get_app()

        self._float_dlg = dialog
        self._container = container

    async def open(self):
        parent = self._instance.layout.current_window
        self._instance.layout.focus(self._float_dlg)

        result = await self._float_dlg.future
        self._instance.layout.focus(parent)

        if self in self._container.floats:
            self._container.floats.remove(float_)

        return result
lighth7015 commented 4 years ago

(For context, this is what I'm working with)

from prompt_toolkit.application import Application, get_app
from prompt_toolkit.formatted_text import HTML, merge_formatted_text
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.layout import Dimension, FormattedTextControl, HSplit, Layout, VSplit, Window
from prompt_toolkit.layout.containers import Float, FloatContainer, AnyContainer
from prompt_toolkit.layout.margins import ScrollbarMargin
from prompt_toolkit.styles import Style
from prompt_toolkit.widgets import Box, Button, Checkbox, Dialog, Label, Frame, RadioList, TextArea
from prompt_toolkit.shortcuts import message_dialog

def create_title(text: str, dont_extend_width: bool = False) -> Label:
    return Label(text, style="fg:ansiblue", dont_extend_width=dont_extend_width)

def indented(container: AnyContainer, amount: int = 2) -> AnyContainer:
    return VSplit([Label("", width=amount), container])

class Modal(Float):
    """ Modal wrapper for dialogs """

    def __init__(self, container: AnyContainer, dialog: Dialog):
        parent = super(Modal, self)
        parent.__init__(content = dialog)

        container.floats.insert(0, self)
        self._instance = get_app()

        self._float_dlg = dialog
        self._container = container

    async def open(self):
        parent = self._instance.layout.current_window
        self._instance.layout.focus(self._float_dlg)

        result = await self._float_dlg.future
        self._instance.layout.focus(parent)

        if self in self._container.floats:
            self._container.floats.remove(float_)

        return result

class ListBox(Window):
    def __init__(self, entries, **bindings):
        self.entries = entries

        self.event_bindings = bindings
        self.selected_line = 1

        parent = super(ListBox, self)
        parent.__init__(
            content=FormattedTextControl(
                text= self._get_formatted_text,
                focusable=True,
                key_bindings=self._get_key_bindings(),
            ),

            style="class:select-box",
            cursorline=True,
            right_margins=[
                ScrollbarMargin(display_arrows = True),
            ],
        )

    def _get_formatted_text(self):
        result = []
        for i, entry in enumerate(self.entries):
            if i == self.selected_line:
                result.append([("[SetCursorPosition]", "")])
            result.append(entry)
            result.append("\n")

        return merge_formatted_text(result)

    def _get_key_bindings(self):
        kb = KeyBindings()

        @kb.add("enter")
        def handle(event) -> None:
            def handle(self, item):
                pass

            callback = self.event_bindings['selected'] or handle
            callback(event)

        @kb.add("up")
        def handle(event) -> None:
            if self.selected_line > 0:
                self.selected_line = self.selected_line - 1

        @kb.add("down")
        def handle(event) -> None:
            if self.selected_line < len(self.entries) - 1:
                self.selected_line = self.selected_line + 1

        return kb

    def __pt_container__(self):
        return self.container

class MenuDialog(Dialog):
    width = 75
    items = [
        "Change BCM data",
        "Delete BCM data",
        "List Item 5"
    ]

    def selected(self, item):
        pass

    def handle_exit(self):
        get_app().exit()

    def create(self):
        self.buttons = [
            Button("Save"),
            Button("Load"),
            Button("Exit", handler=self.handle_exit)
        ]

        return HSplit(
            [
                Box(
                    create_title("Giza SDK Manager"),
                    padding = 0,
                ),
                Box(
                    create_title("─" * self.width),
                    padding_top = 0,
                    padding_left = 0,
                    padding_bottom = 0,
                ),
                Box(
                    ListBox(self.items, selected = self.selected),
                    height = 20,
                    width = self.width,
                    padding_top = 0,
                    padding_left = 0,
                    padding_right = 0,
                    padding_bottom = 0,
                ),
                Box(
                    create_title("┌{0}┐\n"
                             "│ Use the arrow keys to highlight an option from the list above and press │\n"
                             "│ the Return/Enter key to confirm it.{1} │\n"
                             "└{0}┘".format( 
                                "─" * ( self.width - 2 ),
                                ' ' * (self.width - 39), 
                                '─' * ( self.width - 2 ))),
                    padding = 0
                ),
            ]
        )

    def __init__(self, *items):
        parent = super(MenuDialog, self)

        parent.__init__(
            title="SDK Configuration Manager",
            with_background=True,
            body=Box(self.create(), padding=0),
            buttons = self.buttons)

        self.modal = Modal(self, message_dialog(title='Example dialog window', text='You selected an item.'))

style = Style.from_dict({
    "dialog.body": "bg:#cccccc",
    "dialog.body select-box": "bg:#cccccc",
    "dialog.body select-box": "bg:#cccccc",
    "dialog.body select-box cursor-line": "nounderline bg:ansired fg:ansiwhite",
    "dialog.body text-area": "bg:#4444ff fg:white",
    "dialog.body text-area": "bg:#4444ff fg:white",
    "dialog.body radio-list radio": "bg:#4444ff fg:white",
    "dialog.body button.focused": "bg:#4444ff fg:white",
    "dialog.body checkbox-list checkbox": "bg:#4444ff fg:white",
})

kb = KeyBindings()

@kb.add("f6")
def _exit(event):
    event.app.exit()

@kb.add("f5")
def _next(event):
    # TODO
    pass

application: Application[MenuDialog] = \
    Application(layout=Layout(MenuDialog()), full_screen=True, erase_when_done = True, mouse_support = True, style=style, key_bindings=kb)

application.run()
jonathanslenders commented 4 years ago

Hi @lighth7015, I don't have much time to comment in depth, but my main suggestion is to not use inheritance, but only composition instead. The __pt_container__ method is actually made to expose the Container object from a class that doesn't inherit from Widow, VSplit or other container classes. If you inherit from a container, there shouldn't be a __pt_container__.

lighth7015 commented 4 years ago

It's fine, I borrowed the example and tried my best to adapt it.

lighth7015 commented 4 years ago

And I only inherited from float because for me it's a lot of extra effort to manage a Float instance. So why not just be a subclass of it?

jonathanslenders commented 4 years ago

In see inheritance in Window and Dialog too.

The main reason for not using this is because you don't have any control at all of which variables will be added to these classes in the future in prompt_toolkit itself. If in a future version, prompt_toolkit will add a variable named event_bindings to Window, this code will break. You have to make sure that there is not a single collision in attribute variable names between your classes and the classes you're inheriting from in the current version, and you'll have to check it again for every minor version upgrade every time.

So, it's up to you, whatever is the least effort ;) prompt_toolkit is not opinionated about the way it's used.

lighth7015 commented 4 years ago

And both of those stem from my belief that if you're going to use a class (and manage it) internally with a bunch of abstractions, wouldn't it be better to simplify and just have that class as your parent?

And if that causes problems, then I'll adjust codebase until it no loner causes problems :p

So what would the preferred way be? I was just preparing to handle events between my listbox and main dialog, so it would get notified when a menu item has been selected, so I'm not at all married to doing it that way. :p

lighth7015 commented 4 years ago

@jonathanslenders Okay, one more update. I have manged to use the float.py example to accomplish what I'm after... Sort of.. My new issue is that shadows disappear if I change the dialog color via styles to anything other than what the default is.. How do I fix this?