mirror of
https://github.com/1j01/textual-paint.git
synced 2025-01-02 11:52:30 +03:00
Add docstrings to all functions
This commit is contained in:
parent
a5518a5894
commit
3849f57ed6
37
paint.py
37
paint.py
@ -355,6 +355,7 @@ class CharInput(Input, inherit_bindings=False):
|
|||||||
self.value = text[-1] if text else " "
|
self.value = text[-1] if text else " "
|
||||||
|
|
||||||
def render_line(self, y: int) -> Strip:
|
def render_line(self, y: int) -> Strip:
|
||||||
|
"""Overrides rendering to color the character, since Input doesn't seem to support the color style."""
|
||||||
assert isinstance(self.app, PaintApp)
|
assert isinstance(self.app, PaintApp)
|
||||||
# return Strip([Segment(self.value * self.size.width, Style(color=self.app.selected_fg_color, bgcolor=self.app.selected_bg_color))])
|
# return Strip([Segment(self.value * self.size.width, Style(color=self.app.selected_fg_color, bgcolor=self.app.selected_bg_color))])
|
||||||
# There's a LineFilter class that can be subclassed to do stuff like this, but I'm not sure why you'd want a class for it.
|
# There's a LineFilter class that can be subclassed to do stuff like this, but I'm not sure why you'd want a class for it.
|
||||||
@ -484,6 +485,7 @@ class AnsiArtDocument:
|
|||||||
self.selection: Optional[Selection] = None
|
self.selection: Optional[Selection] = None
|
||||||
|
|
||||||
def copy_region(self, source: 'AnsiArtDocument', source_region: Region|None = None, target_region: Region|None = None, mask: list[list[bool]]|None = None) -> None:
|
def copy_region(self, source: 'AnsiArtDocument', source_region: Region|None = None, target_region: Region|None = None, mask: list[list[bool]]|None = None) -> None:
|
||||||
|
"""Copy a region from another document into this document."""
|
||||||
if source_region is None:
|
if source_region is None:
|
||||||
source_region = Region(0, 0, source.width, source.height)
|
source_region = Region(0, 0, source.width, source.height)
|
||||||
if target_region is None:
|
if target_region is None:
|
||||||
@ -727,6 +729,7 @@ def is_inside_polygon(x: int, y: int, points: List[Offset]) -> bool:
|
|||||||
|
|
||||||
# adapted from https://github.com/Pomax/bezierjs
|
# adapted from https://github.com/Pomax/bezierjs
|
||||||
def compute_bezier(t: float, start_x: float, start_y: float, control_1_x: float, control_1_y: float, control_2_x: float, control_2_y: float, end_x: float, end_y: float):
|
def compute_bezier(t: float, start_x: float, start_y: float, control_1_x: float, control_1_y: float, control_2_x: float, control_2_y: float, end_x: float, end_y: float):
|
||||||
|
"""Returns a point along a bezier curve."""
|
||||||
mt = 1 - t
|
mt = 1 - t
|
||||||
mt2 = mt * mt
|
mt2 = mt * mt
|
||||||
t2 = t * t
|
t2 = t * t
|
||||||
@ -947,6 +950,8 @@ class Canvas(Widget):
|
|||||||
self.which_button: Optional[int] = None
|
self.which_button: Optional[int] = None
|
||||||
|
|
||||||
def on_mouse_down(self, event: events.MouseDown) -> None:
|
def on_mouse_down(self, event: events.MouseDown) -> None:
|
||||||
|
"""Called when a mouse button is pressed.
|
||||||
|
Start drawing, or if both mouse buttons are pressed, cancel the current action."""
|
||||||
self.fix_mouse_event(event) # not needed, pointer isn't captured yet.
|
self.fix_mouse_event(event) # not needed, pointer isn't captured yet.
|
||||||
event.x //= self.magnification
|
event.x //= self.magnification
|
||||||
event.y //= self.magnification
|
event.y //= self.magnification
|
||||||
@ -962,6 +967,7 @@ class Canvas(Widget):
|
|||||||
self.capture_mouse(True)
|
self.capture_mouse(True)
|
||||||
|
|
||||||
def fix_mouse_event(self, event: events.MouseEvent) -> None:
|
def fix_mouse_event(self, event: events.MouseEvent) -> None:
|
||||||
|
"""Work around inconsistent widget-relative mouse coordinates by calculating from screen coordinates."""
|
||||||
# Hack to fix mouse coordinates, not needed for mouse down,
|
# Hack to fix mouse coordinates, not needed for mouse down,
|
||||||
# or while the mouse is up.
|
# or while the mouse is up.
|
||||||
# This seems like a bug.
|
# This seems like a bug.
|
||||||
@ -986,6 +992,7 @@ class Canvas(Widget):
|
|||||||
|
|
||||||
|
|
||||||
def on_mouse_move(self, event: events.MouseMove) -> None:
|
def on_mouse_move(self, event: events.MouseMove) -> None:
|
||||||
|
"""Called when the mouse is moved. Update the tool action or preview."""
|
||||||
self.fix_mouse_event(event)
|
self.fix_mouse_event(event)
|
||||||
event.x //= self.magnification
|
event.x //= self.magnification
|
||||||
event.y //= self.magnification
|
event.y //= self.magnification
|
||||||
@ -996,6 +1003,7 @@ class Canvas(Widget):
|
|||||||
self.post_message(self.ToolPreviewUpdate(event))
|
self.post_message(self.ToolPreviewUpdate(event))
|
||||||
|
|
||||||
def on_mouse_up(self, event: events.MouseUp) -> None:
|
def on_mouse_up(self, event: events.MouseUp) -> None:
|
||||||
|
"""Called when a mouse button is released. Stop the current tool."""
|
||||||
self.fix_mouse_event(event)
|
self.fix_mouse_event(event)
|
||||||
event.x //= self.magnification
|
event.x //= self.magnification
|
||||||
event.y //= self.magnification
|
event.y //= self.magnification
|
||||||
@ -1005,13 +1013,16 @@ class Canvas(Widget):
|
|||||||
self.post_message(self.ToolStop(event))
|
self.post_message(self.ToolStop(event))
|
||||||
|
|
||||||
def on_leave(self, event: events.Leave) -> None:
|
def on_leave(self, event: events.Leave) -> None:
|
||||||
|
"""Called when the mouse leaves the canvas. Stop preview if applicable."""
|
||||||
if not self.pointer_active:
|
if not self.pointer_active:
|
||||||
self.post_message(self.ToolPreviewStop())
|
self.post_message(self.ToolPreviewStop())
|
||||||
|
|
||||||
def get_content_width(self, container: Size, viewport: Size) -> int:
|
def get_content_width(self, container: Size, viewport: Size) -> int:
|
||||||
|
"""Defines the intrinsic width of the widget."""
|
||||||
return self.image.width * self.magnification
|
return self.image.width * self.magnification
|
||||||
|
|
||||||
def get_content_height(self, container: Size, viewport: Size, width: int) -> int:
|
def get_content_height(self, container: Size, viewport: Size, width: int) -> int:
|
||||||
|
"""Defines the intrinsic height of the widget."""
|
||||||
return self.image.height * self.magnification
|
return self.image.height * self.magnification
|
||||||
|
|
||||||
def render_line(self, y: int) -> Strip:
|
def render_line(self, y: int) -> Strip:
|
||||||
@ -1295,6 +1306,7 @@ class PaintApp(App[None]):
|
|||||||
# This will matter more when large documents don't freeze up the program...
|
# This will matter more when large documents don't freeze up the program...
|
||||||
|
|
||||||
def stamp_brush(self, x: int, y: int, affected_region_base: Optional[Region] = None) -> Region:
|
def stamp_brush(self, x: int, y: int, affected_region_base: Optional[Region] = None) -> Region:
|
||||||
|
"""Draws the current brush at the given coordinates, with special handling for different tools."""
|
||||||
brush_diameter = 1
|
brush_diameter = 1
|
||||||
square = self.selected_tool == Tool.eraser
|
square = self.selected_tool == Tool.eraser
|
||||||
if self.selected_tool == Tool.brush or self.selected_tool == Tool.airbrush or self.selected_tool == Tool.eraser:
|
if self.selected_tool == Tool.brush or self.selected_tool == Tool.airbrush or self.selected_tool == Tool.eraser:
|
||||||
@ -1316,6 +1328,7 @@ class PaintApp(App[None]):
|
|||||||
return affected_region
|
return affected_region
|
||||||
|
|
||||||
def stamp_char(self, x: int, y: int) -> None:
|
def stamp_char(self, x: int, y: int) -> None:
|
||||||
|
"""Modifies the cell at the given coordinates, with special handling for different tools."""
|
||||||
if x >= self.image.width or y >= self.image.height or x < 0 or y < 0:
|
if x >= self.image.width or y >= self.image.height or x < 0 or y < 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -1372,6 +1385,7 @@ class PaintApp(App[None]):
|
|||||||
self.image.fg[y][x] = fg_color
|
self.image.fg[y][x] = fg_color
|
||||||
|
|
||||||
def erase_region(self, region: Region, mask: Optional[list[list[bool]]] = None) -> None:
|
def erase_region(self, region: Region, mask: Optional[list[list[bool]]] = None) -> None:
|
||||||
|
"""Clears the given region."""
|
||||||
# Time to go undercover as an eraser. 🥸
|
# Time to go undercover as an eraser. 🥸
|
||||||
# TODO: just add a parameter to stamp_char.
|
# TODO: just add a parameter to stamp_char.
|
||||||
# Momentarily masquerading makes me mildly mad.
|
# Momentarily masquerading makes me mildly mad.
|
||||||
@ -1384,6 +1398,7 @@ class PaintApp(App[None]):
|
|||||||
self.selected_tool = original_tool
|
self.selected_tool = original_tool
|
||||||
|
|
||||||
def draw_current_free_form_select_polyline(self) -> Region:
|
def draw_current_free_form_select_polyline(self) -> Region:
|
||||||
|
"""Inverts the colors along a polyline defined by tool_points, for Free-Form Select tool preview."""
|
||||||
# TODO: DRY with draw_current_curve/draw_current_polygon/draw_current_polyline
|
# TODO: DRY with draw_current_curve/draw_current_polygon/draw_current_polyline
|
||||||
# Also (although this may be counter to DRYING (Deduplicating Repetitive Yet Individually Nimble Generators)),
|
# Also (although this may be counter to DRYING (Deduplicating Repetitive Yet Individually Nimble Generators)),
|
||||||
# could optimize to not use stamp_brush, since it's always a single character here.
|
# could optimize to not use stamp_brush, since it's always a single character here.
|
||||||
@ -1397,6 +1412,7 @@ class PaintApp(App[None]):
|
|||||||
return affected_region
|
return affected_region
|
||||||
|
|
||||||
def draw_current_polyline(self) -> Region:
|
def draw_current_polyline(self) -> Region:
|
||||||
|
"""Draws a polyline from tool_points, for Polygon tool preview."""
|
||||||
# TODO: DRY with draw_current_curve/draw_current_polygon
|
# TODO: DRY with draw_current_curve/draw_current_polygon
|
||||||
gen = polyline_walk(self.tool_points)
|
gen = polyline_walk(self.tool_points)
|
||||||
affected_region = Region()
|
affected_region = Region()
|
||||||
@ -1405,6 +1421,7 @@ class PaintApp(App[None]):
|
|||||||
return affected_region
|
return affected_region
|
||||||
|
|
||||||
def draw_current_polygon(self) -> Region:
|
def draw_current_polygon(self) -> Region:
|
||||||
|
"""Draws a polygon from tool_points, for Polygon tool."""
|
||||||
# TODO: DRY with draw_current_curve/draw_current_polyline
|
# TODO: DRY with draw_current_curve/draw_current_polyline
|
||||||
gen = polygon_walk(self.tool_points)
|
gen = polygon_walk(self.tool_points)
|
||||||
affected_region = Region()
|
affected_region = Region()
|
||||||
@ -1413,6 +1430,7 @@ class PaintApp(App[None]):
|
|||||||
return affected_region
|
return affected_region
|
||||||
|
|
||||||
def draw_current_curve(self) -> Region:
|
def draw_current_curve(self) -> Region:
|
||||||
|
"""Draws a curve (or line) from tool_points, for Curve tool."""
|
||||||
points = self.tool_points
|
points = self.tool_points
|
||||||
if len(points) == 4:
|
if len(points) == 4:
|
||||||
gen = bezier_curve_walk(
|
gen = bezier_curve_walk(
|
||||||
@ -1472,6 +1490,7 @@ class PaintApp(App[None]):
|
|||||||
self.tool_points = []
|
self.tool_points = []
|
||||||
|
|
||||||
def action_cancel(self) -> None:
|
def action_cancel(self) -> None:
|
||||||
|
"""Action to end the current tool activity, via Escape key."""
|
||||||
self.stop_action_in_progress()
|
self.stop_action_in_progress()
|
||||||
|
|
||||||
def stop_action_in_progress(self) -> None:
|
def stop_action_in_progress(self) -> None:
|
||||||
@ -1486,6 +1505,7 @@ class PaintApp(App[None]):
|
|||||||
self.selected_tool = self.return_to_tool
|
self.selected_tool = self.return_to_tool
|
||||||
|
|
||||||
def action_undo(self) -> None:
|
def action_undo(self) -> None:
|
||||||
|
"""Undoes the last action."""
|
||||||
self.stop_action_in_progress()
|
self.stop_action_in_progress()
|
||||||
if len(self.undos) > 0:
|
if len(self.undos) > 0:
|
||||||
action = self.undos.pop()
|
action = self.undos.pop()
|
||||||
@ -1495,6 +1515,7 @@ class PaintApp(App[None]):
|
|||||||
self.canvas.refresh(layout=True)
|
self.canvas.refresh(layout=True)
|
||||||
|
|
||||||
def action_redo(self) -> None:
|
def action_redo(self) -> None:
|
||||||
|
"""Redoes the last undone action."""
|
||||||
self.stop_action_in_progress()
|
self.stop_action_in_progress()
|
||||||
if len(self.redos) > 0:
|
if len(self.redos) > 0:
|
||||||
action = self.redos.pop()
|
action = self.redos.pop()
|
||||||
@ -1604,6 +1625,7 @@ class PaintApp(App[None]):
|
|||||||
self.set_timer(0.1, done_expanding)
|
self.set_timer(0.1, done_expanding)
|
||||||
|
|
||||||
def confirm_overwrite(self, filename: str, callback: Callable[[], None]) -> None:
|
def confirm_overwrite(self, filename: 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?", filename)
|
message = _("%1 already exists.\nDo you want to replace it?", filename)
|
||||||
def handle_button(button: Button) -> None:
|
def handle_button(button: Button) -> None:
|
||||||
if not button.has_class("yes"):
|
if not button.has_class("yes"):
|
||||||
@ -1612,6 +1634,7 @@ class PaintApp(App[None]):
|
|||||||
self.warning_message_box(_("Save As"), Static(message, markup=False), "yes/no", handle_button)
|
self.warning_message_box(_("Save As"), Static(message, markup=False), "yes/no", handle_button)
|
||||||
|
|
||||||
def prompt_save_changes(self, filename: str, callback: Callable[[], None]) -> None:
|
def prompt_save_changes(self, filename: str, callback: Callable[[], None]) -> None:
|
||||||
|
"""Asks the user if they want to save changes to a file."""
|
||||||
filename = os.path.basename(filename)
|
filename = os.path.basename(filename)
|
||||||
message = _("Save changes to %1?", filename)
|
message = _("Save changes to %1?", filename)
|
||||||
def handle_button(button: Button) -> None:
|
def handle_button(button: Button) -> None:
|
||||||
@ -1629,15 +1652,18 @@ class PaintApp(App[None]):
|
|||||||
self.warning_message_box(_("Paint"), Static(message, markup=False), "yes/no/cancel", handle_button)
|
self.warning_message_box(_("Paint"), Static(message, markup=False), "yes/no/cancel", handle_button)
|
||||||
|
|
||||||
def is_document_modified(self) -> bool:
|
def is_document_modified(self) -> bool:
|
||||||
|
"""Returns whether the document has been modified since the last save."""
|
||||||
return len(self.undos) != self.saved_undo_count
|
return len(self.undos) != self.saved_undo_count
|
||||||
|
|
||||||
def action_exit(self) -> None:
|
def action_exit(self) -> None:
|
||||||
|
"""Exit the program, prompting to save changes if necessary."""
|
||||||
if self.is_document_modified():
|
if self.is_document_modified():
|
||||||
self.prompt_save_changes(self.filename or _("Untitled"), self.exit)
|
self.prompt_save_changes(self.filename or _("Untitled"), self.exit)
|
||||||
else:
|
else:
|
||||||
self.exit()
|
self.exit()
|
||||||
|
|
||||||
def action_reload(self) -> None:
|
def action_reload(self) -> None:
|
||||||
|
"""Reload the program, prompting to save changes if necessary."""
|
||||||
if self.is_document_modified():
|
if self.is_document_modified():
|
||||||
self.prompt_save_changes(self.filename or _("Untitled"), restart_program)
|
self.prompt_save_changes(self.filename or _("Untitled"), restart_program)
|
||||||
else:
|
else:
|
||||||
@ -1817,6 +1843,7 @@ class PaintApp(App[None]):
|
|||||||
def action_paste(self) -> None:
|
def action_paste(self) -> None:
|
||||||
self.warning_message_box(_("Paint"), "Not implemented.", "ok")
|
self.warning_message_box(_("Paint"), "Not implemented.", "ok")
|
||||||
def action_select_all(self) -> None:
|
def action_select_all(self) -> None:
|
||||||
|
"""Select the entire image."""
|
||||||
self.stop_action_in_progress()
|
self.stop_action_in_progress()
|
||||||
self.image.selection = Selection(Region(0, 0, self.image.width, self.image.height))
|
self.image.selection = Selection(Region(0, 0, self.image.width, self.image.height))
|
||||||
self.canvas.refresh()
|
self.canvas.refresh()
|
||||||
@ -1828,10 +1855,13 @@ class PaintApp(App[None]):
|
|||||||
def action_text_toolbar(self) -> None:
|
def action_text_toolbar(self) -> None:
|
||||||
self.warning_message_box(_("Paint"), "Not implemented.", "ok")
|
self.warning_message_box(_("Paint"), "Not implemented.", "ok")
|
||||||
def action_normal_size(self) -> None:
|
def action_normal_size(self) -> None:
|
||||||
|
"""Zoom to 1x."""
|
||||||
self.magnification = 1
|
self.magnification = 1
|
||||||
def action_large_size(self) -> None:
|
def action_large_size(self) -> None:
|
||||||
|
"""Zoom to 4x."""
|
||||||
self.magnification = 4
|
self.magnification = 4
|
||||||
def action_custom_zoom(self) -> None:
|
def action_custom_zoom(self) -> None:
|
||||||
|
"""Show dialog to set zoom level."""
|
||||||
self.close_windows("#zoom_dialog")
|
self.close_windows("#zoom_dialog")
|
||||||
def handle_button(button: Button) -> None:
|
def handle_button(button: Button) -> None:
|
||||||
if button.has_class("ok"):
|
if button.has_class("ok"):
|
||||||
@ -1891,6 +1921,7 @@ class PaintApp(App[None]):
|
|||||||
self.warning_message_box(_("Paint"), "Not implemented.", "ok")
|
self.warning_message_box(_("Paint"), "Not implemented.", "ok")
|
||||||
|
|
||||||
def action_help_topics(self) -> None:
|
def action_help_topics(self) -> None:
|
||||||
|
"""Show the Help Topics dialog."""
|
||||||
self.close_windows("#help_dialog")
|
self.close_windows("#help_dialog")
|
||||||
window = DialogWindow(
|
window = DialogWindow(
|
||||||
id="help_dialog",
|
id="help_dialog",
|
||||||
@ -2191,6 +2222,7 @@ class PaintApp(App[None]):
|
|||||||
self.canvas.refresh_scaled_region(region)
|
self.canvas.refresh_scaled_region(region)
|
||||||
|
|
||||||
def make_preview(self, draw_proc: Callable[[], Region], show_dimensions_in_status_bar: bool = False) -> None:
|
def make_preview(self, draw_proc: Callable[[], Region], show_dimensions_in_status_bar: bool = False) -> None:
|
||||||
|
"""Preview the result of a draw operation, using a temporary action. Optionally preview dimensions in status bar."""
|
||||||
self.cancel_preview()
|
self.cancel_preview()
|
||||||
image_before = AnsiArtDocument(self.image.width, self.image.height)
|
image_before = AnsiArtDocument(self.image.width, self.image.height)
|
||||||
image_before.copy_region(self.image)
|
image_before.copy_region(self.image)
|
||||||
@ -2268,6 +2300,7 @@ class PaintApp(App[None]):
|
|||||||
self.get_widget_by_id("status_coords", Static).update("")
|
self.get_widget_by_id("status_coords", Static).update("")
|
||||||
|
|
||||||
def get_select_region(self, start: Offset, end: Offset) -> Region:
|
def get_select_region(self, start: Offset, end: Offset) -> Region:
|
||||||
|
"""Returns the minimum region that contains the cells at the start and end offsets."""
|
||||||
# Region.from_corners requires the first point to be the top left,
|
# Region.from_corners requires the first point to be the top left,
|
||||||
# and it doesn't ensure the width and height are non-zero, so it doesn't work here.
|
# and it doesn't ensure the width and height are non-zero, so it doesn't work here.
|
||||||
# We want to treat the inputs as cells, not points,
|
# We want to treat the inputs as cells, not points,
|
||||||
@ -2281,6 +2314,7 @@ class PaintApp(App[None]):
|
|||||||
return region.intersection(Region(0, 0, self.image.width, self.image.height))
|
return region.intersection(Region(0, 0, self.image.width, self.image.height))
|
||||||
|
|
||||||
def meld_or_clear_selection(self, meld: bool) -> None:
|
def meld_or_clear_selection(self, meld: bool) -> None:
|
||||||
|
"""Merges the selection into the image, or deletes it if meld is False."""
|
||||||
if not self.image.selection:
|
if not self.image.selection:
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -2678,12 +2712,15 @@ class PaintApp(App[None]):
|
|||||||
|
|
||||||
|
|
||||||
def action_toggle_tools_box(self) -> None:
|
def action_toggle_tools_box(self) -> None:
|
||||||
|
"""Toggles the visibility of the tools box."""
|
||||||
self.show_tools_box = not self.show_tools_box
|
self.show_tools_box = not self.show_tools_box
|
||||||
|
|
||||||
def action_toggle_colors_box(self) -> None:
|
def action_toggle_colors_box(self) -> None:
|
||||||
|
"""Toggles the visibility of the colors box."""
|
||||||
self.show_colors_box = not self.show_colors_box
|
self.show_colors_box = not self.show_colors_box
|
||||||
|
|
||||||
def action_toggle_status_bar(self) -> None:
|
def action_toggle_status_bar(self) -> None:
|
||||||
|
"""Toggles the visibility of the status bar."""
|
||||||
self.show_status_bar = not self.show_status_bar
|
self.show_status_bar = not self.show_status_bar
|
||||||
|
|
||||||
def on_tools_box_tool_selected(self, event: ToolsBox.ToolSelected) -> None:
|
def on_tools_box_tool_selected(self, event: ToolsBox.ToolSelected) -> None:
|
||||||
|
Loading…
Reference in New Issue
Block a user