2023-04-23 04:35:21 +03:00
|
|
|
|
from typing import Any, Union, Optional, Callable
|
2023-04-15 06:31:05 +03:00
|
|
|
|
from textual import events
|
|
|
|
|
from textual.message import Message
|
|
|
|
|
from textual.app import ComposeResult
|
|
|
|
|
from textual.containers import Container
|
2023-04-23 00:49:15 +03:00
|
|
|
|
from textual.geometry import Offset
|
|
|
|
|
from textual.reactive import var
|
2023-04-21 01:00:01 +03:00
|
|
|
|
from textual.widget import Widget
|
2023-04-25 03:21:40 +03:00
|
|
|
|
from textual.widgets import Button, Static, DataTable
|
2023-04-21 01:00:01 +03:00
|
|
|
|
from textual.containers import Container, Horizontal, Vertical
|
2023-04-16 07:59:23 +03:00
|
|
|
|
from textual.css.query import NoMatches
|
2023-04-21 01:00:01 +03:00
|
|
|
|
from localization.i18n import get as _
|
2023-04-15 06:31:05 +03:00
|
|
|
|
|
|
|
|
|
class WindowTitleBar(Container):
|
|
|
|
|
"""A title bar widget."""
|
|
|
|
|
|
2023-04-22 09:27:54 +03:00
|
|
|
|
title = var("")
|
2023-04-15 06:31:05 +03:00
|
|
|
|
|
2023-04-23 04:35:21 +03:00
|
|
|
|
def __init__(self, title: str = "", **kwargs: Any) -> None:
|
2023-04-15 06:31:05 +03:00
|
|
|
|
"""Initialize a title bar."""
|
|
|
|
|
super().__init__(**kwargs)
|
|
|
|
|
self.title = title
|
|
|
|
|
|
|
|
|
|
def compose(self) -> ComposeResult:
|
|
|
|
|
"""Add our widgets."""
|
|
|
|
|
yield Static(self.title, classes="window_title")
|
|
|
|
|
yield Button("🗙", classes="window_close")
|
|
|
|
|
# yield Button("🗕", classes="window_minimize")
|
|
|
|
|
# yield Button("🗖", classes="window_maximize")
|
|
|
|
|
# 🗗 for restore
|
|
|
|
|
|
2023-04-23 09:06:33 +03:00
|
|
|
|
id_counter = 0
|
2023-04-15 06:31:05 +03:00
|
|
|
|
class Window(Container):
|
|
|
|
|
"""A draggable window widget."""
|
|
|
|
|
|
|
|
|
|
class CloseRequest(Message):
|
|
|
|
|
"""Message when the user clicks the close button. Can be prevented."""
|
|
|
|
|
|
2023-04-23 04:35:21 +03:00
|
|
|
|
def __init__(self, **kwargs: Any) -> None:
|
2023-04-15 06:31:05 +03:00
|
|
|
|
"""Initialize a close request."""
|
|
|
|
|
super().__init__(**kwargs)
|
|
|
|
|
self.prevent_close = False
|
|
|
|
|
|
|
|
|
|
class Closed(Message):
|
|
|
|
|
"""Message when the window is really closed."""
|
|
|
|
|
|
|
|
|
|
title = var([])
|
|
|
|
|
|
2023-04-23 09:06:33 +03:00
|
|
|
|
BINDINGS = [
|
|
|
|
|
# Binding("tab", "focus_next", "Focus Next", show=False),
|
|
|
|
|
# Binding("shift+tab", "focus_previous", "Focus Previous", show=False),
|
|
|
|
|
("tab", "focus_next", "Focus Next"),
|
|
|
|
|
("shift+tab", "focus_previous", "Focus Previous"),
|
2023-04-23 22:38:36 +03:00
|
|
|
|
("right,down", "focus_next_button", "Focus Next Button"),
|
|
|
|
|
("left,up", "focus_previous_button", "Focus Previous Button"),
|
2023-04-23 09:06:33 +03:00
|
|
|
|
]
|
|
|
|
|
|
2023-04-23 04:35:21 +03:00
|
|
|
|
def __init__(self, *children: Widget, title: str = "", **kwargs: Any) -> None:
|
2023-04-15 06:31:05 +03:00
|
|
|
|
"""Initialize a window."""
|
|
|
|
|
super().__init__(*children, **kwargs)
|
|
|
|
|
self.mouse_at_drag_start = None
|
|
|
|
|
self.offset_at_drag_start = None
|
|
|
|
|
self.title_bar = WindowTitleBar(title=title)
|
|
|
|
|
self.content = Container(classes="window_content")
|
|
|
|
|
# must be after title_bar is defined
|
|
|
|
|
self.title = title
|
2023-04-20 19:12:36 +03:00
|
|
|
|
self.can_focus = True
|
2023-04-23 09:06:33 +03:00
|
|
|
|
if not self.id:
|
|
|
|
|
# ID is needed for focus cycling
|
|
|
|
|
global id_counter
|
|
|
|
|
self.id = f"window_auto_id_{id_counter}"
|
|
|
|
|
id_counter += 1
|
|
|
|
|
|
|
|
|
|
def action_focus_next(self) -> None:
|
|
|
|
|
"""Override action to focus the next widget only within the window."""
|
|
|
|
|
self.screen.focus_next(f"#{self.id} .window_content *")
|
|
|
|
|
|
|
|
|
|
def action_focus_previous(self) -> None:
|
|
|
|
|
"""Override action to focus the previous widget only within the window."""
|
|
|
|
|
self.screen.focus_previous(f"#{self.id} .window_content *")
|
2023-04-15 06:31:05 +03:00
|
|
|
|
|
2023-04-23 22:38:36 +03:00
|
|
|
|
def within_buttons(self, widget: Widget|None) -> bool:
|
|
|
|
|
"""Returns True if widget exists and is within .buttons."""
|
|
|
|
|
if not widget:
|
|
|
|
|
return False
|
|
|
|
|
node = widget
|
|
|
|
|
while node:
|
|
|
|
|
if node.has_class("buttons"):
|
|
|
|
|
return True
|
|
|
|
|
node = node.parent
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
def action_focus_next_button(self) -> None:
|
|
|
|
|
"""Action to focus the next button within .buttons IF a button is focused within .buttons."""
|
|
|
|
|
if self.within_buttons(self.screen.focused):
|
|
|
|
|
self.screen.focus_next(f"#{self.id} .buttons *")
|
|
|
|
|
|
|
|
|
|
def action_focus_previous_button(self) -> None:
|
|
|
|
|
"""Action to focus the previous button within .buttons IF a button is focused within .buttons."""
|
|
|
|
|
if self.within_buttons(self.screen.focused):
|
|
|
|
|
self.screen.focus_previous(f"#{self.id} .buttons *")
|
|
|
|
|
|
2023-04-15 06:31:05 +03:00
|
|
|
|
def on_mount(self) -> None:
|
|
|
|
|
"""Called when the widget is mounted."""
|
|
|
|
|
self.mount(self.title_bar)
|
|
|
|
|
self.mount(self.content)
|
2023-04-20 19:38:21 +03:00
|
|
|
|
# Set focus. (In the future, some windows will not want default focus...)
|
|
|
|
|
self.focus()
|
2023-04-16 04:59:29 +03:00
|
|
|
|
# Fix for incorrect layout that would only resolve on mouse over
|
|
|
|
|
# (I peaked into mouse over handling and it calls update_styles.)
|
|
|
|
|
# This can still briefly show the incorrect layout, since it relies on a timer.
|
|
|
|
|
self.set_timer(0.01, lambda: self.app.update_styles(self))
|
2023-04-20 19:12:36 +03:00
|
|
|
|
|
|
|
|
|
def on_focus(self, event: events.Focus) -> None:
|
|
|
|
|
"""Called when the window is focused."""
|
|
|
|
|
# TODO: focus last focused widget if re-focusing
|
|
|
|
|
controls = self.content.query(".submit, Input, Button")
|
|
|
|
|
if controls:
|
|
|
|
|
controls[0].focus()
|
2023-04-15 06:31:05 +03:00
|
|
|
|
|
|
|
|
|
# def compose(self) -> ComposeResult:
|
|
|
|
|
# """Add our widgets."""
|
|
|
|
|
# self.title_bar = yield WindowTitleBar(title=self.title)
|
|
|
|
|
# self.content = yield Container(classes="window_content")
|
|
|
|
|
|
|
|
|
|
# def compose_add_child(self, widget):
|
|
|
|
|
# """When using the context manager compose syntax, we want to attach nodes to the content container."""
|
|
|
|
|
# self.content.mount(widget)
|
|
|
|
|
|
2023-04-23 03:46:16 +03:00
|
|
|
|
def watch_title(self, new_title: str) -> None:
|
2023-04-15 06:31:05 +03:00
|
|
|
|
"""Called when title is changed."""
|
|
|
|
|
self.title_bar.title = new_title
|
|
|
|
|
|
|
|
|
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
|
|
|
"""Called when a button is clicked or activated with the keyboard."""
|
|
|
|
|
|
|
|
|
|
if event.button.has_class("window_close"):
|
2023-04-16 07:59:23 +03:00
|
|
|
|
self.request_close()
|
2023-04-15 06:31:05 +03:00
|
|
|
|
|
|
|
|
|
def on_mouse_down(self, event: events.MouseDown) -> None:
|
|
|
|
|
"""Called when the user presses the mouse button."""
|
2023-04-16 05:18:45 +03:00
|
|
|
|
# detect if the mouse is over the title bar,
|
|
|
|
|
# and not window content or title bar buttons
|
2023-04-16 07:59:23 +03:00
|
|
|
|
if not self.parent:
|
|
|
|
|
# I got NoScreen error accessing self.screen once while closing a window,
|
|
|
|
|
# so I added this check.
|
|
|
|
|
return
|
2023-04-16 05:18:45 +03:00
|
|
|
|
widget, _ = self.screen.get_widget_at(*event.screen_offset)
|
|
|
|
|
if widget not in [self.title_bar, self.title_bar.query_one(".window_title")]:
|
|
|
|
|
return
|
2023-04-15 06:31:05 +03:00
|
|
|
|
|
|
|
|
|
if event.button != 1:
|
|
|
|
|
return
|
|
|
|
|
self.mouse_at_drag_start = event.screen_offset
|
2023-04-22 20:11:02 +03:00
|
|
|
|
self.offset_at_drag_start = Offset(int(self.styles.offset.x.value), int(self.styles.offset.y.value))
|
2023-04-15 06:31:05 +03:00
|
|
|
|
self.capture_mouse()
|
2023-04-21 04:39:16 +03:00
|
|
|
|
"""
|
|
|
|
|
Work around a bug in textual where the MouseUp event
|
|
|
|
|
is not sent to this widget if another widget gets focus,
|
|
|
|
|
and thus, release_mouse() is not called,
|
|
|
|
|
and you can never release the drag or click anything else.
|
|
|
|
|
|
|
|
|
|
An excerpt from Screen._forward_event:
|
|
|
|
|
|
|
|
|
|
if isinstance(event, events.MouseUp) and widget.focusable:
|
|
|
|
|
if self.focused is not widget:
|
|
|
|
|
self.set_focus(widget)
|
|
|
|
|
event.stop()
|
|
|
|
|
return
|
|
|
|
|
event.style = self.get_style_at(event.screen_x, event.screen_y)
|
|
|
|
|
if widget is self:
|
|
|
|
|
event._set_forwarded()
|
|
|
|
|
self.post_message(event)
|
|
|
|
|
else:
|
|
|
|
|
widget._forward_event(event._apply_offset(-region.x, -region.y))
|
|
|
|
|
|
|
|
|
|
Note the return statement.
|
|
|
|
|
I don't know what this special case is for,
|
|
|
|
|
but for this work around, I can make widget.focusable False,
|
|
|
|
|
so that the special case doesn't apply.
|
|
|
|
|
(`focusable` is a getter that uses can_focus and checks ancestors are enabled)
|
|
|
|
|
"""
|
|
|
|
|
self.can_focus = False
|
2023-04-15 06:31:05 +03:00
|
|
|
|
|
|
|
|
|
def on_mouse_move(self, event: events.MouseMove) -> None:
|
|
|
|
|
"""Called when the user moves the mouse."""
|
2023-04-25 21:22:54 +03:00
|
|
|
|
if self.mouse_at_drag_start is not None and self.offset_at_drag_start is not None:
|
2023-04-15 06:31:05 +03:00
|
|
|
|
self.styles.offset = (
|
|
|
|
|
self.offset_at_drag_start.x + event.screen_x - self.mouse_at_drag_start.x,
|
|
|
|
|
self.offset_at_drag_start.y + event.screen_y - self.mouse_at_drag_start.y,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def on_mouse_up(self, event: events.MouseUp) -> None:
|
|
|
|
|
"""Called when the user releases the mouse button."""
|
|
|
|
|
self.mouse_at_drag_start = None
|
|
|
|
|
self.offset_at_drag_start = None
|
|
|
|
|
self.release_mouse()
|
2023-04-21 04:39:16 +03:00
|
|
|
|
# Part of the workaround for the bug mentioned in on_mouse_down
|
|
|
|
|
self.can_focus = True
|
2023-04-15 06:31:05 +03:00
|
|
|
|
|
|
|
|
|
def close(self) -> None:
|
2023-04-16 07:59:23 +03:00
|
|
|
|
"""Force close the window."""
|
2023-04-15 06:31:05 +03:00
|
|
|
|
self.remove()
|
|
|
|
|
self.post_message(self.Closed())
|
2023-04-16 07:59:23 +03:00
|
|
|
|
|
|
|
|
|
def request_close(self) -> None:
|
|
|
|
|
"""Request to close the window."""
|
|
|
|
|
close_request = self.CloseRequest()
|
|
|
|
|
self.post_message(close_request)
|
|
|
|
|
if not close_request.prevent_close:
|
|
|
|
|
self.close()
|
2023-04-16 06:56:37 +03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DialogWindow(Window):
|
|
|
|
|
"""A window that can be submitted like a form."""
|
|
|
|
|
|
2023-04-23 04:35:21 +03:00
|
|
|
|
def __init__(self, *children: Widget, handle_button: Callable[[Button], None], **kwargs: Any) -> None:
|
2023-04-16 06:56:37 +03:00
|
|
|
|
"""Initialize a dialog window."""
|
|
|
|
|
super().__init__(*children, **kwargs)
|
2023-04-16 07:59:23 +03:00
|
|
|
|
self.handle_button = handle_button
|
2023-04-16 06:56:37 +03:00
|
|
|
|
|
|
|
|
|
def on_key(self, event: events.Key) -> None:
|
|
|
|
|
"""Called when a key is pressed."""
|
2023-04-16 09:03:28 +03:00
|
|
|
|
# submit with enter, but not if a button has focus
|
2023-04-16 07:59:23 +03:00
|
|
|
|
# (not even if it's a submit button, because that would double submit)
|
|
|
|
|
# TODO: Use on_input_submitted instead
|
2023-04-16 09:03:28 +03:00
|
|
|
|
if event.key == "enter" and self.app.focused not in self.query("Button").nodes:
|
2023-04-16 07:59:23 +03:00
|
|
|
|
try:
|
|
|
|
|
submit_button = self.query_one(".submit", Button)
|
|
|
|
|
except NoMatches:
|
|
|
|
|
return
|
|
|
|
|
self.handle_button(submit_button)
|
2023-04-16 09:03:28 +03:00
|
|
|
|
elif event.key == "escape":
|
2023-04-16 07:59:23 +03:00
|
|
|
|
# Like the title bar close button,
|
|
|
|
|
# this doesn't call handle_button...
|
|
|
|
|
self.request_close()
|
2023-04-16 06:56:37 +03:00
|
|
|
|
|
|
|
|
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
|
|
|
"""Called when a button is clicked or activated with the keyboard."""
|
2023-04-16 07:59:23 +03:00
|
|
|
|
# Make sure the button is in the window content
|
|
|
|
|
if event.button in self.content.query("Button").nodes:
|
|
|
|
|
self.handle_button(event.button)
|
2023-04-20 07:29:14 +03:00
|
|
|
|
|
2023-04-20 19:12:36 +03:00
|
|
|
|
# def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
|
|
|
# """Called when a the enter key is pressed in an Input."""
|
|
|
|
|
|
2023-04-20 07:29:14 +03:00
|
|
|
|
|
2023-04-21 00:38:18 +03:00
|
|
|
|
class CharacterSelectorDialogWindow(DialogWindow):
|
2023-04-20 07:29:14 +03:00
|
|
|
|
"""A dialog window that lets the user select a character."""
|
|
|
|
|
|
|
|
|
|
# class CharacterSelected(Message):
|
|
|
|
|
# """Sent when a character is selected."""
|
|
|
|
|
# def __init__(self, character: str) -> None:
|
|
|
|
|
# """Initialize the message."""
|
|
|
|
|
# self.character = character
|
|
|
|
|
|
2023-04-26 22:47:03 +03:00
|
|
|
|
# NUL at the beginning (0), SP in the middle (32), and NBSP at the end (255)
|
2023-04-26 22:48:21 +03:00
|
|
|
|
# are all treated as space when selected. Null can cause the screen to malfunction
|
2023-04-26 22:47:03 +03:00
|
|
|
|
# if it's inserted into the document.
|
2023-04-20 07:29:14 +03:00
|
|
|
|
# spell-checker: disable
|
2023-04-26 22:47:03 +03:00
|
|
|
|
code_page_437 = "\0☺☻♥♦♣♠•◘○◙♂♀♪♫☼►◄↕‼¶§▬↨↑↓→←∟↔▲▼ !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~⌂ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜ¢£¥₧ƒáíóúñѪº¿⌐¬½¼¡«»░▒▓│┤╡╢╖╕╣║╗╝╜╛┐└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ²■ "
|
2023-04-20 07:29:14 +03:00
|
|
|
|
# spell-checker: enable
|
2023-04-26 22:48:21 +03:00
|
|
|
|
char_list = [char for char in code_page_437]
|
2023-04-20 07:29:14 +03:00
|
|
|
|
|
2023-04-23 04:35:21 +03:00
|
|
|
|
def __init__(self, *children: Widget, selected_character: str|None, handle_selected_character: Callable[[str], None], **kwargs: Any) -> None:
|
2023-04-20 07:29:14 +03:00
|
|
|
|
"""Initialize the dialog window."""
|
2023-04-23 03:46:16 +03:00
|
|
|
|
super().__init__(handle_button=self.handle_button, *children, **kwargs)
|
2023-04-26 20:19:06 +03:00
|
|
|
|
self._selected_character: str|None = selected_character
|
2023-04-20 07:29:14 +03:00
|
|
|
|
self.handle_selected_character = handle_selected_character
|
|
|
|
|
|
|
|
|
|
def handle_button(self, button: Button) -> None:
|
|
|
|
|
"""Called when a button is clicked or activated with the keyboard."""
|
2023-04-23 21:50:36 +03:00
|
|
|
|
if button.has_class("cancel"):
|
2023-04-20 07:29:14 +03:00
|
|
|
|
self.request_close()
|
|
|
|
|
else:
|
2023-04-26 20:19:06 +03:00
|
|
|
|
if self._selected_character is None:
|
|
|
|
|
# Probably shouldn't happen
|
|
|
|
|
return
|
|
|
|
|
# self.post_message(self.CharacterSelected(self._selected_character))
|
2023-04-20 07:29:14 +03:00
|
|
|
|
# self.close()
|
2023-04-25 03:21:40 +03:00
|
|
|
|
self.handle_selected_character(self._selected_character)
|
|
|
|
|
|
|
|
|
|
def on_data_table_cell_highlighted(self, event: DataTable.CellHighlighted) -> None:
|
|
|
|
|
"""Called when a cell is highlighted."""
|
2023-04-26 22:33:51 +03:00
|
|
|
|
# assert isinstance(event.value, str), "DataTable should only contain strings, but got: " + repr(event.value)
|
2023-04-26 22:48:21 +03:00
|
|
|
|
self._selected_character = event.value if isinstance(event.value, str) and event.value != "\0" else " "
|
2023-04-20 07:29:14 +03:00
|
|
|
|
|
|
|
|
|
def on_mount(self) -> None:
|
|
|
|
|
"""Called when the window is mounted."""
|
2023-04-25 03:21:40 +03:00
|
|
|
|
data_table: DataTable[str] = DataTable()
|
|
|
|
|
column_count = 16
|
|
|
|
|
data_table.add_columns(*([" "] * column_count))
|
|
|
|
|
data_table.show_header = False
|
|
|
|
|
for i in range(0, len(self.char_list), column_count):
|
|
|
|
|
data_table.add_row(*self.char_list[i:i+column_count])
|
|
|
|
|
self.content.mount(data_table)
|
2023-04-25 03:28:57 +03:00
|
|
|
|
self.content.mount(Button(_("OK"), classes="ok submit"))
|
|
|
|
|
self.content.mount(Button(_("Cancel"), classes="cancel"))
|
2023-04-21 01:00:01 +03:00
|
|
|
|
|
2023-04-21 01:31:35 +03:00
|
|
|
|
# ASCII line art version:
|
2023-04-21 04:00:43 +03:00
|
|
|
|
# get_warning_icon = lambda: Static("""[#ffff00]
|
2023-04-21 01:00:01 +03:00
|
|
|
|
# _
|
|
|
|
|
# / \\
|
|
|
|
|
# / | \\
|
|
|
|
|
# / . \\
|
|
|
|
|
# /_______\\
|
2023-04-21 01:31:35 +03:00
|
|
|
|
# [/]""", classes="warning_icon message_box_icon")
|
|
|
|
|
# Unicode solid version 1:
|
2023-04-21 04:00:43 +03:00
|
|
|
|
# get_warning_icon = lambda: Static("""[#ffff00 on #000000]
|
2023-04-21 01:00:01 +03:00
|
|
|
|
# _
|
|
|
|
|
# ◢█◣
|
|
|
|
|
# ◢[#000000 on #ffff00] ▼ [/]◣
|
|
|
|
|
# ◢[#000000 on #ffff00] ● [/]◣
|
|
|
|
|
# ◢███████◣
|
2023-04-21 01:31:35 +03:00
|
|
|
|
# [/]""", classes="warning_icon message_box_icon")
|
|
|
|
|
# Unicode line art version (' might be a better than ╰/╯):
|
2023-04-21 04:00:43 +03:00
|
|
|
|
# get_warning_icon = lambda: Static("""[#ffff00]
|
2023-04-21 01:00:01 +03:00
|
|
|
|
# _
|
|
|
|
|
# ╱ ╲
|
|
|
|
|
# ╱ │ ╲
|
|
|
|
|
# ╱ . ╲
|
|
|
|
|
# ╰───────╯
|
2023-04-21 01:31:35 +03:00
|
|
|
|
# """, classes="warning_icon message_box_icon")
|
|
|
|
|
# Unicode solid version 2:
|
2023-04-21 04:00:43 +03:00
|
|
|
|
# get_warning_icon = lambda: Static("""[#ffff00 on #000000]
|
2023-04-21 01:00:01 +03:00
|
|
|
|
# 🭯
|
|
|
|
|
# 🭅[#000000 on #ffff00]🭯[/]🭐
|
|
|
|
|
# 🭅[#000000 on #ffff00] ▼ [/]🭐
|
|
|
|
|
# 🭅[#000000 on #ffff00] ● [/]🭐
|
|
|
|
|
# 🭅███████🭐
|
2023-04-21 01:31:35 +03:00
|
|
|
|
# [/]""", classes="warning_icon message_box_icon")
|
|
|
|
|
# Unicode solid version 3, now with a border:
|
|
|
|
|
# VS Code's terminal seems unsure of the width of these characters (like it's rendering 2 wide but advancing by 1), and has gaps/seams.
|
|
|
|
|
# Ubuntu's terminal looks better, and the graphics have less gaps, but the overall shape is worse.
|
|
|
|
|
# I guess a lot of this comes down to the font as well.
|
2023-04-24 23:06:16 +03:00
|
|
|
|
# get_warning_icon = lambda: Static("""
|
|
|
|
|
# [#000000]🭋[#ffff00 on #000000]🭯[/]🭀[/]
|
|
|
|
|
# [#000000]🭋[#ffff00 on #000000]🭅█🭐[/]🭀[/]
|
|
|
|
|
# [#000000]🭋[#ffff00 on #000000]🭅[#000000 on #ffff00] ▼ [/]🭐[/]🭀[/]
|
|
|
|
|
# [#000000]🭋[#ffff00 on #000000]🭅[#000000 on #ffff00] ● [/]🭐[/]🭀[/]
|
|
|
|
|
# [#000000]🭋[#ffff00 on #000000]🭅███████🭐[/]🭀[/]
|
|
|
|
|
# [#000000]🮃🮃🮃🮃🮃🮃🮃🮃🮃🮃🮃[/]
|
|
|
|
|
# """, classes="warning_icon message_box_icon")
|
|
|
|
|
# Unicode solid version 4:
|
|
|
|
|
# This now looks great in Ubuntu's terminal.
|
|
|
|
|
# In VS Code's terminal, all the gaps make it look like it's under frosted glass,
|
2023-04-24 23:31:57 +03:00
|
|
|
|
# but it's acceptable. Alternatively, you may see it as looking "spiky",
|
|
|
|
|
# which is sensible for a warning icon, if not particularly attractive.
|
|
|
|
|
# get_warning_icon = lambda: Static("""
|
|
|
|
|
# [#000000]◢[#ffff00 on #000000]🭯[/]◣[/]
|
|
|
|
|
# [#000000]◢[#ffff00 on #000000]◢█◣[/]◣[/]
|
|
|
|
|
# [#000000]◢[#ffff00 on #000000]◢[#000000 on #ffff00] ▼ [/]◣[/]◣[/]
|
|
|
|
|
# [#000000]◢[#ffff00 on #000000]◢[#000000 on #ffff00] ● [/]◣[/]◣[/]
|
|
|
|
|
# [#000000]◢[#ffff00 on #000000]◢███████◣[/]◣[/]
|
|
|
|
|
# [#000000]🮃🮃🮃🮃🮃🮃🮃🮃🮃🮃🮃[/]
|
|
|
|
|
# """, classes="warning_icon message_box_icon")
|
|
|
|
|
# Unicode solid version 5, rounder exclamation mark:
|
|
|
|
|
# get_warning_icon = lambda: Static("""
|
|
|
|
|
# [#000000]◢[#ffff00 on #000000]🭯[/]◣[/]
|
|
|
|
|
# [#000000]◢[#ffff00 on #000000]◢█◣[/]◣[/]
|
|
|
|
|
# [#000000]◢[#ffff00 on #000000]◢[#000000 on #ffff00] ⬮ [/]◣[/]◣[/]
|
|
|
|
|
# [#000000]◢[#ffff00 on #000000]◢[#000000 on #ffff00] • [/]◣[/]◣[/]
|
|
|
|
|
# [#000000]◢[#ffff00 on #000000]◢███████◣[/]◣[/]
|
|
|
|
|
# [#000000]🮃🮃🮃🮃🮃🮃🮃🮃🮃🮃🮃[/]
|
|
|
|
|
# """, classes="warning_icon message_box_icon")
|
|
|
|
|
# Unicode solid version 6, smaller overall:
|
2023-04-21 04:00:43 +03:00
|
|
|
|
get_warning_icon = lambda: Static("""
|
2023-04-24 23:31:57 +03:00
|
|
|
|
[#000000]◢[#ffff00 on #000000]🭯[/]◣[/]
|
|
|
|
|
[#000000]◢[#ffff00 on #000000]◢█◣[/]◣[/]
|
|
|
|
|
[#000000]◢[#ffff00 on #000000]◢[#000000 on #ffff00] ⬮ [/]◣[/]◣[/]
|
|
|
|
|
[#000000]◢[#ffff00 on #000000]◢[#000000 on #ffff00] • [/]◣[/]◣[/]
|
|
|
|
|
[#000000]🮃🮃🮃🮃🮃🮃🮃🮃🮃[/]
|
2023-04-21 01:31:35 +03:00
|
|
|
|
""", classes="warning_icon message_box_icon")
|
|
|
|
|
|
|
|
|
|
class MessageBox(DialogWindow):
|
|
|
|
|
"""A simple dialog window that displays a message, a group of buttons, and an optional icon."""
|
2023-04-23 03:46:16 +03:00
|
|
|
|
def __init__(self,
|
|
|
|
|
*children: Widget,
|
|
|
|
|
message_widget: Union[Widget, str],
|
|
|
|
|
button_types: str = "ok",
|
|
|
|
|
icon_widget: Optional[Widget],
|
|
|
|
|
handle_button: Callable[[Button], None],
|
2023-04-23 04:35:21 +03:00
|
|
|
|
**kwargs: Any,
|
2023-04-23 03:46:16 +03:00
|
|
|
|
) -> None:
|
2023-04-21 01:31:35 +03:00
|
|
|
|
"""Initialize the message box."""
|
2023-04-23 03:46:16 +03:00
|
|
|
|
super().__init__(*children, handle_button=handle_button, **kwargs)
|
2023-04-21 01:31:35 +03:00
|
|
|
|
if isinstance(message_widget, str):
|
|
|
|
|
message_widget = Static(message_widget, markup=False)
|
|
|
|
|
if not icon_widget:
|
|
|
|
|
icon_widget = Static("")
|
|
|
|
|
self.message_widget = message_widget
|
|
|
|
|
self.icon_widget = icon_widget
|
|
|
|
|
self.button_types = button_types
|
|
|
|
|
|
|
|
|
|
def on_mount(self):
|
|
|
|
|
"""Called when the window is mounted."""
|
|
|
|
|
|
|
|
|
|
if self.button_types == "ok":
|
|
|
|
|
buttons = [Button(_("OK"), classes="ok submit", variant="primary")]
|
|
|
|
|
elif self.button_types == "yes/no":
|
|
|
|
|
buttons = [
|
|
|
|
|
Button(_("Yes"), classes="yes submit"), #, variant="primary"),
|
|
|
|
|
Button(_("No"), classes="no"),
|
|
|
|
|
]
|
|
|
|
|
elif self.button_types == "yes/no/cancel":
|
|
|
|
|
buttons = [
|
|
|
|
|
Button(_("Yes"), classes="yes submit", variant="primary"),
|
|
|
|
|
Button(_("No"), classes="no"),
|
|
|
|
|
Button(_("Cancel"), classes="cancel"),
|
|
|
|
|
]
|
|
|
|
|
else:
|
|
|
|
|
raise ValueError("Invalid button_types: " + repr(self.button_types))
|
|
|
|
|
|
|
|
|
|
self.content.mount(
|
|
|
|
|
Horizontal(
|
|
|
|
|
self.icon_widget,
|
|
|
|
|
Vertical(
|
|
|
|
|
self.message_widget,
|
2023-04-23 22:38:36 +03:00
|
|
|
|
# Window class provides arrow key navigation within .buttons
|
2023-04-21 01:31:35 +03:00
|
|
|
|
Horizontal(*buttons, classes="buttons"),
|
|
|
|
|
classes="main_content"
|
|
|
|
|
)
|
2023-04-21 01:00:01 +03:00
|
|
|
|
)
|
|
|
|
|
)
|