From bf7c4df2d1e616a49e1ed43361ae8d1f185480cd Mon Sep 17 00:00:00 2001 From: Isaiah Odhner Date: Thu, 4 May 2023 19:01:16 -0400 Subject: [PATCH] Prompt to enlarge the document when image on clipboard is larger --- question_icon.ans | 5 +++ src/textual_paint/paint.py | 68 ++++++++++++++++++++++++------------ src/textual_paint/windows.py | 19 ++++++++++ 3 files changed, 69 insertions(+), 23 deletions(-) create mode 100644 question_icon.ans diff --git a/question_icon.ans b/question_icon.ans new file mode 100644 index 0000000..013dbec --- /dev/null +++ b/question_icon.ans @@ -0,0 +1,5 @@ + ______   +▄      ▄  +   𝟔❩   ▌ +▀▂  • ▁▀▘ +  🮂🮂◥▛🮂   diff --git a/src/textual_paint/paint.py b/src/textual_paint/paint.py index 11f8b79..b721c40 100755 --- a/src/textual_paint/paint.py +++ b/src/textual_paint/paint.py @@ -34,7 +34,7 @@ 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 windows import Window, DialogWindow, CharacterSelectorDialogWindow, MessageBox, get_warning_icon, get_question_icon from file_dialogs import SaveAsDialogWindow, OpenDialogWindow from edit_colors import EditColorsDialogWindow from localization.i18n import get as _, load_language, remove_hotkey @@ -2022,11 +2022,14 @@ class PaintApp(App[None]): else: restart_program() + # TODO: rename since it supports an icon parameter (it's not just for warning messages) + # It's just a wrapper around MessageBox. def warning_message_box(self, title: str, message_widget: Widget|str, button_types: str = "ok", callback: Callable[[Button], None]|None = None, + icon_widget: Widget|None = get_warning_icon(), ) -> None: """Show a warning message box with the given title, message, and buttons.""" self.close_windows("#message_box") @@ -2043,7 +2046,7 @@ class PaintApp(App[None]): window = MessageBox( id="message_box", title=title, - icon_widget=get_warning_icon(), + icon_widget=icon_widget, message_widget=message_widget, button_types=button_types, handle_button=handle_button, @@ -2284,16 +2287,30 @@ class PaintApp(App[None]): textbox.textbox_edited = True self.canvas.refresh_scaled_region(textbox.region) return + # paste as selection pasted_image = AnsiArtDocument.from_text(text) - self.stop_action_in_progress() - # paste at top left corner of viewport - x: int = max(0, min(self.image.width - 1, int(self.editing_area.scroll_x // self.magnification))) - y: int = max(0, min(self.image.height - 1, int(self.editing_area.scroll_y // self.magnification))) - self.image.selection = Selection(Region(x, y, pasted_image.width, pasted_image.height)) - self.image.selection.contained_image = pasted_image - self.image.selection.pasted = True # create undo state when finalizing selection - self.canvas.refresh_scaled_region(self.image.selection.region) - self.selected_tool = Tool.select + def do_the_paste(): + self.stop_action_in_progress() + # paste at top left corner of viewport + x: int = max(0, min(self.image.width - 1, int(self.editing_area.scroll_x // self.magnification))) + y: int = max(0, min(self.image.height - 1, int(self.editing_area.scroll_y // self.magnification))) + self.image.selection = Selection(Region(x, y, pasted_image.width, pasted_image.height)) + self.image.selection.contained_image = pasted_image + self.image.selection.pasted = True # create undo state when finalizing selection + self.canvas.refresh_scaled_region(self.image.selection.region) + self.selected_tool = Tool.select + if pasted_image.width > self.image.width or pasted_image.height > self.image.height: + # "bitmap" is inaccurate for ANSI art, but it's what MS Paint says, so we have translation coverage. + message = _("The image in the clipboard is larger than the bitmap.") + "\n" + _("Would you like the bitmap enlarged?") + def handle_button(button: Button) -> None: + if button.has_class("yes"): + self.resize_document(max(pasted_image.width, self.image.width), max(pasted_image.height, self.image.height)) + do_the_paste() + elif button.has_class("no"): + do_the_paste() + self.warning_message_box(_("Paint"), Static(message, markup=False), "yes/no/cancel", handle_button, icon_widget=get_question_icon()) + else: + do_the_paste() def action_select_all(self) -> None: """Select the entire image, or in a textbox, all the text.""" @@ -2373,6 +2390,22 @@ class PaintApp(App[None]): def action_invert_colors(self) -> None: self.warning_message_box(_("Paint"), "Not implemented.", "ok") + def resize_document(self, width: int, height: int) -> None: + """Resize the document, creating an undo state, and refresh the canvas.""" + self.cancel_preview() + + # TODO: DRY undo state creation (at least the undos/redos part) + action = Action(_("Attributes"), Region(0, 0, self.image.width, self.image.height)) + action.is_resize = True + action.update(self.image) + if len(self.redos) > 0: + self.redos = [] + self.undos.append(action) + + self.image.resize(width, height, default_bg=self.selected_bg_color, default_fg=self.selected_fg_color) + + self.canvas.refresh(layout=True) + def action_attributes(self) -> None: """Show dialog to set the image attributes.""" self.close_windows("#attributes_dialog") @@ -2384,18 +2417,7 @@ class PaintApp(App[None]): if width < 1 or height < 1: raise ValueError - self.cancel_preview() - - # TODO: DRY undo state creation (at least the undos/redos part) - action = Action(_("Attributes"), Region(0, 0, self.image.width, self.image.height)) - action.is_resize = True - action.update(self.image) - if len(self.redos) > 0: - self.redos = [] - self.undos.append(action) - - self.image.resize(width, height, default_bg=self.selected_bg_color, default_fg=self.selected_fg_color) - self.canvas.refresh(layout=True) + self.resize_document(width, height) window.close() except ValueError: diff --git a/src/textual_paint/windows.py b/src/textual_paint/windows.py index 1e417da..644a507 100644 --- a/src/textual_paint/windows.py +++ b/src/textual_paint/windows.py @@ -481,6 +481,25 @@ get_warning_icon = lambda: Static(""" [#000000]🮃🮃🮃🮃🮃🮃🮃🮃🮃[/] """, classes="warning_icon message_box_icon") +# question_icon_ansi = "" +# def get_question_icon() -> Static: +# global question_icon_ansi +# if not question_icon_ansi: +# with open("question_icon.ans", "r") as f: +# question_icon_ansi = f.read() +# return Static(question_icon_ansi, classes="question_icon message_box_icon") + +# I added a little stopgap to save as Rich console markup by using file extension "._RICH_CONSOLE_MARKUP". +# It's very messy markup because it's generated from the ANSI art. +# TODO: make background transparent +get_question_icon = lambda: Static(""" +[rgb(0,0,0) on rgb(128,128,128)] [rgb(0,0,0) on rgb(128,128,128)][/rgb(0,0,0) on rgb(128,128,128)]_[rgb(0,0,0) on rgb(128,128,128)][/rgb(0,0,0) on rgb(128,128,128)]_[rgb(0,0,0) on rgb(128,128,128)][/rgb(0,0,0) on rgb(128,128,128)]_[rgb(0,0,0) on rgb(128,128,128)][/rgb(0,0,0) on rgb(128,128,128)]_[rgb(0,0,0) on rgb(128,128,128)][/rgb(0,0,0) on rgb(128,128,128)]_[rgb(0,0,0) on rgb(128,128,128)][/rgb(0,0,0) on rgb(128,128,128)]_[rgb(0,0,0) on rgb(128,128,128)][/rgb(0,0,0) on rgb(128,128,128)] [rgb(0,0,0) on rgb(128,128,128)][/rgb(0,0,0) on rgb(128,128,128)] [/rgb(0,0,0) on rgb(128,128,128)] +[rgb(255,255,255) on rgb(128,128,128)]▄[rgb(0,0,0) on rgb(255,255,255)][/rgb(255,255,255) on rgb(128,128,128)] [rgb(0,0,0) on rgb(255,255,255)][/rgb(0,0,0) on rgb(255,255,255)] [rgb(0,0,0) on rgb(255,255,255)][/rgb(0,0,0) on rgb(255,255,255)] [rgb(0,0,0) on rgb(255,255,255)][/rgb(0,0,0) on rgb(255,255,255)] [rgb(0,0,0) on rgb(255,255,255)][/rgb(0,0,0) on rgb(255,255,255)] [rgb(0,0,0) on rgb(255,255,255)][/rgb(0,0,0) on rgb(255,255,255)] [rgb(255,255,255) on rgb(128,128,128)][/rgb(0,0,0) on rgb(255,255,255)]▄[rgb(0,0,0) on rgb(128,128,128)][/rgb(255,255,255) on rgb(128,128,128)] [/rgb(0,0,0) on rgb(128,128,128)] +[rgb(0,0,0) on rgb(255,255,255)] [rgb(0,0,0) on rgb(255,255,255)][/rgb(0,0,0) on rgb(255,255,255)] [rgb(0,0,0) on rgb(255,255,255)][/rgb(0,0,0) on rgb(255,255,255)] [rgb(0,0,255) on rgb(255,255,255)][/rgb(0,0,0) on rgb(255,255,255)]𝟔[rgb(0,0,255) on rgb(255,255,255)][/rgb(0,0,255) on rgb(255,255,255)]❩[rgb(0,0,0) on rgb(255,255,255)][/rgb(0,0,255) on rgb(255,255,255)] [rgb(0,0,0) on rgb(255,255,255)][/rgb(0,0,0) on rgb(255,255,255)] [rgb(0,0,0) on rgb(255,255,255)][/rgb(0,0,0) on rgb(255,255,255)] [rgb(0,0,0) on rgb(128,128,128)][/rgb(0,0,0) on rgb(255,255,255)]▌[/rgb(0,0,0) on rgb(128,128,128)] +[rgb(255,255,255) on rgb(128,128,128)]▀[rgb(0,0,0) on rgb(255,255,255)][/rgb(255,255,255) on rgb(128,128,128)]▂[rgb(0,0,0) on rgb(255,255,255)][/rgb(0,0,0) on rgb(255,255,255)] [rgb(0,0,0) on rgb(255,255,255)][/rgb(0,0,0) on rgb(255,255,255)] [rgb(0,0,255) on rgb(255,255,255)][/rgb(0,0,0) on rgb(255,255,255)]•[rgb(0,0,0) on rgb(255,255,255)][/rgb(0,0,255) on rgb(255,255,255)] [rgb(0,0,0) on rgb(255,255,255)][/rgb(0,0,0) on rgb(255,255,255)]▁[rgb(255,255,255) on rgb(0,0,0)][/rgb(0,0,0) on rgb(255,255,255)]▀[rgb(0,0,0) on rgb(128,128,128)][/rgb(255,255,255) on rgb(0,0,0)]▘[/rgb(0,0,0) on rgb(128,128,128)] +[rgb(0,0,0) on rgb(128,128,128)] [rgb(0,0,0) on rgb(128,128,128)][/rgb(0,0,0) on rgb(128,128,128)] [rgb(0,0,0) on rgb(128,128,128)][/rgb(0,0,0) on rgb(128,128,128)]🮂[rgb(0,0,0) on rgb(128,128,128)][/rgb(0,0,0) on rgb(128,128,128)]🮂[rgb(255,255,255) on rgb(128,128,128)][/rgb(0,0,0) on rgb(128,128,128)]◥[rgb(0,0,0) on rgb(128,128,128)][/rgb(255,255,255) on rgb(128,128,128)]▛[rgb(0,0,0) on rgb(128,128,128)][/rgb(0,0,0) on rgb(128,128,128)]🮂[rgb(0,0,0) on rgb(128,128,128)][/rgb(0,0,0) on rgb(128,128,128)] [rgb(0,0,0) on rgb(128,128,128)][/rgb(0,0,0) on rgb(128,128,128)] [/rgb(0,0,0) on rgb(128,128,128)] +""", classes="question_icon message_box_icon") + class MessageBox(DialogWindow): """A simple dialog window that displays a message, a group of buttons, and an optional icon."""