Skip to content

Added scrollable pane. #1347

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/pages/full_screen_apps.rst
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ responsible for generating the actual content.
| | :class:`~prompt_toolkit.layout.VSplit` |
| | :class:`~prompt_toolkit.layout.FloatContainer` |
| | :class:`~prompt_toolkit.layout.Window` |
| | :class:`~prompt_toolkit.layout.ScrollablePane` |
+---------------------------------------------+------------------------------------------------------+
| :class:`~prompt_toolkit.layout.UIControl` | :class:`~prompt_toolkit.layout.BufferControl` |
| | :class:`~prompt_toolkit.layout.FormattedTextControl` |
Expand Down Expand Up @@ -229,6 +230,10 @@ If you want to make some part of the layout only visible when a certain
condition is satisfied, use a
:class:`~prompt_toolkit.layout.ConditionalContainer`.

Finally, there is :class:`~prompt_toolkit.layout.ScrollablePane`, a container
class that can be used to create long forms or nested layouts that are
scrollable as a whole.


Focusing windows
^^^^^^^^^^^^^^^^^
Expand Down
2 changes: 2 additions & 0 deletions docs/pages/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,8 @@ Containers

.. autoclass:: prompt_toolkit.layout.DynamicContainer

.. autoclass:: prompt_toolkit.layout.ScrollablePane

.. autoclass:: prompt_toolkit.layout.ScrollOffsets

.. autoclass:: prompt_toolkit.layout.ColorColumn
Expand Down
45 changes: 45 additions & 0 deletions examples/full-screen/scrollable-panes/simple-example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#!/usr/bin/env python
"""
A simple example of a scrollable pane.
"""
from prompt_toolkit.application import Application
from prompt_toolkit.application.current import get_app
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.key_binding.bindings.focus import focus_next, focus_previous
from prompt_toolkit.layout import Dimension, HSplit, Layout, ScrollablePane
from prompt_toolkit.widgets import Frame, Label, TextArea


def main():
# Create a big layout of many text areas, then wrap them in a `ScrollablePane`.
root_container = Frame(
ScrollablePane(
HSplit(
[
Frame(TextArea(text=f"label-{i}"), width=Dimension())
for i in range(20)
]
)
)
# ScrollablePane(HSplit([TextArea(text=f"label-{i}") for i in range(20)]))
)

layout = Layout(container=root_container)

# Key bindings.
kb = KeyBindings()

@kb.add("c-c")
def exit(event) -> None:
get_app().exit()

kb.add("tab")(focus_next)
kb.add("s-tab")(focus_previous)

# Create and run application.
application = Application(layout=layout, key_bindings=kb, full_screen=True)
application.run()


if __name__ == "__main__":
main()
120 changes: 120 additions & 0 deletions examples/full-screen/scrollable-panes/with-completion-menu.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#!/usr/bin/env python
"""
A simple example of a scrollable pane.
"""
from prompt_toolkit.application import Application
from prompt_toolkit.application.current import get_app
from prompt_toolkit.completion import WordCompleter
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.key_binding.bindings.focus import focus_next, focus_previous
from prompt_toolkit.layout import (
CompletionsMenu,
Float,
FloatContainer,
HSplit,
Layout,
ScrollablePane,
VSplit,
)
from prompt_toolkit.widgets import Frame, Label, TextArea


def main():
# Create a big layout of many text areas, then wrap them in a `ScrollablePane`.
root_container = VSplit(
[
Label("<left column>"),
HSplit(
[
Label("ScrollContainer Demo"),
Frame(
ScrollablePane(
HSplit(
[
Frame(
TextArea(
text=f"label-{i}",
completer=animal_completer,
)
)
for i in range(20)
]
)
),
),
]
),
]
)

root_container = FloatContainer(
root_container,
floats=[
Float(
xcursor=True,
ycursor=True,
content=CompletionsMenu(max_height=16, scroll_offset=1),
),
],
)

layout = Layout(container=root_container)

# Key bindings.
kb = KeyBindings()

@kb.add("c-c")
def exit(event) -> None:
get_app().exit()

kb.add("tab")(focus_next)
kb.add("s-tab")(focus_previous)

# Create and run application.
application = Application(
layout=layout, key_bindings=kb, full_screen=True, mouse_support=True
)
application.run()


animal_completer = WordCompleter(
[
"alligator",
"ant",
"ape",
"bat",
"bear",
"beaver",
"bee",
"bison",
"butterfly",
"cat",
"chicken",
"crocodile",
"dinosaur",
"dog",
"dolphin",
"dove",
"duck",
"eagle",
"elephant",
"fish",
"goat",
"gorilla",
"kangaroo",
"leopard",
"lion",
"mouse",
"rabbit",
"rat",
"snake",
"spider",
"turkey",
"turtle",
],
ignore_case=True,
)


if __name__ == "__main__":
main()
4 changes: 2 additions & 2 deletions prompt_toolkit/key_binding/bindings/mouse.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ def _(event: E) -> None:
return

# Call the mouse handler from the renderer.
handler = event.app.renderer.mouse_handlers.mouse_handlers[x, y]
handler = event.app.renderer.mouse_handlers.mouse_handlers[y][x]
handler(MouseEvent(position=Point(x=x, y=y), event_type=mouse_event_type))

@key_bindings.add(Keys.ScrollUp)
Expand Down Expand Up @@ -141,7 +141,7 @@ def _mouse(event: E) -> None:
y -= rows_above_cursor

# Call the mouse event handler.
handler = event.app.renderer.mouse_handlers.mouse_handlers[x, y]
handler = event.app.renderer.mouse_handlers.mouse_handlers[y][x]
handler(MouseEvent(position=Point(x=x, y=y), event_type=event_type))

return key_bindings
2 changes: 2 additions & 0 deletions prompt_toolkit/layout/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
ScrollbarMargin,
)
from .menus import CompletionsMenu, MultiColumnCompletionsMenu
from .scrollable_pane import ScrollablePane

__all__ = [
# Layout.
Expand Down Expand Up @@ -123,6 +124,7 @@
"to_window",
"is_container",
"DynamicContainer",
"ScrollablePane",
# Controls.
"BufferControl",
"SearchBufferControl",
Expand Down
5 changes: 3 additions & 2 deletions prompt_toolkit/layout/containers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1923,8 +1923,9 @@ def render_margin(m: Margin, width: int) -> UIContent:
# Apply 'self.style'
self._apply_style(screen, write_position, parent_style)

# Tell the screen that this user control has been painted.
screen.visible_windows.append(self)
# Tell the screen that this user control has been painted at this
# position.
screen.visible_windows_to_write_positions[self] = write_position

def _copy_body(
self,
Expand Down
20 changes: 15 additions & 5 deletions prompt_toolkit/layout/mouse_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@
from prompt_toolkit.mouse_events import MouseEvent

__all__ = [
"MouseHandler",
"MouseHandlers",
]

MouseHandler = Callable[[MouseEvent], None]


class MouseHandlers:
"""
Expand All @@ -20,10 +23,14 @@ def dummy_callback(mouse_event: MouseEvent) -> None:
:param mouse_event: `MouseEvent` instance.
"""

# Map (x,y) tuples to handlers.
# NOTE: Previously, the data structure was a dictionary mapping (x,y)
# to the handlers. This however would be more inefficient when copying
# over the mouse handlers of the visible region in the scrollable pane.

# Map y (row) to x (column) to handlers.
self.mouse_handlers: DefaultDict[
Tuple[int, int], Callable[[MouseEvent], None]
] = defaultdict(lambda: dummy_callback)
int, DefaultDict[int, MouseHandler]
] = defaultdict(lambda: defaultdict(lambda: dummy_callback))

def set_mouse_handler_for_range(
self,
Expand All @@ -36,5 +43,8 @@ def set_mouse_handler_for_range(
"""
Set mouse handler for a region.
"""
for x, y in product(range(x_min, x_max), range(y_min, y_max)):
self.mouse_handlers[x, y] = handler
for y in range(y_min, y_max):
row = self.mouse_handlers[y]

for x in range(x_min, x_max):
row[x] = handler
6 changes: 5 additions & 1 deletion prompt_toolkit/layout/screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,11 +186,15 @@ def __init__(

# Windows that have been drawn. (Each `Window` class will add itself to
# this list.)
self.visible_windows: List["Window"] = []
self.visible_windows_to_write_positions: Dict["Window", "WritePosition"] = {}

# List of (z_index, draw_func)
self._draw_float_functions: List[Tuple[int, Callable[[], None]]] = []

@property
def visible_windows(self) -> List["Window"]:
return list(self.visible_windows_to_write_positions.keys())

def set_cursor_position(self, window: "Window", position: Point) -> None:
"""
Set the cursor position for a given window.
Expand Down
Loading