Extract Open and Save As dialogs to a file_dialogs module

This commit is contained in:
Isaiah Odhner 2023-05-03 21:32:34 -04:00
parent 3ae8af3ad7
commit c9c58f12c6
3 changed files with 178 additions and 99 deletions

View File

@ -0,0 +1,161 @@
import os
from typing import Any, Callable
from textual.containers import Container
from textual.widget import Widget
from textual.widgets import Button, Input, Tree
from textual.widgets._directory_tree import DirEntry
from textual.containers import Container
from localization.i18n import get as _
from windows import DialogWindow
from enhanced_directory_tree import EnhancedDirectoryTree
class FileDialogWindow(DialogWindow):
"""A dialog window that lets the user select a file."""
def __init__(
self,
*children: Widget,
selected_file_path: str | None,
handle_selected_file_path: Callable[[str], None],
**kwargs: Any,
) -> None:
"""Initialize the dialog window."""
super().__init__(handle_button=self.handle_button, *children, **kwargs)
self._selected_file_path: str | None = selected_file_path
self.handle_selected_file_path = handle_selected_file_path
self._directory_tree_selected_path: str | None = None
"""Last highlighted item in the directory tree"""
self._expanding_directory_tree: bool = False
"""Flag to prevent setting the filename input when initially expanding the directory tree"""
def on_mount(self) -> None:
"""Called when the window is mounted."""
self._expand_directory_tree()
# This MIGHT be more reliable even though it's hacky.
# I don't know what the exact preconditions are for the expansion to work.
# self.call_after_refresh(self._expand_directory_tree)
def on_tree_node_highlighted(self, event: Tree.NodeHighlighted[DirEntry]) -> None:
"""
Called when a file/folder is selected in the DirectoryTree.
This message comes from Tree.
DirectoryTree gives FileSelected, but only for files, not folders.
"""
assert event.node.data
if event.node.data.is_dir:
self._directory_tree_selected_path = event.node.data.path
elif event.node.parent:
assert event.node.parent.data
self._directory_tree_selected_path = event.node.parent.data.path
name = os.path.basename(event.node.data.path)
if not self._expanding_directory_tree:
self.query_one("FileDialogWindow .filename_input", Input).value = name
else:
self._directory_tree_selected_path = None
def _expand_directory_tree(self) -> None:
"""Expand the directory tree to the target directory, either the folder of the open file or the current working directory."""
tree = self.content.query_one(EnhancedDirectoryTree)
self._expanding_directory_tree = True
target_dir = (self._selected_file_path or os.getcwd()).rstrip(os.path.sep)
tree.expand_to_path(target_dir)
# There are currently some timers in expand_to_path.
# In particular, it waits before selecting the target node,
# and this flag is for avoiding responding to that.
def done_expanding():
self._expanding_directory_tree = False
self.set_timer(0.1, done_expanding)
class OpenDialogWindow(FileDialogWindow):
"""A dialog window that lets the user select a file to open.
`handle_selected_file_path` is called when the user clicks the Open button,
and the window is NOT closed in that case.
"""
def __init__(
self,
*children: Widget,
selected_file_path: str | None,
handle_selected_file_path: Callable[[str], None],
**kwargs: Any,
) -> None:
"""Initialize the dialog window."""
super().__init__(*children, selected_file_path=selected_file_path, handle_selected_file_path=handle_selected_file_path, **kwargs)
def on_mount(self) -> None:
"""Called when the window is mounted."""
self.content.mount(
EnhancedDirectoryTree(path="/"),
Input(classes="filename_input", placeholder=_("Filename")),
Container(
Button(_("Open"), classes="open submit", variant="primary"),
Button(_("Cancel"), classes="cancel"),
classes="buttons",
),
)
def handle_button(self, button: Button) -> None:
"""Called when a button is clicked or activated with the keyboard."""
if not button.has_class("open"):
self.close()
return
filename = self.content.query_one(".filename_input", Input).value
if not filename:
return
# TODO: allow entering an absolute or relative path, not just a filename
if self._directory_tree_selected_path:
file_path = os.path.join(self._directory_tree_selected_path, filename)
else:
file_path = filename
self.handle_selected_file_path(file_path)
class SaveAsDialogWindow(FileDialogWindow):
"""A dialog window that lets the user select a file to save to.
`handle_selected_file_path` is called when the user clicks the Save button,
and the window is NOT closed in that case.
"""
def __init__(
self,
*children: Widget,
selected_file_path: str | None,
handle_selected_file_path: Callable[[str], None],
**kwargs: Any,
) -> None:
"""Initialize the dialog window."""
super().__init__(*children, selected_file_path=selected_file_path, handle_selected_file_path=handle_selected_file_path, **kwargs)
def on_mount(self) -> None:
"""Called when the window is mounted."""
filename: str = os.path.basename(self._selected_file_path) if self._selected_file_path else ""
self.content.mount(
EnhancedDirectoryTree(path="/"),
Input(classes="filename_input", placeholder=_("Filename"), value=filename),
Container(
Button(_("Save"), classes="save submit", variant="primary"),
Button(_("Cancel"), classes="cancel"),
classes="buttons",
),
)
def handle_button(self, button: Button) -> None:
"""Called when a button is clicked or activated with the keyboard."""
if button.has_class("cancel"):
self.request_close()
elif button.has_class("save"):
if self._selected_file_path is None:
return
name = self.query_one(".filename_input", Input).value
if not name:
return
# TODO: allow entering an absolute or relative path, not just a filename
if self._directory_tree_selected_path:
file_path = os.path.join(self._directory_tree_selected_path, name)
else:
file_path = name
self.handle_selected_file_path(file_path)

View File

@ -202,20 +202,20 @@ DialogWindow .window_content {
background: $surface;
}
.file_dialog_window .window_content {
FileDialogWindow .window_content {
padding: 2 4;
width: 80;
height: 30;
}
.file_dialog_window .window_content Input {
FileDialogWindow .window_content Input {
width: 100%;
margin-bottom: 1;
}
.file_dialog_window .window_content DirectoryTree {
FileDialogWindow .window_content DirectoryTree {
height: 15;
margin-bottom: 1;
}
.file_dialog_window .window_content Button {
FileDialogWindow .window_content Button {
width: auto;
height: auto;
}

View File

@ -29,16 +29,15 @@ from textual.reactive import var, reactive
from textual.strip import Strip
from textual.dom import DOMNode
from textual.widget import Widget
from textual.widgets import Button, Static, Input, Tree, Header
from textual.widgets._directory_tree import DirEntry
from textual.widgets import Button, Static, Input, Header
from textual.binding import Binding
from textual.color import Color
from menus import MenuBar, Menu, MenuItem, Separator
from windows import Window, DialogWindow, CharacterSelectorDialogWindow, MessageBox, get_warning_icon
from file_dialogs import SaveAsDialogWindow, OpenDialogWindow
from edit_colors import EditColorsDialogWindow
from localization.i18n import get as _, load_language, remove_hotkey
from enhanced_directory_tree import EnhancedDirectoryTree
from wallpaper import get_config_dir, set_wallpaper
from __init__ import __version__
@ -1494,11 +1493,6 @@ class PaintApp(App[None]):
file_path = var(None)
"""The path to the file being edited."""
directory_tree_selected_path: str|None = None
"""Last highlighted item in Open/Save As dialogs"""
expanding_directory_tree = False
"""Flag to prevent setting the filename input when initially expanding the directory tree"""
image = var(AnsiArtDocument.from_text("Not Loaded"))
"""The document being edited. Contains the selection, if any."""
image_initialized = False
@ -1892,22 +1886,11 @@ class PaintApp(App[None]):
# which is more important than here, since the dialog isn't (currently) modal.
# You could make a selection while the dialog is open, for example.
self.stop_action_in_progress()
self.close_windows("#save_as_dialog, #open_dialog")
self.close_windows("SaveAsDialogWindow, OpenDialogWindow")
saved_future: asyncio.Future[None] = asyncio.Future()
def handle_button(button: Button) -> None:
if not button.has_class("save"):
window.close()
return
name = self.query_one("#save_as_dialog .filename_input", Input).value
if not name:
return
# TODO: allow entering an absolute or relative path, not just a filename
if self.directory_tree_selected_path:
file_path = os.path.join(self.directory_tree_selected_path, name)
else:
file_path = name
def handle_selected_file_path(file_path: str) -> None:
def on_save_confirmed():
async def async_on_save_confirmed():
self.file_path = file_path
@ -1923,38 +1906,14 @@ class PaintApp(App[None]):
else:
on_save_confirmed()
window = DialogWindow(
id="save_as_dialog",
classes="file_dialog_window",
window = SaveAsDialogWindow(
title=_("Save As"),
handle_button=handle_button,
handle_selected_file_path=handle_selected_file_path,
selected_file_path=self.file_path or _("Untitled")
)
filename: str = os.path.basename(self.file_path) if self.file_path else _("Untitled")
window.content.mount(
EnhancedDirectoryTree(id="save_as_dialog_directory_tree", path="/"),
Input(classes="filename_input", placeholder=_("Filename"), value=filename),
Container(
Button(_("Save"), classes="save submit", variant="primary"),
Button(_("Cancel"), classes="cancel"),
classes="buttons",
),
)
self.mount(window)
self.expand_directory_tree(window.content.query_one("#save_as_dialog_directory_tree", EnhancedDirectoryTree))
await self.mount(window)
await saved_future
def expand_directory_tree(self, tree: EnhancedDirectoryTree) -> None:
"""Expand the directory tree to the target directory, either the folder of the open file or the current working directory."""
self.expanding_directory_tree = True
target_dir = (self.file_path or os.getcwd()).rstrip(os.path.sep)
tree.expand_to_path(target_dir)
# There are currently some timers in expand_to_path.
# In particular, it waits before selecting the target node,
# and this flag is for avoiding responding to that.
def done_expanding():
self.expanding_directory_tree = False
self.set_timer(0.1, done_expanding)
def confirm_overwrite(self, file_path: str, callback: Callable[[], None]) -> None:
"""Asks the user if they want to overwrite a file."""
message = _("%1 already exists.\nDo you want to replace it?", file_path)
@ -2077,38 +2036,16 @@ class PaintApp(App[None]):
def action_open(self) -> None:
"""Show dialog to open an image from a file."""
def handle_button(button: Button) -> None:
if not button.has_class("open"):
window.close()
return
filename = window.content.query_one("#open_dialog .filename_input", Input).value
if not filename:
return
# TODO: allow entering an absolute or relative path, not just a filename
if self.directory_tree_selected_path:
file_path = os.path.join(self.directory_tree_selected_path, filename)
else:
file_path = filename
def handle_selected_file_path(file_path: str) -> None:
self.open_from_file_path(file_path, window.close)
self.close_windows("#save_as_dialog, #open_dialog")
window = DialogWindow(
id="open_dialog",
classes="file_dialog_window",
self.close_windows("SaveAsDialogWindow, OpenDialogWindow")
window = OpenDialogWindow(
title=_("Open"),
handle_button=handle_button,
)
window.content.mount(
EnhancedDirectoryTree(id="open_dialog_directory_tree", path="/"),
Input(classes="filename_input", placeholder=_("Filename")),
Container(
Button(_("Open"), classes="open submit", variant="primary"),
Button(_("Cancel"), classes="cancel"),
classes="buttons",
),
handle_selected_file_path=handle_selected_file_path,
selected_file_path=self.file_path or "",
)
self.mount(window)
self.expand_directory_tree(window.content.query_one("#open_dialog_directory_tree", EnhancedDirectoryTree))
def action_new(self, *, force: bool = False) -> None:
"""Create a new image."""
@ -3349,25 +3286,6 @@ class PaintApp(App[None]):
else:
self.selected_bg_color = event.color
def on_tree_node_highlighted(self, event: Tree.NodeHighlighted[DirEntry]) -> None:
"""
Called when a file/folder is selected in the DirectoryTree.
This message comes from Tree.
DirectoryTree gives FileSelected but only for files.
"""
assert event.node.data
if event.node.data.is_dir:
self.directory_tree_selected_path = event.node.data.path
elif event.node.parent:
assert event.node.parent.data
self.directory_tree_selected_path = event.node.parent.data.path
name = os.path.basename(event.node.data.path)
if not self.expanding_directory_tree:
self.query_one(".file_dialog_window .filename_input", Input).value = name
else:
self.directory_tree_selected_path = None
def on_menu_status_info(self, event: Menu.StatusInfo) -> None:
"""Called when a menu item is hovered."""
text: str = event.description or ""