mirror of
https://github.com/1j01/textual-paint.git
synced 2025-01-04 21:21:35 +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;
|
background: $surface;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file_dialog_window .window_content {
|
FileDialogWindow .window_content {
|
||||||
padding: 2 4;
|
padding: 2 4;
|
||||||
width: 80;
|
width: 80;
|
||||||
height: 30;
|
height: 30;
|
||||||
}
|
}
|
||||||
.file_dialog_window .window_content Input {
|
FileDialogWindow .window_content Input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-bottom: 1;
|
margin-bottom: 1;
|
||||||
}
|
}
|
||||||
.file_dialog_window .window_content DirectoryTree {
|
FileDialogWindow .window_content DirectoryTree {
|
||||||
height: 15;
|
height: 15;
|
||||||
margin-bottom: 1;
|
margin-bottom: 1;
|
||||||
}
|
}
|
||||||
.file_dialog_window .window_content Button {
|
FileDialogWindow .window_content Button {
|
||||||
width: auto;
|
width: auto;
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
@ -29,16 +29,15 @@ from textual.reactive import var, reactive
|
|||||||
from textual.strip import Strip
|
from textual.strip import Strip
|
||||||
from textual.dom import DOMNode
|
from textual.dom import DOMNode
|
||||||
from textual.widget import Widget
|
from textual.widget import Widget
|
||||||
from textual.widgets import Button, Static, Input, Tree, Header
|
from textual.widgets import Button, Static, Input, Header
|
||||||
from textual.widgets._directory_tree import DirEntry
|
|
||||||
from textual.binding import Binding
|
from textual.binding import Binding
|
||||||
from textual.color import Color
|
from textual.color import Color
|
||||||
|
|
||||||
from menus import MenuBar, Menu, MenuItem, Separator
|
from menus import MenuBar, Menu, MenuItem, Separator
|
||||||
from windows import Window, DialogWindow, CharacterSelectorDialogWindow, MessageBox, get_warning_icon
|
from windows import Window, DialogWindow, CharacterSelectorDialogWindow, MessageBox, get_warning_icon
|
||||||
|
from file_dialogs import SaveAsDialogWindow, OpenDialogWindow
|
||||||
from edit_colors import EditColorsDialogWindow
|
from edit_colors import EditColorsDialogWindow
|
||||||
from localization.i18n import get as _, load_language, remove_hotkey
|
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 wallpaper import get_config_dir, set_wallpaper
|
||||||
|
|
||||||
from __init__ import __version__
|
from __init__ import __version__
|
||||||
@ -1494,11 +1493,6 @@ class PaintApp(App[None]):
|
|||||||
file_path = var(None)
|
file_path = var(None)
|
||||||
"""The path to the file being edited."""
|
"""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"))
|
image = var(AnsiArtDocument.from_text("Not Loaded"))
|
||||||
"""The document being edited. Contains the selection, if any."""
|
"""The document being edited. Contains the selection, if any."""
|
||||||
image_initialized = False
|
image_initialized = False
|
||||||
@ -1892,22 +1886,11 @@ class PaintApp(App[None]):
|
|||||||
# which is more important than here, since the dialog isn't (currently) modal.
|
# 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.
|
# You could make a selection while the dialog is open, for example.
|
||||||
self.stop_action_in_progress()
|
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()
|
saved_future: asyncio.Future[None] = asyncio.Future()
|
||||||
|
|
||||||
def handle_button(button: Button) -> None:
|
def handle_selected_file_path(file_path: str) -> 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 on_save_confirmed():
|
def on_save_confirmed():
|
||||||
async def async_on_save_confirmed():
|
async def async_on_save_confirmed():
|
||||||
self.file_path = file_path
|
self.file_path = file_path
|
||||||
@ -1923,38 +1906,14 @@ class PaintApp(App[None]):
|
|||||||
else:
|
else:
|
||||||
on_save_confirmed()
|
on_save_confirmed()
|
||||||
|
|
||||||
window = DialogWindow(
|
window = SaveAsDialogWindow(
|
||||||
id="save_as_dialog",
|
|
||||||
classes="file_dialog_window",
|
|
||||||
title=_("Save As"),
|
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")
|
await self.mount(window)
|
||||||
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 saved_future
|
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:
|
def confirm_overwrite(self, file_path: str, callback: Callable[[], None]) -> None:
|
||||||
"""Asks the user if they want to overwrite a file."""
|
"""Asks the user if they want to overwrite a file."""
|
||||||
message = _("%1 already exists.\nDo you want to replace it?", file_path)
|
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:
|
def action_open(self) -> None:
|
||||||
"""Show dialog to open an image from a file."""
|
"""Show dialog to open an image from a file."""
|
||||||
|
|
||||||
def handle_button(button: Button) -> None:
|
def handle_selected_file_path(file_path: str) -> 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
|
|
||||||
self.open_from_file_path(file_path, window.close)
|
self.open_from_file_path(file_path, window.close)
|
||||||
|
|
||||||
self.close_windows("#save_as_dialog, #open_dialog")
|
self.close_windows("SaveAsDialogWindow, OpenDialogWindow")
|
||||||
window = DialogWindow(
|
window = OpenDialogWindow(
|
||||||
id="open_dialog",
|
|
||||||
classes="file_dialog_window",
|
|
||||||
title=_("Open"),
|
title=_("Open"),
|
||||||
handle_button=handle_button,
|
handle_selected_file_path=handle_selected_file_path,
|
||||||
)
|
selected_file_path=self.file_path or "",
|
||||||
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",
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
self.mount(window)
|
self.mount(window)
|
||||||
self.expand_directory_tree(window.content.query_one("#open_dialog_directory_tree", EnhancedDirectoryTree))
|
|
||||||
|
|
||||||
def action_new(self, *, force: bool = False) -> None:
|
def action_new(self, *, force: bool = False) -> None:
|
||||||
"""Create a new image."""
|
"""Create a new image."""
|
||||||
@ -3349,25 +3286,6 @@ class PaintApp(App[None]):
|
|||||||
else:
|
else:
|
||||||
self.selected_bg_color = event.color
|
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:
|
def on_menu_status_info(self, event: Menu.StatusInfo) -> None:
|
||||||
"""Called when a menu item is hovered."""
|
"""Called when a menu item is hovered."""
|
||||||
text: str = event.description or ""
|
text: str = event.description or ""
|
||||||
|
Loading…
Reference in New Issue
Block a user