From 4832ea28bf25ffb27fa0e8c483e1f33f7d127308 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 22 Aug 2023 14:37:11 +0930 Subject: [PATCH 1/6] Correct the aspect ratio for terminal layouts. --- textual/src/toga_textual/widgets/base.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/textual/src/toga_textual/widgets/base.py b/textual/src/toga_textual/widgets/base.py index da21ee780f..e3026153f9 100644 --- a/textual/src/toga_textual/widgets/base.py +++ b/textual/src/toga_textual/widgets/base.py @@ -3,12 +3,11 @@ from toga.style.pack import ROW -# We assume a terminal is 800x600 pixels, mapping to 80x25 characters; -# then deduct 1 row for the titlebar of the window. +# We assume a terminal is 800x600 pixels, mapping to 80x25 characters. # This results in an uneven scale in the horizontal and vertical directions. class Scalable: HORIZONTAL_SCALE = 800 // 80 - VERTICAL_SCALE = 600 // 24 + VERTICAL_SCALE = 600 // 25 def scale_in_horizontal(self, value): return value // self.HORIZONTAL_SCALE From a4e2ee50888437e158cdf95230fb92df0e15d293 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 22 Aug 2023 17:59:07 +0930 Subject: [PATCH 2/6] Add info, question, confirm, error, and stack trace dialogs. Co-authored-by: Katie McLaughlin --- textual/src/toga_textual/dialogs.py | 206 ++++++++++++++++++++++++++++ textual/src/toga_textual/factory.py | 4 +- 2 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 textual/src/toga_textual/dialogs.py diff --git a/textual/src/toga_textual/dialogs.py b/textual/src/toga_textual/dialogs.py new file mode 100644 index 0000000000..9e0227fd51 --- /dev/null +++ b/textual/src/toga_textual/dialogs.py @@ -0,0 +1,206 @@ +from abc import ABC + +from textual.app import ComposeResult +from textual.containers import Horizontal, Vertical, VerticalScroll +from textual.screen import ModalScreen +from textual.widgets import Button, Header, Label, Static + + +class TextualDialog(ModalScreen[bool]): + def __init__(self, impl): + super().__init__() + self.impl = impl + + def compose(self) -> ComposeResult: + self.native_title = Header(name=self.impl.title) + self.impl.compose_content(self) + self.native_buttons = self.impl.create_buttons() + self.native_button_box = Horizontal(*self.native_buttons) + self.native_dialog = Vertical( + self.native_title, + self.native_content, + self.native_button_box, + id="dialog", + ) + yield self.native_dialog + + def on_mount(self) -> None: + self.styles.align = ("center", "middle") + + self.native_dialog.styles.width = 50 + self.native_dialog.styles.border = ("solid", "darkgray") + + self.impl.mount_content(self) + + self.native_button_box.styles.align = ("center", "middle") + for native_button in self.native_buttons: + native_button.styles.margin = (0, 1, 0, 1) + + def on_button_pressed(self, event: Button.Pressed) -> None: + if event.button.id == "result-True": + self.dismiss(True) + elif event.button.id == "result-False": + self.dismiss(False) + else: + self.dismiss(None) + + +class BaseDialog(ABC): + def __init__(self, interface, title, message, on_result): + self.interface = interface + self.interface._impl = self + self.on_result = on_result + self.title = title + self.message = message + + self.native = TextualDialog(self) + self.interface.app._impl.native.push_screen(self.native, self.on_close) + + def compose_content(self, dialog): + dialog.native_content = Label(self.message, id="message") + + def mount_content(self, dialog): + dialog.native_content.styles.margin = 1 + dialog.native_content.styles.height = 5 + + dialog.native_dialog.styles.height = 13 + + def on_close(self, result: bool): + self.on_result(self, result) + self.interface.future.set_result(result) + + +class InfoDialog(BaseDialog): + def create_buttons(self): + return [ + Button("OK", variant="primary", id="result-None"), + ] + + +class QuestionDialog(BaseDialog): + def create_buttons(self): + return [ + Button("Yes", variant="primary", id="result-True"), + Button("No", variant="error", id="result-False"), + ] + + +class ConfirmDialog(BaseDialog): + def create_buttons(self): + return [ + Button("Cancel", variant="error", id="result-False"), + Button("OK", variant="primary", id="result-True"), + ] + + +class ErrorDialog(BaseDialog): + def create_buttons(self): + return [ + Button("OK", variant="primary", id="result-None"), + ] + + +class StackTraceDialog(BaseDialog): + def __init__( + self, + interface, + title, + message, + on_result=None, + retry=False, + content="", + ): + super().__init__( + interface=interface, + title=title, + message=message, + on_result=on_result, + ) + self.retry = retry + self.content = content + + def compose_content(self, dialog): + dialog.native_label = Label(self.message, id="message") + dialog.native_scroll = VerticalScroll( + Static(self.content, id="content"), + ) + dialog.native_content = Vertical( + dialog.native_label, + dialog.native_scroll, + ) + + def create_buttons(self): + if self.retry: + return [ + Button("Cancel", variant="error", id="result-False"), + Button("Retry", variant="primary", id="result-True"), + ] + else: + return [ + Button("OK", variant="primary", id="result-None"), + ] + + def mount_content(self, dialog): + dialog.native_content.styles.margin = 1 + dialog.native_content.styles.height = self.interface.window.size[1] - 18 + + dialog.native_dialog.styles.width = "80%" + dialog.native_dialog.styles.height = self.interface.window.size[1] - 10 + + dialog.native_label.styles.margin = (0, 0, 1, 0) + + +class SaveFileDialog(BaseDialog): + def __init__( + self, + interface, + title, + filename, + initial_directory, + file_types=None, + on_result=None, + ): + super().__init__(interface=interface) + self.on_result = on_result + + interface.window.factory.not_implemented("Window.save_file_dialog()") + + self.on_result(self, None) + self.interface.future.set_result(None) + + +class OpenFileDialog(BaseDialog): + def __init__( + self, + interface, + title, + initial_directory, + file_types, + multiselect, + on_result=None, + ): + super().__init__(interface=interface) + self.on_result = on_result + + interface.window.factory.not_implemented("Window.open_file_dialog()") + + self.on_result(self, None) + self.interface.future.set_result(None) + + +class SelectFolderDialog(BaseDialog): + def __init__( + self, + interface, + title, + initial_directory, + multiselect, + on_result=None, + ): + super().__init__(interface=interface) + self.on_result = on_result + + interface.window.factory.not_implemented("Window.select_folder_dialog()") + + self.on_result(self, None) + self.interface.future.set_result(None) diff --git a/textual/src/toga_textual/factory.py b/textual/src/toga_textual/factory.py index 5964350f06..74f345b3dd 100644 --- a/textual/src/toga_textual/factory.py +++ b/textual/src/toga_textual/factory.py @@ -1,4 +1,4 @@ -# from . import dialogs +from . import dialogs from .app import App, DocumentApp, MainWindow # from .command import Command @@ -55,7 +55,7 @@ def not_implemented(feature): "Icon", # "Image", "Paths", - # "dialogs", + "dialogs", # # Widgets # "ActivityIndicator", "Box", From ee371e7af5983c1c1dbb7ffc7b658507dbd56162 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 22 Aug 2023 18:01:15 +0930 Subject: [PATCH 3/6] Add Changenote. --- changes/2092.misc.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/2092.misc.rst diff --git a/changes/2092.misc.rst b/changes/2092.misc.rst new file mode 100644 index 0000000000..16b6a7f30a --- /dev/null +++ b/changes/2092.misc.rst @@ -0,0 +1 @@ +Dialogs were added to the textual backend. From f2efa1755ea7710b08487146ab1d47dbab7f4a2b Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 23 Aug 2023 14:16:46 +0800 Subject: [PATCH 4/6] Add file dialogs, and correct titlebar text handling. --- examples/dialogs/dialogs/app.py | 4 +- textual/src/toga_textual/dialogs.py | 310 +++++++++++++++++----- textual/src/toga_textual/widgets/label.py | 2 +- textual/src/toga_textual/window.py | 92 ++++++- 4 files changed, 337 insertions(+), 71 deletions(-) diff --git a/examples/dialogs/dialogs/app.py b/examples/dialogs/dialogs/app.py index f3971bef67..0bf817687f 100644 --- a/examples/dialogs/dialogs/app.py +++ b/examples/dialogs/dialogs/app.py @@ -6,7 +6,7 @@ from toga.style import Pack -class ExampledialogsApp(toga.App): +class ExampleDialogsApp(toga.App): # Button callback functions def do_clear(self, widget, **kwargs): self.label.text = "Ready." @@ -333,7 +333,7 @@ def startup(self): def main(): - return ExampledialogsApp("Dialogs", "org.beeware.widgets.dialogs") + return ExampleDialogsApp("Dialogs", "org.beeware.widgets.dialogs") if __name__ == "__main__": diff --git a/textual/src/toga_textual/dialogs.py b/textual/src/toga_textual/dialogs.py index 9e0227fd51..ac1c83614f 100644 --- a/textual/src/toga_textual/dialogs.py +++ b/textual/src/toga_textual/dialogs.py @@ -1,9 +1,12 @@ from abc import ABC +from pathlib import Path + +from toga_textual.window import TitleBar from textual.app import ComposeResult from textual.containers import Horizontal, Vertical, VerticalScroll from textual.screen import ModalScreen -from textual.widgets import Button, Header, Label, Static +from textual.widgets import Button, DirectoryTree, Input, Label, Static class TextualDialog(ModalScreen[bool]): @@ -12,37 +15,35 @@ def __init__(self, impl): self.impl = impl def compose(self) -> ComposeResult: - self.native_title = Header(name=self.impl.title) + self.title = TitleBar(self.impl.title) self.impl.compose_content(self) - self.native_buttons = self.impl.create_buttons() - self.native_button_box = Horizontal(*self.native_buttons) - self.native_dialog = Vertical( - self.native_title, - self.native_content, - self.native_button_box, + self.buttons = self.impl.create_buttons() + self.button_box = Horizontal(*self.buttons) + self.container = Vertical( + self.title, + self.content, + self.button_box, id="dialog", ) - yield self.native_dialog + yield self.container def on_mount(self) -> None: self.styles.align = ("center", "middle") - self.native_dialog.styles.width = 50 - self.native_dialog.styles.border = ("solid", "darkgray") + self.container.styles.width = 50 + self.container.styles.border = ("solid", "darkgray") - self.impl.mount_content(self) + self.impl.style_content(self) - self.native_button_box.styles.align = ("center", "middle") - for native_button in self.native_buttons: - native_button.styles.margin = (0, 1, 0, 1) + self.button_box.styles.align = ("center", "middle") + for button in self.buttons: + button.styles.margin = (0, 1, 0, 1) def on_button_pressed(self, event: Button.Pressed) -> None: - if event.button.id == "result-True": - self.dismiss(True) - elif event.button.id == "result-False": - self.dismiss(False) - else: - self.dismiss(None) + self.dismiss(self.impl.return_value(event.button.variant)) + + def on_resize(self, event) -> None: + self.impl.style_content(self) class BaseDialog(ABC): @@ -57,13 +58,16 @@ def __init__(self, interface, title, message, on_result): self.interface.app._impl.native.push_screen(self.native, self.on_close) def compose_content(self, dialog): - dialog.native_content = Label(self.message, id="message") + dialog.content = Label(self.message, id="message") - def mount_content(self, dialog): - dialog.native_content.styles.margin = 1 - dialog.native_content.styles.height = 5 + def style_content(self, dialog): + dialog.content.styles.margin = 1 + dialog.content.styles.height = 5 - dialog.native_dialog.styles.height = 13 + dialog.container.styles.height = 13 + + def return_value(self, variant): + return variant == "primary" def on_close(self, result: bool): self.on_result(self, result) @@ -73,32 +77,38 @@ def on_close(self, result: bool): class InfoDialog(BaseDialog): def create_buttons(self): return [ - Button("OK", variant="primary", id="result-None"), + Button("OK", variant="primary"), ] + def return_value(self, variant): + return None + class QuestionDialog(BaseDialog): def create_buttons(self): return [ - Button("Yes", variant="primary", id="result-True"), - Button("No", variant="error", id="result-False"), + Button("Yes", variant="primary"), + Button("No", variant="error"), ] class ConfirmDialog(BaseDialog): def create_buttons(self): return [ - Button("Cancel", variant="error", id="result-False"), - Button("OK", variant="primary", id="result-True"), + Button("Cancel", variant="error"), + Button("OK", variant="primary"), ] class ErrorDialog(BaseDialog): def create_buttons(self): return [ - Button("OK", variant="primary", id="result-None"), + Button("OK", variant="primary"), ] + def return_value(self, variant): + return None + class StackTraceDialog(BaseDialog): def __init__( @@ -120,34 +130,82 @@ def __init__( self.content = content def compose_content(self, dialog): - dialog.native_label = Label(self.message, id="message") - dialog.native_scroll = VerticalScroll( + dialog.label = Label(self.message, id="message") + dialog.scroll = VerticalScroll( Static(self.content, id="content"), ) - dialog.native_content = Vertical( - dialog.native_label, - dialog.native_scroll, + dialog.content = Vertical( + dialog.label, + dialog.scroll, ) def create_buttons(self): if self.retry: return [ - Button("Cancel", variant="error", id="result-False"), - Button("Retry", variant="primary", id="result-True"), + Button("Cancel", variant="error"), + Button("Retry", variant="primary"), ] else: return [ - Button("OK", variant="primary", id="result-None"), + Button("OK", variant="primary"), ] - def mount_content(self, dialog): - dialog.native_content.styles.margin = 1 - dialog.native_content.styles.height = self.interface.window.size[1] - 18 + def style_content(self, dialog): + dialog.content.styles.margin = 1 + dialog.content.styles.height = self.interface.window.size[1] - 18 + + dialog.container.styles.width = "80%" + dialog.container.styles.height = self.interface.window.size[1] - 10 + + dialog.label.styles.margin = (0, 0, 1, 0) + + def return_value(self, variant): + if self.retry: + return variant == "primary" + else: + return None + + +class ParentFolderButton(Button): + DEFAULT_CSS = """ + ParentFolderButton { + border: none; + min-width: 4; + height: 1; + } + ParentFolderButton.-active { + border: none; + } + """ + + def __init__(self, dialog): + super().__init__("..") + self.dialog = dialog + + def on_button_pressed(self, event): + self.dialog.native.directory_tree.path = ( + self.dialog.native.directory_tree.path.parent + ) + event.stop() - dialog.native_dialog.styles.width = "80%" - dialog.native_dialog.styles.height = self.interface.window.size[1] - 10 - dialog.native_label.styles.margin = (0, 0, 1, 0) +class FilteredDirectoryTree(DirectoryTree): + def __init__(self, dialog): + super().__init__(dialog.initial_directory) + self.dialog = dialog + + def filter_paths(self, paths): + if self.dialog.filter_func: + return [ + path + for path in paths + if (path.is_dir() or self.dialog.filter_func(path)) + ] + else: + return paths + + def on_tree_node_selected(self, event): + self.dialog.on_select_file(event.node.data.path) class SaveFileDialog(BaseDialog): @@ -160,13 +218,64 @@ def __init__( file_types=None, on_result=None, ): - super().__init__(interface=interface) - self.on_result = on_result + super().__init__( + interface=interface, + title=title, + message=None, + on_result=on_result, + ) + self.initial_filename = filename + self.initial_directory = initial_directory if initial_directory else Path.cwd() + self.file_types = file_types + if self.file_types: + self.filter_func = lambda p: p.suffix[1:] in self.file_types + else: + self.filter_func = None + + def compose_content(self, dialog): + dialog.directory_tree = FilteredDirectoryTree(self) + dialog.parent_button = ParentFolderButton(self) + dialog.scroll = VerticalScroll(dialog.directory_tree) + dialog.filename_label = Label("Filename:") + dialog.filename = Input(self.initial_filename) + dialog.file_specifier = Horizontal( + dialog.filename_label, + dialog.filename, + ) + dialog.content = Vertical( + dialog.parent_button, + dialog.scroll, + dialog.file_specifier, + ) + + def create_buttons(self): + return [ + Button("Cancel", variant="error"), + Button("OK", variant="primary"), + ] - interface.window.factory.not_implemented("Window.save_file_dialog()") + def style_content(self, dialog): + dialog.content.styles.margin = 1 + dialog.content.styles.height = self.interface.window.size[1] - 18 - self.on_result(self, None) - self.interface.future.set_result(None) + dialog.filename_label.styles.margin = (1, 0) + dialog.scroll.styles.height = self.interface.window.size[1] - 22 + + dialog.container.styles.width = "80%" + dialog.container.styles.height = self.interface.window.size[1] - 10 + + def on_select_file(self, path): + if path.is_file(): + self.native.filename.value = path.name + + def return_value(self, variant): + if variant == "primary": + return ( + self.native.directory_tree.cursor_node.data.path.parent + / self.native.filename.value + ) + else: + return None class OpenFileDialog(BaseDialog): @@ -179,13 +288,55 @@ def __init__( multiselect, on_result=None, ): - super().__init__(interface=interface) - self.on_result = on_result + super().__init__( + interface=interface, + title=title, + message=None, + on_result=on_result, + ) + self.initial_directory = initial_directory if initial_directory else Path.cwd() + self.file_types = file_types + if self.file_types: + self.filter_func = lambda p: p.is_dir() or p.suffix[1:] in self.file_types + else: + self.filter_func = None + + self.multiselect = multiselect + + def compose_content(self, dialog): + dialog.directory_tree = FilteredDirectoryTree(self) + dialog.parent_button = ParentFolderButton(self) + dialog.scroll = VerticalScroll(dialog.directory_tree) + dialog.content = Vertical( + dialog.parent_button, + dialog.scroll, + ) - interface.window.factory.not_implemented("Window.open_file_dialog()") + def create_buttons(self): + return [ + Button("Cancel", variant="error"), + Button("OK", variant="primary", disabled=True), + ] + + def style_content(self, dialog): + dialog.content.styles.margin = 1 + dialog.content.styles.height = self.interface.window.size[1] - 18 + + dialog.container.styles.width = "80%" + dialog.container.styles.height = self.interface.window.size[1] - 10 + + def on_select_file(self, path): + ok_button = self.native.buttons[-1] + if self.filter_func: + ok_button.disabled = not self.filter_func(path) + else: + ok_button.disabled = not path.is_file() - self.on_result(self, None) - self.interface.future.set_result(None) + def return_value(self, variant): + if variant == "primary": + return self.native.directory_tree.cursor_node.data.path + else: + return None class SelectFolderDialog(BaseDialog): @@ -197,10 +348,47 @@ def __init__( multiselect, on_result=None, ): - super().__init__(interface=interface) - self.on_result = on_result + super().__init__( + interface=interface, + title=title, + message=None, + on_result=on_result, + ) + self.initial_directory = initial_directory if initial_directory else Path.cwd() + self.filter_func = lambda path: path.is_dir() + self.multiselect = multiselect + + def compose_content(self, dialog): + dialog.directory_tree = FilteredDirectoryTree(self) + dialog.parent_button = ParentFolderButton(self) + dialog.scroll = VerticalScroll(dialog.directory_tree) + dialog.content = Vertical( + dialog.parent_button, + dialog.scroll, + ) + + def create_buttons(self): + return [ + Button("Cancel", variant="error"), + Button("OK", variant="primary"), + ] - interface.window.factory.not_implemented("Window.select_folder_dialog()") + def style_content(self, dialog): + dialog.content.styles.margin = 1 + dialog.content.styles.height = self.interface.window.size[1] - 19 - self.on_result(self, None) - self.interface.future.set_result(None) + dialog.container.styles.width = "80%" + dialog.container.styles.height = self.interface.window.size[1] - 10 + + def on_select_file(self, path): + ok_button = self.native.buttons[-1] + if self.filter_func: + ok_button.disabled = not self.filter_func(path) + else: + ok_button.disabled = not path.is_file() + + def return_value(self, variant): + if variant == "primary": + return self.native.directory_tree.cursor_node.data.path + else: + return None diff --git a/textual/src/toga_textual/widgets/label.py b/textual/src/toga_textual/widgets/label.py index 13759ff5e4..424d12e071 100644 --- a/textual/src/toga_textual/widgets/label.py +++ b/textual/src/toga_textual/widgets/label.py @@ -13,7 +13,7 @@ def get_text(self): return str(self.native.renderable) def set_text(self, value): - self.native.renderable = value + self.native.update(value) def rehint(self): self.interface.intrinsic.width = at_least(len(self.native.renderable)) diff --git a/textual/src/toga_textual/window.py b/textual/src/toga_textual/window.py index 65ddcc38ae..bf5a29bf52 100644 --- a/textual/src/toga_textual/window.py +++ b/textual/src/toga_textual/window.py @@ -1,17 +1,96 @@ +from rich.text import Text + +from textual.app import RenderResult +from textual.reactive import Reactive from textual.screen import Screen as TextualScreen -from textual.widgets import Header as TextualHeader +from textual.widget import Widget as TextualWidget from .container import Container +class CloseIcon(TextualWidget): + DEFAULT_CSS = """ + CloseIcon { + dock: left; + padding: 0 1; + width: 4; + content-align: left middle; + } + """ + + def render(self) -> RenderResult: + return "⭘" + + +class TitleSpacer(TextualWidget): + DEFAULT_CSS = """ + TitleSpacer { + dock: right; + padding: 0 1; + width: 4; + content-align: right middle; + } + """ + + def render(self) -> RenderResult: + return "" + + +class TitleText(TextualWidget): + DEFAULT_CSS = """ + TitleText { + content-align: center middle; + width: 100%; + } + """ + text: Reactive[str] = Reactive("") + + def __init__(self, text): + super().__init__() + self.text = text + + def render(self) -> RenderResult: + return Text(self.text, no_wrap=True, overflow="ellipsis") + + +class TitleBar(TextualWidget): + DEFAULT_CSS = """ + TitleBar { + dock: top; + width: 100%; + background: $foreground 5%; + color: $text; + height: 1; + } + """ + + def __init__(self, title): + super().__init__() + self.title = TitleText(title) + + @property + def text(self): + return self.title.text + + @text.setter + def text(self, value): + self.title.text = value + + def compose(self): + yield CloseIcon() + yield self.title + yield TitleSpacer() + + class TogaWindow(TextualScreen): - def __init__(self, impl): + def __init__(self, impl, title): super().__init__() self.interface = impl.interface self.impl = impl + self.titlebar = TitleBar(title) def on_mount(self) -> None: - self.mount(TextualHeader()) + self.mount(self.titlebar) def on_resize(self, event) -> None: self.interface.content.refresh() @@ -20,9 +99,8 @@ def on_resize(self, event) -> None: class Window: def __init__(self, interface, title, position, size): self.interface = interface - self.native = TogaWindow(self) + self.native = TogaWindow(self, title) self.container = Container(self.native) - self.set_title(title) def create_toolbar(self): pass @@ -36,10 +114,10 @@ def set_content(self, widget): self.native.mount(widget.native) def get_title(self): - return self._title + return self.native.titlebar.text def set_title(self, title): - self._title = title + self.native.titlebar.text = title def get_position(self): return (0, 0) From 049552697ed87d09923247403e93d77de52a7e7f Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 25 Aug 2023 11:06:17 +0800 Subject: [PATCH 5/6] Convert titlebar widget to an actual close button. --- textual/src/toga_textual/app.py | 3 ++- textual/src/toga_textual/dialogs.py | 14 +++++++++++ textual/src/toga_textual/window.py | 39 +++++++++++++++++++++-------- 3 files changed, 45 insertions(+), 11 deletions(-) diff --git a/textual/src/toga_textual/app.py b/textual/src/toga_textual/app.py index 923db033d5..fed7a0bce2 100644 --- a/textual/src/toga_textual/app.py +++ b/textual/src/toga_textual/app.py @@ -6,7 +6,8 @@ class MainWindow(Window): - pass + def textual_close(self): + self.interface.app.on_exit(None) class TogaApp(TextualApp): diff --git a/textual/src/toga_textual/dialogs.py b/textual/src/toga_textual/dialogs.py index ac1c83614f..553379d48c 100644 --- a/textual/src/toga_textual/dialogs.py +++ b/textual/src/toga_textual/dialogs.py @@ -39,6 +39,8 @@ def on_mount(self) -> None: for button in self.buttons: button.styles.margin = (0, 1, 0, 1) + self.buttons[-1].focus() + def on_button_pressed(self, event: Button.Pressed) -> None: self.dismiss(self.impl.return_value(event.button.variant)) @@ -69,6 +71,9 @@ def style_content(self, dialog): def return_value(self, variant): return variant == "primary" + def textual_close(self): + self.native.dismiss(None) + def on_close(self, result: bool): self.on_result(self, result) self.interface.future.set_result(result) @@ -172,6 +177,14 @@ class ParentFolderButton(Button): border: none; min-width: 4; height: 1; + background: white 10%; + } + ParentFolderButton:hover { + background: white 10%; + } + ParentFolderButton:focus { + text-style: bold; + color: white; } ParentFolderButton.-active { border: none; @@ -294,6 +307,7 @@ def __init__( message=None, on_result=on_result, ) + self.initial_directory = initial_directory if initial_directory else Path.cwd() self.file_types = file_types if self.file_types: diff --git a/textual/src/toga_textual/window.py b/textual/src/toga_textual/window.py index bf5a29bf52..f8531b517f 100644 --- a/textual/src/toga_textual/window.py +++ b/textual/src/toga_textual/window.py @@ -4,22 +4,38 @@ from textual.reactive import Reactive from textual.screen import Screen as TextualScreen from textual.widget import Widget as TextualWidget +from textual.widgets import Button as TextualButton from .container import Container -class CloseIcon(TextualWidget): +class WindowCloseButton(TextualButton): DEFAULT_CSS = """ - CloseIcon { + WindowCloseButton { dock: left; - padding: 0 1; - width: 4; - content-align: left middle; + border: none; + min-width: 3; + height: 1; + background: white 10%; + color: white; + } + WindowCloseButton:hover { + background: white 10%; + } + WindowCloseButton:focus { + text-style: bold; + } + WindowCloseButton.-active { + border: none; } """ - def render(self) -> RenderResult: - return "⭘" + def __init__(self): + super().__init__("✕") + + def on_button_pressed(self, event): + self.screen.impl.textual_close() + event.stop() class TitleSpacer(TextualWidget): @@ -27,7 +43,7 @@ class TitleSpacer(TextualWidget): TitleSpacer { dock: right; padding: 0 1; - width: 4; + width: 3; content-align: right middle; } """ @@ -77,7 +93,7 @@ def text(self, value): self.title.text = value def compose(self): - yield CloseIcon() + yield WindowCloseButton() yield self.title yield TitleSpacer() @@ -143,8 +159,11 @@ def hide(self): def get_visible(self): return True + def textual_close(self): + self.interface.on_close(self) + def close(self): - pass + self.native.dismiss(None) def set_full_screen(self, is_full_screen): pass From 1dcacae78cca0fd45be3d07d24c8d5465af01cdd Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Fri, 25 Aug 2023 08:59:09 +0100 Subject: [PATCH 6/6] Improve close button hover effect --- textual/src/toga_textual/window.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/textual/src/toga_textual/window.py b/textual/src/toga_textual/window.py index f8531b517f..2ce56e954f 100644 --- a/textual/src/toga_textual/window.py +++ b/textual/src/toga_textual/window.py @@ -20,7 +20,8 @@ class WindowCloseButton(TextualButton): color: white; } WindowCloseButton:hover { - background: white 10%; + background: black; + border: none; } WindowCloseButton:focus { text-style: bold;