mirror of
https://github.com/1j01/textual-paint.git
synced 2025-01-02 03:48:20 +03:00
Extract Open and Save As dialogs to a file_dialogs module
This commit is contained in:
parent
3ae8af3ad7
commit
c9c58f12c6
161
src/textual_paint/file_dialogs.py
Normal file
161
src/textual_paint/file_dialogs.py
Normal 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)
|
@ -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;
|
||||
}
|
||||
|
@ -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 ""
|
||||
|
Loading…
Reference in New Issue
Block a user