mirror of
https://github.com/1j01/textual-paint.git
synced 2024-07-07 04:06:37 +03:00
Remove all trailing whitespace
using regular expression: \s+$
This commit is contained in:
parent
bd6ce9b3a7
commit
c9757a4549
|
@ -382,7 +382,7 @@ cspell-cli lint .
|
|||
|
||||
# Type checking
|
||||
# I use the "Python" and "Pylance" VS Code extensions, and the Pyright CLI:
|
||||
pyright
|
||||
pyright
|
||||
# It should give 0 errors at this version of Pyright:
|
||||
PYRIGHT_PYTHON_FORCE_VERSION=1.1.317 pyright
|
||||
# It gives 508 errors with the next version (the current latest) for some reason:
|
||||
|
|
|
@ -86,7 +86,7 @@ def write_ansi_file(file: TextIO) -> None:
|
|||
write(BOX_VERTICAL)
|
||||
write(f'\u001b[{start_y + k + 1};{start_x + box_outer_width - 1}H')
|
||||
write(BOX_VERTICAL)
|
||||
|
||||
|
||||
# Write the character in the center of the box
|
||||
# write(f'\u001b[{start_y + box_inner_height // 2 + 1};{start_x + box_inner_width // 2 - 1}H')
|
||||
# write(character)
|
||||
|
|
|
@ -54,7 +54,7 @@ def generate_ansi_art(width: int, height: int, file: TextIO) -> None:
|
|||
|
||||
# Write the colored glyph to the file
|
||||
file.write(color + GLYPHS[glyph_index])
|
||||
|
||||
|
||||
# Reset the color at the end of each row and add a newline character
|
||||
file.write(RESET + '\n')
|
||||
|
||||
|
|
|
@ -46,7 +46,7 @@ def extract_textures(image_path: str):
|
|||
|
||||
# Create a new image to store the extracted textures
|
||||
extracted_image = Image.new('RGB', (num_textures_x * texture_width, num_textures_y * texture_height))
|
||||
|
||||
|
||||
half_size_meta_glyphs: dict[int, str] = {}
|
||||
full_size_meta_glyphs: dict[int, str] = {}
|
||||
|
||||
|
@ -98,9 +98,9 @@ def extract_textures(image_path: str):
|
|||
|
||||
# Add a newline after each row
|
||||
extracted_text_half += '\n'
|
||||
|
||||
|
||||
half_size_meta_glyphs[ordinal] = extracted_text_half
|
||||
|
||||
|
||||
# Extract as full-size FIGlet font
|
||||
extracted_text_full = ""
|
||||
for y in range(texture_height):
|
||||
|
@ -109,9 +109,9 @@ def extract_textures(image_path: str):
|
|||
|
||||
# Add a newline after each row
|
||||
extracted_text_full += '\n'
|
||||
|
||||
|
||||
full_size_meta_glyphs[ordinal] = extracted_text_full
|
||||
|
||||
|
||||
for figChars in [half_size_meta_glyphs, full_size_meta_glyphs]:
|
||||
# Fill in the space characters with hard blanks
|
||||
# figChars[32] = figChars[32].replace(' ', '$')
|
||||
|
@ -123,7 +123,7 @@ def extract_textures(image_path: str):
|
|||
# although it won't look pretty having the dollar signs scattered in the font file.
|
||||
for ordinal in figChars:
|
||||
figChars[ordinal] = '\n'.join([row.rstrip() + '$' for row in figChars[ordinal].split('\n')])
|
||||
|
||||
|
||||
shared_comment_lines = [
|
||||
"by Isaiah Odhner",
|
||||
"",
|
||||
|
|
|
@ -46,7 +46,7 @@ def update_cli_help_on_readme():
|
|||
parser.formatter_class = lambda prog: argparse.HelpFormatter(prog, width=width)
|
||||
help_text = parser.format_help()
|
||||
parser.formatter_class = old_formatter_class
|
||||
|
||||
|
||||
md = f.read()
|
||||
start_match = readme_help_start.search(md)
|
||||
if start_match is None:
|
||||
|
|
|
@ -59,7 +59,7 @@ def restart_program() -> None:
|
|||
|
||||
def restart_on_changes(app: PaintApp) -> None:
|
||||
"""Restarts the current program when a file is changed"""
|
||||
|
||||
|
||||
from watchdog.events import PatternMatchingEventHandler, FileSystemEvent, EVENT_TYPE_CLOSED, EVENT_TYPE_OPENED
|
||||
from watchdog.observers import Observer
|
||||
|
||||
|
|
|
@ -50,7 +50,7 @@ class ColorGrid(Container):
|
|||
|
||||
class Changed(Message):
|
||||
"""A message that is sent when the selected color changes."""
|
||||
|
||||
|
||||
def __init__(self, color: str, color_grid: "ColorGrid", index: int) -> None:
|
||||
"""Initialize the message."""
|
||||
super().__init__()
|
||||
|
@ -68,7 +68,7 @@ class ColorGrid(Container):
|
|||
self._color_by_button: dict[Button, str] = {}
|
||||
self.color_list = color_list # This immediately calls `watch_color_list`.
|
||||
self.can_focus = True
|
||||
|
||||
|
||||
def on_mount(self) -> None:
|
||||
"""Called when the window is mounted."""
|
||||
found_match = False
|
||||
|
@ -107,7 +107,7 @@ class ColorGrid(Container):
|
|||
self._navigate_absolute(len(self.color_list) - 1)
|
||||
elif event.key in ("space", "enter"):
|
||||
self._select_focused_color()
|
||||
|
||||
|
||||
def _select_focused_color(self) -> None:
|
||||
try:
|
||||
focused = self.query_one(".focused", Button)
|
||||
|
@ -119,7 +119,7 @@ class ColorGrid(Container):
|
|||
self.selected_color = self._color_by_button[focused]
|
||||
index = list(self._color_by_button.keys()).index(focused)
|
||||
self.post_message(self.Changed(self.selected_color, self, index))
|
||||
|
||||
|
||||
def _navigate_relative(self, delta: int) -> None:
|
||||
"""Navigate to a color relative to the currently focused color."""
|
||||
try:
|
||||
|
@ -144,7 +144,7 @@ class ColorGrid(Container):
|
|||
for button in self._color_by_button:
|
||||
button.remove_class("focused")
|
||||
target_button.add_class("focused")
|
||||
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
"""Called when a button is clicked or activated with the keyboard."""
|
||||
self.selected_color = self._color_by_button[event.button]
|
||||
|
@ -207,7 +207,7 @@ class LuminosityRamp(Widget):
|
|||
self._update_color(event.y)
|
||||
self._mouse_down = True
|
||||
self.capture_mouse()
|
||||
|
||||
|
||||
def on_mouse_up(self, event: events.MouseUp) -> None:
|
||||
"""Called when the mouse is released."""
|
||||
self.release_mouse()
|
||||
|
@ -217,7 +217,7 @@ class LuminosityRamp(Widget):
|
|||
"""Called when the mouse is moved."""
|
||||
if self._mouse_down:
|
||||
self._update_color(event.y)
|
||||
|
||||
|
||||
def _update_color(self, y: int) -> None:
|
||||
"""Update the color based on the given y coordinate."""
|
||||
self.luminosity = max(0, min(1, 1 - y / (self.size.height - 1)))
|
||||
|
@ -268,7 +268,7 @@ class ColorField(Widget):
|
|||
self._update_color(event.offset)
|
||||
self._mouse_down = True
|
||||
self.capture_mouse()
|
||||
|
||||
|
||||
def on_mouse_up(self, event: events.MouseUp) -> None:
|
||||
"""Called when the mouse is released."""
|
||||
self.release_mouse()
|
||||
|
@ -278,7 +278,7 @@ class ColorField(Widget):
|
|||
"""Called when the mouse is moved."""
|
||||
if self._mouse_down:
|
||||
self._update_color(event.offset)
|
||||
|
||||
|
||||
def _update_color(self, offset: Offset) -> None:
|
||||
"""Update the color based on the given offset."""
|
||||
x, y = offset
|
||||
|
@ -310,7 +310,7 @@ class IntegerInput(Input):
|
|||
self.min = min
|
||||
self.max = max
|
||||
self.last_valid_int = 0
|
||||
|
||||
|
||||
def _track_valid_int(self, value: str) -> int:
|
||||
try:
|
||||
value_as_int = int(value)
|
||||
|
@ -348,7 +348,7 @@ class EditColorsDialogWindow(DialogWindow):
|
|||
self._inputs_by_letter: dict[str, IntegerInput] = {}
|
||||
self._custom_colors_index = 0
|
||||
self.handle_selected_color = handle_selected_color
|
||||
|
||||
|
||||
def handle_button(self, button: Button) -> None:
|
||||
"""Called when a button is clicked or activated with the keyboard."""
|
||||
if button.has_class("cancel"):
|
||||
|
|
|
@ -24,7 +24,7 @@ class EnhancedDirectoryTree(DirectoryTree):
|
|||
|
||||
node_highlighted_by_expand_to_path = var(False)
|
||||
"""Whether a NodeHighlighted event was triggered by expand_to_path.
|
||||
|
||||
|
||||
(An alternative would be to create a new message type wrapping `NodeHighlighted`,
|
||||
which includes a flag.)
|
||||
(Also, this could be a simple attribute, but I didn't want to make an `__init__`
|
||||
|
@ -55,7 +55,7 @@ class EnhancedDirectoryTree(DirectoryTree):
|
|||
# * (adds NodeHighlighted to queue)
|
||||
# * clear flag
|
||||
# * on_tree_node_highlighted
|
||||
#
|
||||
#
|
||||
# So instead, listen for NodeHighlighted,
|
||||
# and then clear the flag.
|
||||
|
||||
|
@ -69,7 +69,7 @@ class EnhancedDirectoryTree(DirectoryTree):
|
|||
|
||||
def on_tree_node_highlighted(self, event: Tree.NodeHighlighted[DirEntry]) -> None:
|
||||
"""Called when a node is highlighted in the DirectoryTree.
|
||||
|
||||
|
||||
This handler is used to clear the flag set by expand_to_path.
|
||||
See _go_to_node for more details.
|
||||
"""
|
||||
|
@ -88,7 +88,7 @@ class EnhancedDirectoryTree(DirectoryTree):
|
|||
|
||||
def _expand_matching_child(self, node: TreeNode[DirEntry], remaining_parts: tuple[str], callback: Callable[[], None]) -> None:
|
||||
"""Hooks into DirectoryTree's add method, and expands the child node matching the next path part, recursively.
|
||||
|
||||
|
||||
Once the last part of the path is reached, it scrolls to and selects the node.
|
||||
"""
|
||||
# print("_expand_matching_child", node, remaining_parts)
|
||||
|
|
|
@ -29,7 +29,7 @@ from enum import Enum
|
|||
|
||||
class FIGletFontWriter:
|
||||
"""Used to write FIGlet fonts.
|
||||
|
||||
|
||||
createFigFileData() returns a string that can be written to a .flf file.
|
||||
|
||||
It can automatically fix some common problems with FIGlet fonts, such as
|
||||
|
@ -52,7 +52,7 @@ class FIGletFontWriter:
|
|||
|
||||
charOrder: list[int] = [ii for ii in range(32, 127)] + [196, 214, 220, 228, 246, 252, 223]
|
||||
R"""Character codes that are required to be in any FIGlet font.
|
||||
|
||||
|
||||
Printable portion of the ASCII character set:
|
||||
32 (blank/space) 64 @ 96 `
|
||||
33 ! 65 A 97 a
|
||||
|
@ -144,55 +144,55 @@ Additional characters must use code tagged characters, which are not yet support
|
|||
|
||||
self.figChars: dict[int, str] = figChars
|
||||
"""Dictionary that maps character codes to FIGcharacter strings."""
|
||||
|
||||
|
||||
self.height = height
|
||||
"""Height of a FIGcharacter, in sub-characters."""
|
||||
|
||||
|
||||
self.baseline = baseline
|
||||
"""Distance from the top of the FIGcharacter to the baseline. If not specified, defaults to height."""
|
||||
|
||||
|
||||
self.maxLength = maxLength
|
||||
"""Maximum length of a line INCLUDING two endMark characters."""
|
||||
|
||||
|
||||
self.commentLines: list[str] = commentLines
|
||||
"""List of comment lines to be included in the header. It's recommended to include at least the name of the font and the name of the author."""
|
||||
|
||||
|
||||
self.rightToLeft = rightToLeft
|
||||
"""Indicates RTL writing direction (or LTR if False)."""
|
||||
|
||||
|
||||
self.codeTagCount = codeTagCount
|
||||
"""Number of extra FIGcharacters included in the font (in addition to the required 102 untagged characters). Outputting tagged characters is not yet supported."""
|
||||
|
||||
|
||||
self.hardBlank = hardBlank
|
||||
"""Character rendered as a space which can prevent smushing."""
|
||||
|
||||
|
||||
self.endMark = endMark
|
||||
"""Denotes the end of a line. Two of these characters in a row denotes the end of a FIGcharacter."""
|
||||
|
||||
|
||||
self.horizontalLayout = horizontalLayout
|
||||
"""Defines how FIGcharacters are spaced horizontally."""
|
||||
|
||||
|
||||
self.verticalLayout = verticalLayout
|
||||
"""Defines how FIGcharacters are spaced vertically."""
|
||||
|
||||
|
||||
self.hRule = [False] * 7
|
||||
"""Horizontal Smushing Rules, 1-6 (0 is not used, so that indices correspond with the names of the parameters).
|
||||
|
||||
|
||||
horizontalLayout must be Layout.CONTROLLED_SMUSHING for these to take effect."""
|
||||
|
||||
|
||||
self.vRule = [False] * 6
|
||||
"""Vertical Smushing Rules, 1-5 (0 is not used, so that indices correspond with the names of the parameters).
|
||||
|
||||
|
||||
verticalLayout must be Layout.CONTROLLED_SMUSHING for these to take effect."""
|
||||
|
||||
|
||||
self.caseInsensitive = caseInsensitive
|
||||
"""Makes lowercase same as uppercase. Note that this is one-way overwrite. It doesn't check if a character already exists, and it won't fill in uppercase using lowercase."""
|
||||
|
||||
self._validateOptions()
|
||||
|
||||
|
||||
def _validateOptions(self) -> None:
|
||||
"""Called on init and before generating a font file.
|
||||
|
||||
|
||||
See also _fixFigChars() which actively fixes things.
|
||||
"""
|
||||
# Check enums
|
||||
|
|
|
@ -14,7 +14,7 @@ from .enhanced_directory_tree import EnhancedDirectoryTree
|
|||
|
||||
class FileDialogWindow(DialogWindow):
|
||||
"""A dialog window that lets the user select a file."""
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*children: Widget,
|
||||
|
@ -99,12 +99,12 @@ class FileDialogWindow(DialogWindow):
|
|||
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.path.is_dir():
|
||||
self._directory_tree_selected_path = str(event.node.data.path)
|
||||
elif event.node.parent:
|
||||
|
@ -128,7 +128,7 @@ class FileDialogWindow(DialogWindow):
|
|||
|
||||
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.
|
||||
"""
|
||||
|
@ -152,7 +152,7 @@ class OpenDialogWindow(FileDialogWindow):
|
|||
|
||||
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.
|
||||
"""
|
||||
|
|
|
@ -19,7 +19,7 @@ with others.
|
|||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
|
|
|
@ -125,13 +125,13 @@ def midpoint_ellipse(xc: int, yc: int, rx: int, ry: int) -> Iterator[tuple[int,
|
|||
|
||||
x = 0
|
||||
y = ry
|
||||
|
||||
|
||||
# Initial decision parameter of region 1
|
||||
d1 = ((ry * ry) - (rx * rx * ry) +
|
||||
(0.25 * rx * rx))
|
||||
dx = 2 * ry * ry * x
|
||||
dy = 2 * rx * rx * y
|
||||
|
||||
|
||||
# For region 1
|
||||
while (dx < dy):
|
||||
# Yield points based on 4-way symmetry
|
||||
|
@ -139,7 +139,7 @@ def midpoint_ellipse(xc: int, yc: int, rx: int, ry: int) -> Iterator[tuple[int,
|
|||
yield -x + xc, y + yc
|
||||
yield x + xc, -y + yc
|
||||
yield -x + xc, -y + yc
|
||||
|
||||
|
||||
# Checking and updating value of
|
||||
# decision parameter based on algorithm
|
||||
if (d1 < 0):
|
||||
|
@ -152,12 +152,12 @@ def midpoint_ellipse(xc: int, yc: int, rx: int, ry: int) -> Iterator[tuple[int,
|
|||
dx = dx + (2 * ry * ry)
|
||||
dy = dy - (2 * rx * rx)
|
||||
d1 = d1 + dx - dy + (ry * ry)
|
||||
|
||||
|
||||
# Decision parameter of region 2
|
||||
d2 = (((ry * ry) * ((x + 0.5) * (x + 0.5))) +
|
||||
((rx * rx) * ((y - 1) * (y - 1))) -
|
||||
(rx * rx * ry * ry))
|
||||
|
||||
|
||||
# Plotting points of region 2
|
||||
while (y >= 0):
|
||||
# Yielding points based on 4-way symmetry
|
||||
|
@ -165,7 +165,7 @@ def midpoint_ellipse(xc: int, yc: int, rx: int, ry: int) -> Iterator[tuple[int,
|
|||
yield -x + xc, y + yc
|
||||
yield x + xc, -y + yc
|
||||
yield -x + xc, -y + yc
|
||||
|
||||
|
||||
# Checking and updating parameter
|
||||
# value based on algorithm
|
||||
if (d2 > 0):
|
||||
|
|
|
@ -152,7 +152,7 @@ def subtract_multiple_regions(base: Region, negations: Iterable[Region]) -> list
|
|||
class DOMTree(Tree[DOMNode]):
|
||||
"""A widget that displays the widget hierarchy."""
|
||||
# TODO: live update
|
||||
|
||||
|
||||
class Hovered(Message, bubble=True):
|
||||
"""Posted when a node in the tree is hovered with the mouse or highlighted with the keyboard.
|
||||
|
||||
|
@ -277,7 +277,7 @@ class DOMTree(Tree[DOMNode]):
|
|||
self.post_message(self.Hovered(self, node, node.data))
|
||||
else:
|
||||
self.post_message(self.Hovered(self, None, None))
|
||||
|
||||
|
||||
def on_leave(self, event: events.Leave) -> None:
|
||||
"""Handle the mouse leaving the tree."""
|
||||
self.hover_line = -1
|
||||
|
@ -343,7 +343,7 @@ class PropertiesTree(Tree[object]):
|
|||
|
||||
self._already_loaded: dict[TreeNode[object], set[str]] = {}
|
||||
"""A mapping of tree nodes to the keys that have already been loaded.
|
||||
|
||||
|
||||
This allows the tree to be collapsed and expanded without duplicating nodes.
|
||||
It's also used for lazy-loading nodes when clicking the ellipsis in long lists...
|
||||
"""
|
||||
|
@ -403,11 +403,11 @@ class PropertiesTree(Tree[object]):
|
|||
"a_frame": inspect.currentframe(),
|
||||
"a_traceback": traceback.extract_stack(),
|
||||
}
|
||||
|
||||
|
||||
@property
|
||||
def AAA_test_property_that_raises_exception(self) -> str:
|
||||
"""This property raises an exception when accessed.
|
||||
|
||||
|
||||
Navigate to this node in the DOM Tree and look in the Properties Panel to see the error message.
|
||||
"""
|
||||
raise Exception("EMIT: Error Message Itself Test")
|
||||
|
@ -420,7 +420,7 @@ class PropertiesTree(Tree[object]):
|
|||
|
||||
def _populate_node(self, node: TreeNode[object], load_more: bool = False) -> None:
|
||||
"""Populate a node with its children, or some of them.
|
||||
|
||||
|
||||
If load_more is True (ellipsis node clicked), load more children.
|
||||
Otherwise just load an initial batch.
|
||||
If the node is collapsed and re-expanded, no new nodes should be added.
|
||||
|
@ -442,7 +442,7 @@ class PropertiesTree(Tree[object]):
|
|||
|
||||
ellipsis_node: TreeNode[object] | None = None
|
||||
"""Node to show more properties when clicked."""
|
||||
|
||||
|
||||
only_counting = False
|
||||
"""Flag set when we've reached the limit and aren't adding any more nodes."""
|
||||
|
||||
|
@ -477,7 +477,7 @@ class PropertiesTree(Tree[object]):
|
|||
iterator = map(with_no_error, enumerate(data)) # type: ignore
|
||||
else:
|
||||
iterator = safe_dir_items(data) # type: ignore
|
||||
|
||||
|
||||
self._num_keys_accessed[node] = 0
|
||||
for key, value, exception in iterator:
|
||||
count += 1
|
||||
|
@ -600,7 +600,7 @@ class NodeInfo(Container):
|
|||
|
||||
class StaticWithLinkSupport(Static):
|
||||
"""Static text that supports DOM node links and file opening links.
|
||||
|
||||
|
||||
This class exists because actions can't target an arbitrary parent.
|
||||
The only supported namespaces are `screen` and `app`.
|
||||
So action_select_node has to be defined directly on the widget that
|
||||
|
@ -620,7 +620,7 @@ class NodeInfo(Container):
|
|||
if dom_node is None:
|
||||
return
|
||||
self.post_message(NodeInfo.FollowLinkToNode(dom_node))
|
||||
|
||||
|
||||
def action_open_file(self, path: str, line_number: int | None = None, column_number: int | None = None) -> None:
|
||||
"""Open a file."""
|
||||
# print("action_open_file", path, line_number, column_number)
|
||||
|
@ -726,7 +726,7 @@ class NodeInfo(Container):
|
|||
selector_set = rule_set.selector_set
|
||||
if match(selector_set, dom_node):
|
||||
applicable_rule_sets.append(rule_set)
|
||||
|
||||
|
||||
to_ignore = [
|
||||
("inspector.py", "set_rule"), # inspector's instrumentation
|
||||
("styles.py", "set_rule"),
|
||||
|
@ -769,7 +769,7 @@ class NodeInfo(Container):
|
|||
if frame_info.filename.endswith("inspector.py") and frame_info.function == "_apply_style_value":
|
||||
return "EDITED_WITH_INSPECTOR"
|
||||
return (frame_info.filename, frame_info.lineno)
|
||||
|
||||
|
||||
def format_location_info(location: tuple[str, int | None] | Literal["EDITED_WITH_INSPECTOR"] | None) -> Text:
|
||||
"""Shows a link to open the the source code where a style is set."""
|
||||
if location is None:
|
||||
|
@ -788,7 +788,7 @@ class NodeInfo(Container):
|
|||
# css_lines = dom_node.styles.inline.css_lines
|
||||
# But we need to associate the snake_cased/hyphenated/shorthand CSS property names,
|
||||
# in order to provide links to the source code.
|
||||
|
||||
|
||||
def format_style_line(rule: str, styles: Styles, rules: RulesMap, inline: bool) -> Text:
|
||||
"""Formats a single CSS line for display, with a link to open the source code."""
|
||||
# TODO: probably refactor arguments to take simpler data (!important flag bool etc....)
|
||||
|
@ -826,7 +826,7 @@ class NodeInfo(Container):
|
|||
# Note: rules[rule] won't be a Color for border-left etc. even if it SHOWS as just a color.
|
||||
# if isinstance(value, Color):
|
||||
# value_text = Text.styled(value_str, Style(bgcolor=value.rich_color, color=value.get_contrast_text().rich_color))
|
||||
|
||||
|
||||
# This is a bit specific, but it handles border values that are a border style followed by a color
|
||||
# (as well as plain colors).
|
||||
if " " in value_str:
|
||||
|
@ -872,7 +872,7 @@ class NodeInfo(Container):
|
|||
"}",
|
||||
)
|
||||
inline_style_text = format_styles_block(dom_node.styles.inline, Text.styled("inline styles", "italic"), None)
|
||||
|
||||
|
||||
def format_rule_set(rule_set: RuleSet) -> Text:
|
||||
"""Formats a CSS rule set for display, with a link to open the source code."""
|
||||
path: str | None = None
|
||||
|
@ -916,11 +916,11 @@ class NodeInfo(Container):
|
|||
# key_bindings_static.update("\n".join(map(repr, dom_node.BINDINGS)) or "(None defined with BINDINGS)")
|
||||
# highlighter = ReprHighlighter()
|
||||
# key_bindings_static.update(Text("\n").join(map(lambda binding: highlighter(repr(binding)), dom_node.BINDINGS)) or "(None defined with BINDINGS)")
|
||||
|
||||
|
||||
# sources = [dom_node]
|
||||
sources = dom_node.ancestors_with_self
|
||||
nodes_and_bindings = [
|
||||
(ancestor, binding)
|
||||
(ancestor, binding)
|
||||
for ancestor in sources
|
||||
for binding in ancestor._bindings.keys.values() # keys as in keybindings
|
||||
]
|
||||
|
@ -1016,7 +1016,7 @@ class NodeInfo(Container):
|
|||
usage_info = Text("\n\n").join(usages)
|
||||
else:
|
||||
usage_info = Text(f"No listeners found for {' or '.join(handler_names)}")
|
||||
|
||||
|
||||
def_location = format_object_location_info(message_class)
|
||||
qualname = message_class.__qualname__
|
||||
doc = inspect.getdoc(message_class) or '(No docstring)'
|
||||
|
@ -1070,7 +1070,7 @@ class NodeInfo(Container):
|
|||
input_parent.mount(self._style_value_input) # before setting value
|
||||
input_parent.mount(self._style_value_error)
|
||||
self._style_value_input.display = True
|
||||
|
||||
|
||||
# Find leftmost and rightmost positions of the rule
|
||||
while x > 0 and self._get_rule_at(x - 1, y) == rule:
|
||||
x -= 1
|
||||
|
@ -1121,12 +1121,12 @@ class NodeInfo(Container):
|
|||
# Tip: `merge` rather than `set_rule` allows a little trick of adding a new rule with
|
||||
# "<old rule value>; <new rule>: <new rule value>"
|
||||
# which is useful as a stopgap until there's a proper way to add new rules.
|
||||
|
||||
|
||||
# Prevent "(edited)" if the rule is unchanged.
|
||||
for rule in self.dom_node._inline_styles.get_rules():
|
||||
if new_styles.get_rule(rule) == self.dom_node._inline_styles.get_rule(rule):
|
||||
new_styles.clear_rule(rule)
|
||||
|
||||
|
||||
self.dom_node._inline_styles.merge(new_styles)
|
||||
self.dom_node.refresh(layout=True)
|
||||
self.watch_dom_node(self.dom_node) # refresh the inspector
|
||||
|
@ -1139,7 +1139,7 @@ class NodeInfo(Container):
|
|||
|
||||
class ResizeHandle(Widget):
|
||||
"""A handle for resizing a panel.
|
||||
|
||||
|
||||
This should be a child of the panel.
|
||||
Therefore, one of the sides of the divide needs to be a container.
|
||||
It will be positioned on the edge of the panel according to the `side` parameter.
|
||||
|
@ -1425,7 +1425,7 @@ class Inspector(Container):
|
|||
# Only widgets have a region, App (the root) doesn't.
|
||||
self.reset_highlight()
|
||||
return
|
||||
|
||||
|
||||
# Rainbow highlight of ancestors.
|
||||
"""
|
||||
if dom_node and dom_node is not self.screen:
|
||||
|
@ -1465,7 +1465,7 @@ class Inspector(Container):
|
|||
|
||||
if "inspector_highlight" not in self.app.styles.layers:
|
||||
self.app.styles.layers += ("inspector_highlight",)
|
||||
|
||||
|
||||
if dom_node not in self._highlight_boxes:
|
||||
self._highlight_boxes[dom_node] = {}
|
||||
used_boxes: list[Container] = []
|
||||
|
|
|
@ -88,7 +88,7 @@ class Menu(Container):
|
|||
item.press()
|
||||
break
|
||||
|
||||
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
"""Called when a button is clicked or activated with the keyboard."""
|
||||
|
||||
|
@ -181,7 +181,7 @@ class Menu(Container):
|
|||
plain_parts = item.label.plain.split("\t")
|
||||
if len(markup_parts) > 1:
|
||||
item.label = markup_parts[0] + " " * (max_width - len(plain_parts[0])) + markup_parts[1]
|
||||
|
||||
|
||||
def close(self):
|
||||
for item in self.items:
|
||||
if item.submenu:
|
||||
|
@ -189,7 +189,7 @@ class Menu(Container):
|
|||
if not isinstance(self, MenuBar):
|
||||
self.display = False
|
||||
self.post_message(Menu.StatusInfo(None, closed=True))
|
||||
|
||||
|
||||
def any_menus_open(self) -> bool:
|
||||
for item in self.items:
|
||||
if item.submenu and item.submenu.display:
|
||||
|
@ -233,7 +233,7 @@ class MenuItem(Button):
|
|||
self.id = "rc_" + str(id)
|
||||
else:
|
||||
self.id = "menu_item_" + to_snake_case(name)
|
||||
|
||||
|
||||
def on_enter(self, event: events.Enter) -> None:
|
||||
if isinstance(self.parent_menu, MenuBar):
|
||||
# The message is only reset to the default help text on close, so don't change it while no menu is open.
|
||||
|
|
|
@ -88,7 +88,7 @@ class MetaGlyphFont:
|
|||
self.covered_characters = covered_characters
|
||||
"""The characters supported by this font."""
|
||||
self.load()
|
||||
|
||||
|
||||
def load(self):
|
||||
"""Load the font from the .flf FIGlet font file."""
|
||||
# fig = Figlet(font=self.file_path) # gives FontNotFound error!
|
||||
|
@ -270,7 +270,7 @@ class Tool(Enum):
|
|||
|
||||
def get_name(self) -> str:
|
||||
"""Get the localized name for this tool.
|
||||
|
||||
|
||||
Not to be confused with tool.name, which is an identifier.
|
||||
"""
|
||||
return {
|
||||
|
@ -367,7 +367,7 @@ class ToolsBox(Container):
|
|||
button.tooltip = tool.get_name()
|
||||
self.tool_by_button[button] = tool
|
||||
yield button
|
||||
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
"""Called when a button is clicked."""
|
||||
|
||||
|
@ -376,7 +376,7 @@ class ToolsBox(Container):
|
|||
|
||||
class CharInput(Input, inherit_bindings=False):
|
||||
"""Widget for entering a single character."""
|
||||
|
||||
|
||||
class CharSelected(Message):
|
||||
"""Message sent when a character is selected."""
|
||||
def __init__(self, char: str) -> None:
|
||||
|
@ -397,7 +397,7 @@ class CharInput(Input, inherit_bindings=False):
|
|||
def validate_value(self, value: str) -> str:
|
||||
"""Limit the value to a single character."""
|
||||
return value[-1] if value else " "
|
||||
|
||||
|
||||
# Previously this used watch_value,
|
||||
# and had a bug where the character would oscillate between multiple values
|
||||
# due to a feedback loop between watch_value and on_char_input_char_selected.
|
||||
|
@ -423,7 +423,7 @@ class CharInput(Input, inherit_bindings=False):
|
|||
def validate_cursor_position(self, cursor_position: int) -> int:
|
||||
"""Force the cursor position to 0 so that it's over the character."""
|
||||
return 0
|
||||
|
||||
|
||||
def insert_text_at_cursor(self, text: str) -> None:
|
||||
"""Override to limit the value to a single character."""
|
||||
self.value = text[-1] if text else " "
|
||||
|
@ -538,7 +538,7 @@ class Selection:
|
|||
"""Copy the image data from the document into the selection."""
|
||||
self.contained_image = AnsiArtDocument(self.region.width, self.region.height)
|
||||
self.contained_image.copy_region(source=document, source_region=self.region)
|
||||
|
||||
|
||||
def copy_to_document(self, document: 'AnsiArtDocument') -> None:
|
||||
"""Draw the selection onto the document."""
|
||||
if not self.contained_image:
|
||||
|
@ -810,7 +810,7 @@ class AnsiArtDocument:
|
|||
@staticmethod
|
||||
def format_from_extension(file_path: str) -> str | None:
|
||||
"""Get the format ID from the file extension of the given path.
|
||||
|
||||
|
||||
Most format IDs are similar to the extension, e.g. 'PNG' for '.png',
|
||||
but some are different, e.g. 'JPEG2000' for '.jp2'.
|
||||
"""
|
||||
|
@ -865,7 +865,7 @@ class AnsiArtDocument:
|
|||
return self.encode_image_format(format_id)
|
||||
else:
|
||||
raise FormatWriteNotSupported(localized_message=_("Cannot write files in %1 format.", format_id) + "\n\n" + _("To save your changes, use a different filename."))
|
||||
|
||||
|
||||
def encode_image_format(self, pil_format_id: str) -> bytes:
|
||||
"""Encode the document as an image file."""
|
||||
size = (self.width, self.height)
|
||||
|
@ -956,12 +956,12 @@ class AnsiArtDocument:
|
|||
def get_rich_console_markup(self) -> str:
|
||||
"""Get the Rich API markup representation of the document."""
|
||||
return self.get_renderable().markup
|
||||
|
||||
|
||||
def get_html(self) -> str:
|
||||
"""Get the HTML representation of the document."""
|
||||
console = self.get_console()
|
||||
return console.export_html(inline_styles=True, code_format=CUSTOM_CONSOLE_HTML_FORMAT)
|
||||
|
||||
|
||||
def get_svg(self) -> str:
|
||||
"""Get the SVG representation of the document."""
|
||||
console = self.get_console()
|
||||
|
@ -972,7 +972,7 @@ class AnsiArtDocument:
|
|||
# unless I escaped all the braces, but that would be ugly! So I'm just using a different syntax.)
|
||||
# `html.escape` leaves control codes, which blows up ET.fromstring, so use base64 instead.
|
||||
return svg.replace("%ANSI_GOES_HERE%", base64.b64encode(self.get_ansi().encode("utf-8")).decode("utf-8"))
|
||||
|
||||
|
||||
def get_renderable(self) -> Text:
|
||||
"""Get a Rich renderable for the document."""
|
||||
joiner = Text("\n")
|
||||
|
@ -1260,7 +1260,7 @@ class AnsiArtDocument:
|
|||
document.bg[y].append(default_bg)
|
||||
document.fg[y].append(default_fg)
|
||||
return document
|
||||
|
||||
|
||||
@staticmethod
|
||||
def from_text(text: str, default_bg: str = "#ffffff", default_fg: str = "#000000") -> 'AnsiArtDocument':
|
||||
"""Creates a document from the given text, detecting if it uses ANSI control codes or not."""
|
||||
|
@ -1272,7 +1272,7 @@ class AnsiArtDocument:
|
|||
@staticmethod
|
||||
def from_image_format(content: bytes) -> 'AnsiArtDocument':
|
||||
"""Creates a document from the given bytes, detecting the file format.
|
||||
|
||||
|
||||
Raises UnidentifiedImageError if the format is not detected.
|
||||
"""
|
||||
image = Image.open(io.BytesIO(content))
|
||||
|
@ -1288,7 +1288,7 @@ class AnsiArtDocument:
|
|||
@staticmethod
|
||||
def from_svg(svg: str, default_bg: str = "#ffffff", default_fg: str = "#000000") -> 'AnsiArtDocument':
|
||||
"""Creates a document from an SVG containing a character grid with rects for cell backgrounds.
|
||||
|
||||
|
||||
- If the SVG contains a special <ansi> element, this is used instead of anything else.
|
||||
Otherwise it falls back to flexible grid detection:
|
||||
- rect elements can be in any order.
|
||||
|
@ -1355,11 +1355,11 @@ class AnsiArtDocument:
|
|||
def rewrite_property(match: re.Match[str]) -> str:
|
||||
property_name = match.group(1)
|
||||
property_value = match.group(2)
|
||||
|
||||
|
||||
if property_name in property_map:
|
||||
rewritten_name = property_map[property_name]
|
||||
return f"{rewritten_name}: {property_value}"
|
||||
|
||||
|
||||
return match.group(0) # Return the original match if no rewrite is needed
|
||||
|
||||
for style_element in root.findall(".//{http://www.w3.org/2000/svg}style"):
|
||||
|
@ -1660,7 +1660,7 @@ class AnsiArtDocument:
|
|||
except IndexError:
|
||||
print("Warning: text element is out of bounds: " + ET.tostring(text, encoding="unicode"))
|
||||
continue
|
||||
|
||||
|
||||
# For debugging, write the SVG with the ignored rects outlined, and coordinate markers added.
|
||||
if DEBUG_SVG_LOADING:
|
||||
ET.ElementTree(root).write("debug.svg", encoding="unicode")
|
||||
|
@ -1670,7 +1670,7 @@ class AnsiArtDocument:
|
|||
@staticmethod
|
||||
def decode_based_on_file_extension(content: bytes, file_path: str, default_bg: str = "#ffffff", default_fg: str = "#000000") -> 'AnsiArtDocument':
|
||||
"""Creates a document from the given bytes, detecting the file format.
|
||||
|
||||
|
||||
Raises FormatReadNotSupported if the file format is not supported for reading. Some are write-only.
|
||||
Raises UnicodeDecodeError, which can be a very long message, so make sure to handle it!
|
||||
Raises UnidentifiedImageError if the format is not detected.
|
||||
|
@ -1706,7 +1706,7 @@ class Action:
|
|||
|
||||
TODO: In the future it would be more efficient to use a mask for the region update,
|
||||
to store only modified pixels, and use RLE compression on the mask and image data.
|
||||
|
||||
|
||||
NOTE: Not to be confused with Textual's `class Action(Event)`, or the type of law suit.
|
||||
Indeed, Textual's actions are used significantly in this application, with action_* methods,
|
||||
but this class is not related. Perhaps I should rename this class to UndoOp, or HistoryOperation.
|
||||
|
@ -1720,7 +1720,7 @@ class Action:
|
|||
"""The region of the document that was modified."""
|
||||
self.is_full_update = False
|
||||
"""Indicates that this action resizes the document, and thus should not be undone with a region update.
|
||||
|
||||
|
||||
That is, unless in the future region updates support a mask and work in tandem with resizes.
|
||||
"""
|
||||
self.sub_image_before: AnsiArtDocument|None = None
|
||||
|
@ -1775,7 +1775,7 @@ class Canvas(Widget):
|
|||
self.button = mouse_down_event.button
|
||||
self.ctrl = mouse_down_event.ctrl
|
||||
super().__init__()
|
||||
|
||||
|
||||
class ToolUpdate(Message):
|
||||
"""Message when dragging on the canvas."""
|
||||
|
||||
|
@ -1837,7 +1837,7 @@ class Canvas(Widget):
|
|||
self.pointer_active = True
|
||||
self.which_button = event.button
|
||||
self.capture_mouse(True)
|
||||
|
||||
|
||||
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,
|
||||
|
@ -1884,7 +1884,7 @@ class Canvas(Widget):
|
|||
self.fix_mouse_event(event)
|
||||
event.x //= self.magnification
|
||||
event.y //= self.magnification
|
||||
|
||||
|
||||
if self.pointer_active:
|
||||
self.post_message(self.ToolStop(event))
|
||||
self.pointer_active = False
|
||||
|
@ -1993,7 +1993,7 @@ class Canvas(Widget):
|
|||
style = Style(color=inverse_color, bgcolor=inverse_bgcolor)
|
||||
segments.append(Segment(ch, style))
|
||||
return Strip(segments, self.size.width)
|
||||
|
||||
|
||||
def refresh_scaled_region(self, region: Region) -> None:
|
||||
"""Refresh a region of the widget, scaled by the magnification."""
|
||||
if self.magnification == 1:
|
||||
|
@ -2006,7 +2006,7 @@ class Canvas(Widget):
|
|||
(region.width + 2) * self.magnification,
|
||||
(region.height + 2) * self.magnification,
|
||||
))
|
||||
|
||||
|
||||
def watch_magnification(self) -> None:
|
||||
"""Called when magnification changes."""
|
||||
self.active_meta_glyph_font = largest_font_that_fits(self.magnification, self.magnification)
|
||||
|
@ -2210,7 +2210,7 @@ class PaintApp(App[None]):
|
|||
"""The document being edited. Contains the selection, if any."""
|
||||
image_initialized = False
|
||||
"""Whether the image is ready. This flag exists to avoid type checking woes if I were to allow image to be None."""
|
||||
|
||||
|
||||
magnification = var(1)
|
||||
"""Current magnification level."""
|
||||
return_to_magnification = var(4)
|
||||
|
@ -2236,12 +2236,12 @@ class PaintApp(App[None]):
|
|||
"""The folder to save a temporary backup file to. If None, will save alongside the file being edited."""
|
||||
backup_checked_for: Optional[str] = None
|
||||
"""The file path last checked for a backup save.
|
||||
|
||||
|
||||
This is tracked to prevent discarding Untitled.ans~ when loading a document on startup.
|
||||
Indicates that the file path either was loaded (recovered) or was not found.
|
||||
Not set when failing to load a backup, since the file maybe shouldn't be discarded in that case.
|
||||
"""
|
||||
|
||||
|
||||
mouse_gesture_cancelled = False
|
||||
"""For Undo/Redo, to interrupt the current action"""
|
||||
mouse_at_start: Offset = Offset(0, 0)
|
||||
|
@ -2262,7 +2262,7 @@ class PaintApp(App[None]):
|
|||
"""Used for Polygon tool to detect double-click"""
|
||||
color_eraser_mode: bool = False
|
||||
"""Used for Eraser/Color Eraser tool, when using the right mouse button"""
|
||||
|
||||
|
||||
background_tasks: set[asyncio.Task[None]] = set()
|
||||
"""Stores references to Task objects so they don't get garbage collected."""
|
||||
|
||||
|
@ -2278,7 +2278,7 @@ class PaintApp(App[None]):
|
|||
def watch_show_tools_box(self, show_tools_box: bool) -> None:
|
||||
"""Called when show_tools_box changes."""
|
||||
self.query_one("#tools_box", ToolsBox).display = show_tools_box
|
||||
|
||||
|
||||
def watch_show_colors_box(self, show_colors_box: bool) -> None:
|
||||
"""Called when show_colors_box changes."""
|
||||
self.query_one("#colors_box", ColorsBox).display = show_colors_box
|
||||
|
@ -2359,7 +2359,7 @@ class PaintApp(App[None]):
|
|||
return affected_region_base.union(affected_region)
|
||||
else:
|
||||
return affected_region
|
||||
|
||||
|
||||
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:
|
||||
|
@ -2417,7 +2417,7 @@ class PaintApp(App[None]):
|
|||
self.image.ch[y][x] = char
|
||||
self.image.bg[y][x] = bg_color
|
||||
self.image.fg[y][x] = fg_color
|
||||
|
||||
|
||||
def erase_region(self, region: Region, mask: Optional[list[list[bool]]] = None) -> None:
|
||||
"""Clears the given region."""
|
||||
# Time to go undercover as an eraser. 🥸
|
||||
|
@ -2498,7 +2498,7 @@ class PaintApp(App[None]):
|
|||
|
||||
if self.selected_tool not in [Tool.polygon, Tool.curve]:
|
||||
return
|
||||
|
||||
|
||||
if self.selected_tool == Tool.polygon and len(self.tool_points) < 3:
|
||||
return
|
||||
if self.selected_tool == Tool.curve and len(self.tool_points) < 2:
|
||||
|
@ -2513,7 +2513,7 @@ class PaintApp(App[None]):
|
|||
affected_region = self.draw_current_polygon()
|
||||
else:
|
||||
affected_region = self.draw_current_curve()
|
||||
|
||||
|
||||
action.region = affected_region
|
||||
action.region = action.region.intersection(Region(0, 0, self.image.width, self.image.height))
|
||||
action.update(self.image_at_start)
|
||||
|
@ -2666,7 +2666,7 @@ class PaintApp(App[None]):
|
|||
return True
|
||||
except PermissionError:
|
||||
self.message_box(dialog_title, _("Access denied."), "ok")
|
||||
except FileNotFoundError:
|
||||
except FileNotFoundError:
|
||||
self.message_box(dialog_title, _("%1 contains an invalid path.", file_path), "ok")
|
||||
except OSError as e:
|
||||
self.message_box(dialog_title, _("Failed to save document."), "ok", error=e)
|
||||
|
@ -2676,7 +2676,7 @@ class PaintApp(App[None]):
|
|||
|
||||
def reload_after_save(self, content: bytes, file_path: str) -> bool:
|
||||
"""Reload the document from saved content, to show information loss from the file format.
|
||||
|
||||
|
||||
Unlike `open_from_file_path`, this method:
|
||||
- doesn't short circuit when the file path matches the current file path, crucially
|
||||
- skips backup management (discarding or checking for a backup)
|
||||
|
@ -2705,7 +2705,7 @@ class PaintApp(App[None]):
|
|||
|
||||
def update_palette_from_format_id(self, format_id: str | None) -> None:
|
||||
"""Update the palette based on the file format.
|
||||
|
||||
|
||||
In the future, this should update from attributes set when loading the file,
|
||||
such as whether it supports color, and if not, it could show pattern fills,
|
||||
such as ░▒▓█... that's not a lot of patterns, and you could get those from the
|
||||
|
@ -2722,7 +2722,7 @@ class PaintApp(App[None]):
|
|||
|
||||
async def save(self) -> bool:
|
||||
"""Save the image to a file.
|
||||
|
||||
|
||||
Note that this method will never return if the user cancels the Save As dialog.
|
||||
"""
|
||||
self.stop_action_in_progress()
|
||||
|
@ -2751,7 +2751,7 @@ class PaintApp(App[None]):
|
|||
await self.save_as()
|
||||
# If the user cancels the Save As dialog, we'll never get here.
|
||||
return True
|
||||
|
||||
|
||||
def action_save_as(self) -> None:
|
||||
"""Show the save as dialog, without waiting for it to close."""
|
||||
# Action must not await the dialog closing,
|
||||
|
@ -2767,7 +2767,7 @@ class PaintApp(App[None]):
|
|||
# You could make a selection while the dialog is open, for example.
|
||||
self.stop_action_in_progress()
|
||||
self.close_windows("SaveAsDialogWindow, OpenDialogWindow")
|
||||
|
||||
|
||||
saved_future: asyncio.Future[None] = asyncio.Future()
|
||||
|
||||
def handle_selected_file_path(file_path: str) -> None:
|
||||
|
@ -2860,7 +2860,7 @@ class PaintApp(App[None]):
|
|||
self.confirm_overwrite(file_path, on_save_confirmed)
|
||||
else:
|
||||
on_save_confirmed()
|
||||
|
||||
|
||||
window = SaveAsDialogWindow(
|
||||
title=_("Copy To"),
|
||||
handle_selected_file_path=handle_selected_file_path,
|
||||
|
@ -2944,7 +2944,7 @@ class PaintApp(App[None]):
|
|||
|
||||
def confirm_information_loss(self, format_id: str | None, callback: Callable[[bool], None]) -> None:
|
||||
"""Confirms discarding information when saving as a particular format. Callback variant. Never calls back if unconfirmed.
|
||||
|
||||
|
||||
The callback argument is whether there's information loss AND the file is openable.
|
||||
This is used to determine whether the file should be reloaded to show the information loss.
|
||||
It can't be reloaded if it's not openable.
|
||||
|
@ -3020,14 +3020,14 @@ class PaintApp(App[None]):
|
|||
"""Exit the program immediately, deleting the backup file."""
|
||||
self.discard_backup()
|
||||
self.exit()
|
||||
|
||||
|
||||
def action_exit(self) -> None:
|
||||
"""Exit the program, prompting to save changes if necessary."""
|
||||
if self.is_document_modified():
|
||||
self.prompt_save_changes(self.file_path or _("Untitled"), self.discard_backup_and_exit)
|
||||
else:
|
||||
self.discard_backup_and_exit()
|
||||
|
||||
|
||||
def action_reload(self) -> None:
|
||||
"""Reload the program, prompting to save changes if necessary."""
|
||||
# restart_program() calls discard_backup()
|
||||
|
@ -3057,7 +3057,7 @@ class PaintApp(App[None]):
|
|||
icon_widget = get_warning_icon()
|
||||
|
||||
# self.close_windows("#message_box")
|
||||
|
||||
|
||||
self.bell()
|
||||
|
||||
def handle_button(button: Button) -> None:
|
||||
|
@ -3128,7 +3128,7 @@ class PaintApp(App[None]):
|
|||
def go_ahead():
|
||||
# Note: exceptions handled outside of this function (UnicodeDecodeError, UnidentifiedImageError, FormatReadNotSupported)
|
||||
new_image = AnsiArtDocument.decode_based_on_file_extension(content, file_path)
|
||||
|
||||
|
||||
# action_new handles discarding the backup, and recovering from Untitled.ans~, by default
|
||||
# but we need to 1. handle the case where the backup is the file to be opened,
|
||||
# and 2. recover from <file to be opened>.ans~ instead of Untitled.ans~
|
||||
|
@ -3146,7 +3146,7 @@ class PaintApp(App[None]):
|
|||
print("Error comparing files:", e)
|
||||
if not opening_backup:
|
||||
self.discard_backup()
|
||||
|
||||
|
||||
self.action_new(force=True, manage_backup=False)
|
||||
self.canvas.image = self.image = new_image
|
||||
self.canvas.refresh(layout=True)
|
||||
|
@ -3226,7 +3226,7 @@ class PaintApp(App[None]):
|
|||
|
||||
def action_new(self, *, force: bool = False, manage_backup: bool = True) -> None:
|
||||
"""Create a new image, discarding the backup file for the old file path, and undos/redos.
|
||||
|
||||
|
||||
This method is used as part of opening files as well,
|
||||
in which case force=True and recover=False,
|
||||
because prompting and recovering are handled outside.
|
||||
|
@ -3259,7 +3259,7 @@ class PaintApp(App[None]):
|
|||
|
||||
if manage_backup:
|
||||
self.recover_from_backup()
|
||||
|
||||
|
||||
def action_open_character_selector(self) -> None:
|
||||
"""Show dialog to select a character."""
|
||||
self.close_windows("#character_selector_dialog")
|
||||
|
@ -3439,7 +3439,7 @@ Columns: {len(palette) // 2}
|
|||
self.message_box(_("Paint"), "Not implemented.", "ok")
|
||||
def action_send(self) -> None:
|
||||
self.message_box(_("Paint"), "Not implemented.", "ok")
|
||||
|
||||
|
||||
def action_set_as_wallpaper_tiled(self) -> None:
|
||||
"""Tile the image as the wallpaper."""
|
||||
self.set_as_wallpaper(tiled=True)
|
||||
|
@ -3538,7 +3538,7 @@ Columns: {len(palette) // 2}
|
|||
|
||||
def get_selected_content(self, file_path: str|None = None) -> bytes | None:
|
||||
"""Returns the content of the selection, or underlying the selection if it hasn't been cut out yet.
|
||||
|
||||
|
||||
For a textbox, returns the selected text within the textbox. May include ANSI escape sequences, either way.
|
||||
|
||||
Raises FormatWriteNotSupported if the file_path implies a format that can't be encoded.
|
||||
|
@ -3663,18 +3663,18 @@ Columns: {len(palette) // 2}
|
|||
self.image.selection = Selection(Region(0, 0, self.image.width, self.image.height))
|
||||
self.canvas.refresh()
|
||||
self.selected_tool = Tool.select
|
||||
|
||||
|
||||
def action_text_toolbar(self) -> None:
|
||||
self.message_box(_("Paint"), "Not implemented.", "ok")
|
||||
|
||||
|
||||
def action_normal_size(self) -> None:
|
||||
"""Zoom to 1x."""
|
||||
self.magnification = 1
|
||||
|
||||
|
||||
def action_large_size(self) -> None:
|
||||
"""Zoom to 4x."""
|
||||
self.magnification = 4
|
||||
|
||||
|
||||
def action_custom_zoom(self) -> None:
|
||||
"""Show dialog to set zoom level."""
|
||||
self.close_windows("#zoom_dialog")
|
||||
|
@ -3850,7 +3850,7 @@ Columns: {len(palette) // 2}
|
|||
self.image.fg[self.image.height - y - 1][x] = source.fg[y][x]
|
||||
self.image.bg[self.image.height - y - 1][x] = source.bg[y][x]
|
||||
self.canvas.refresh()
|
||||
|
||||
|
||||
def action_rotate_by_angle(self, angle: int) -> None:
|
||||
"""Rotate the image by the given angle, one of 90, 180, or 270."""
|
||||
action = Action(_("Rotate by angle"), Region(0, 0, self.image.width, self.image.height))
|
||||
|
@ -3863,7 +3863,7 @@ Columns: {len(palette) // 2}
|
|||
|
||||
if angle != 180:
|
||||
self.image.resize(self.image.height, self.image.width)
|
||||
|
||||
|
||||
for y in range(self.image.height):
|
||||
for x in range(self.image.width):
|
||||
if angle == 90:
|
||||
|
@ -4072,8 +4072,8 @@ Columns: {len(palette) // 2}
|
|||
self.add_action(action)
|
||||
|
||||
self.image.invert()
|
||||
self.canvas.refresh()
|
||||
|
||||
self.canvas.refresh()
|
||||
|
||||
def resize_document(self, width: int, height: int) -> None:
|
||||
"""Resize the document, creating an undo state, and refresh the canvas."""
|
||||
self.cancel_preview()
|
||||
|
@ -4087,7 +4087,7 @@ Columns: {len(palette) // 2}
|
|||
self.add_action(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:
|
||||
|
@ -4131,7 +4131,7 @@ Columns: {len(palette) // 2}
|
|||
)
|
||||
)
|
||||
self.mount(window)
|
||||
|
||||
|
||||
def action_clear_image(self) -> None:
|
||||
"""Clear the image, creating an undo state."""
|
||||
# This could be simplified to use erase_region, but that would be marginally slower.
|
||||
|
@ -4155,7 +4155,7 @@ Columns: {len(palette) // 2}
|
|||
def action_draw_opaque(self) -> None:
|
||||
"""Toggles opaque/transparent selection mode."""
|
||||
self.message_box(_("Paint"), "Not implemented.", "ok")
|
||||
|
||||
|
||||
def action_help_topics(self) -> None:
|
||||
"""Show the Help Topics dialog."""
|
||||
self.close_windows("#help_dialog")
|
||||
|
@ -4193,7 +4193,7 @@ Columns: {len(palette) // 2}
|
|||
window.content.mount(Container(Static(help_text, markup=False), classes="help_text_container"))
|
||||
window.content.mount(Button(_("OK"), classes="ok submit"))
|
||||
self.mount(window)
|
||||
|
||||
|
||||
def action_about_paint(self) -> None:
|
||||
"""Show the About Paint dialog."""
|
||||
self.close_windows("#about_paint_dialog")
|
||||
|
@ -4349,7 +4349,7 @@ Columns: {len(palette) // 2}
|
|||
|
||||
def magnifier_click(self, x: int, y: int) -> None:
|
||||
"""Zooms in or out on the image."""
|
||||
|
||||
|
||||
prev_magnification = self.magnification
|
||||
prospective_magnification = self.get_prospective_magnification()
|
||||
|
||||
|
@ -4386,7 +4386,7 @@ Columns: {len(palette) // 2}
|
|||
|
||||
def extract_to_selection(self, erase_underlying: bool = True) -> None:
|
||||
"""Extracts image data underlying the selection from the document into the selection.
|
||||
|
||||
|
||||
This creates an undo state with the current tool's name, which should be Select or Free-Form Select.
|
||||
"""
|
||||
sel = self.image.selection
|
||||
|
@ -4497,7 +4497,7 @@ Columns: {len(palette) // 2}
|
|||
self.image_at_start.copy_region(self.image)
|
||||
action = Action(self.selected_tool.get_name())
|
||||
self.add_action(action)
|
||||
|
||||
|
||||
affected_region = None
|
||||
if self.selected_tool == Tool.pencil or self.selected_tool == Tool.brush:
|
||||
affected_region = self.stamp_brush(event.x, event.y)
|
||||
|
@ -4531,7 +4531,7 @@ Columns: {len(palette) // 2}
|
|||
region = self.canvas.select_preview_region
|
||||
self.canvas.select_preview_region = None
|
||||
self.canvas.refresh_scaled_region(region)
|
||||
|
||||
|
||||
# To avoid saving with a tool preview as part of the image data,
|
||||
# or interrupting the user's flow by canceling the preview occasionally to auto-save a backup,
|
||||
# we postpone auto-saving the backup until the image is clean of any previews.
|
||||
|
@ -4642,12 +4642,12 @@ Columns: {len(palette) // 2}
|
|||
"""Merges the selection into the image, or deletes it if meld is False."""
|
||||
if not self.image.selection:
|
||||
return
|
||||
|
||||
|
||||
if self.image.selection.textbox_mode:
|
||||
# The Text tool creates an undo state only when you switch tools
|
||||
# or click outside the textbox, melding the textbox into the image.
|
||||
# If you're deleting the textbox, an undo state doesn't need to be created.
|
||||
|
||||
|
||||
# If you haven't typed anything into the textbox yet, it should be deleted
|
||||
# to make it easier to start over in positioning the textbox.
|
||||
# If you have typed something, it should be melded into the image,
|
||||
|
@ -4708,7 +4708,7 @@ Columns: {len(palette) // 2}
|
|||
|
||||
def on_canvas_tool_update(self, event: Canvas.ToolUpdate) -> None:
|
||||
"""Called when the user is drawing on the canvas.
|
||||
|
||||
|
||||
Several tools do a preview of sorts here, even though it's not the ToolPreviewUpdate event.
|
||||
TODO: rename these events to describe when they occur, ascribe less semantics to them.
|
||||
"""
|
||||
|
@ -4741,7 +4741,7 @@ Columns: {len(palette) // 2}
|
|||
|
||||
if self.selected_tool in [Tool.fill, Tool.magnifier]:
|
||||
return
|
||||
|
||||
|
||||
if self.selected_tool in [Tool.select, Tool.free_form_select, Tool.text]:
|
||||
sel = self.image.selection
|
||||
if self.selecting_text:
|
||||
|
@ -4797,7 +4797,7 @@ Columns: {len(palette) // 2}
|
|||
old_action.undo(self.image)
|
||||
action = Action(self.selected_tool.get_name(), affected_region)
|
||||
self.undos.append(action)
|
||||
|
||||
|
||||
if self.selected_tool in [Tool.pencil, Tool.brush, Tool.eraser, Tool.airbrush]:
|
||||
for x, y in bresenham_walk(self.mouse_previous.x, self.mouse_previous.y, event.x, event.y):
|
||||
affected_region = self.stamp_brush(x, y, affected_region)
|
||||
|
@ -4841,7 +4841,7 @@ Columns: {len(palette) // 2}
|
|||
affected_region = self.stamp_brush(x, y, affected_region)
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
# Update action region and image data
|
||||
if action.region and affected_region:
|
||||
action.region = action.region.union(affected_region)
|
||||
|
@ -4858,7 +4858,7 @@ Columns: {len(palette) // 2}
|
|||
assert old_action is not None, "old_action should have been set if replace_action is True"
|
||||
affected_region = affected_region.union(old_action.region)
|
||||
self.canvas.refresh_scaled_region(affected_region)
|
||||
|
||||
|
||||
self.mouse_previous = Offset(event.x, event.y)
|
||||
|
||||
def on_canvas_tool_stop(self, event: Canvas.ToolStop) -> None:
|
||||
|
@ -4885,7 +4885,7 @@ Columns: {len(palette) // 2}
|
|||
# Done selecting text
|
||||
self.selecting_text = False
|
||||
return
|
||||
|
||||
|
||||
assert self.mouse_at_start is not None, "mouse_at_start should be set on mouse down"
|
||||
# Note that self.mouse_at_start is not set to None on mouse up,
|
||||
# so it can't be used to check if the mouse is down.
|
||||
|
@ -4927,7 +4927,7 @@ Columns: {len(palette) // 2}
|
|||
self.make_preview(self.draw_current_curve)
|
||||
elif self.selected_tool == Tool.polygon:
|
||||
# Maybe finish drawing a polygon
|
||||
|
||||
|
||||
# Check if the distance between the first and last point is small enough,
|
||||
# or if the user double-clicked.
|
||||
close_gap_threshold_cells = 2
|
||||
|
@ -5106,7 +5106,7 @@ Columns: {len(palette) // 2}
|
|||
# Detect file drop
|
||||
def _extract_filepaths(text: str) -> list[str]:
|
||||
"""Extracts escaped filepaths from text.
|
||||
|
||||
|
||||
Taken from https://github.com/agmmnn/textual-filedrop/blob/55a288df65d1397b959d55ef429e5282a0bb21ff/textual_filedrop/_filedrop.py#L17-L36
|
||||
"""
|
||||
split_filepaths = []
|
||||
|
@ -5128,7 +5128,7 @@ Columns: {len(palette) // 2}
|
|||
# for file in files:
|
||||
# filepaths.append(os.path.join(root, file))
|
||||
return filepaths
|
||||
|
||||
|
||||
try:
|
||||
filepaths = _extract_filepaths(event.text)
|
||||
if filepaths:
|
||||
|
@ -5137,7 +5137,7 @@ Columns: {len(palette) // 2}
|
|||
return
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
# Text pasting is only supported with Ctrl+V or Edit > Paste, handled separately.
|
||||
return
|
||||
|
||||
|
@ -5162,7 +5162,7 @@ Columns: {len(palette) // 2}
|
|||
self.selected_tool = event.tool
|
||||
if self.selected_tool not in [Tool.magnifier, Tool.pick_color]:
|
||||
self.return_to_tool = self.selected_tool
|
||||
|
||||
|
||||
def on_char_input_char_selected(self, event: CharInput.CharSelected) -> None:
|
||||
"""Called when a character is entered in the character input."""
|
||||
self.selected_char = event.char
|
||||
|
@ -5280,7 +5280,7 @@ HeaderIcon.icon = "[rgb(0,0,0) on rgb(255,255,255)]...[/][rgb(255,255,255)]\\\\[
|
|||
# Prevent wrapping, for a CSS effect, cropping to hide the shading "~" of the page fold when the page fold isn't visible.
|
||||
HeaderIcon.icon = Text.from_markup(HeaderIcon.icon, overflow="crop")
|
||||
|
||||
# `textual run --dev src.textual_paint.paint` will search for a
|
||||
# `textual run --dev src.textual_paint.paint` will search for a
|
||||
# global variable named `app`, and fallback to
|
||||
# anything that is an instance of `App`, or
|
||||
# a subclass of `App`.
|
||||
|
|
|
@ -21,7 +21,7 @@ def get_desktop_environment() -> str:
|
|||
if desktop_session is not None: # easier to match if we doesn't have to deal with character cases
|
||||
desktop_session = desktop_session.lower()
|
||||
if desktop_session in [
|
||||
"gnome", "unity", "cinnamon", "mate", "xfce4", "lxde", "fluxbox",
|
||||
"gnome", "unity", "cinnamon", "mate", "xfce4", "lxde", "fluxbox",
|
||||
"blackbox", "openbox", "icewm", "jwm", "afterstep", "trinity", "kde"
|
||||
]:
|
||||
return desktop_session
|
||||
|
@ -33,11 +33,11 @@ def get_desktop_environment() -> str:
|
|||
elif desktop_session.startswith("ubuntustudio"):
|
||||
return "kde"
|
||||
elif desktop_session.startswith("ubuntu"):
|
||||
return "gnome"
|
||||
return "gnome"
|
||||
elif desktop_session.startswith("lubuntu"):
|
||||
return "lxde"
|
||||
elif desktop_session.startswith("kubuntu"):
|
||||
return "kde"
|
||||
return "lxde"
|
||||
elif desktop_session.startswith("kubuntu"):
|
||||
return "kde"
|
||||
elif desktop_session.startswith("razor"): # e.g. razorkwin
|
||||
return "razor-qt"
|
||||
elif desktop_session.startswith("wmaker"): # e.g. wmaker-common
|
||||
|
@ -141,7 +141,7 @@ def set_wallpaper(file_loc: str, first_run: bool = True):
|
|||
import configparser
|
||||
desktop_conf = configparser.ConfigParser()
|
||||
# Development version
|
||||
desktop_conf_file = os.path.join(get_config_dir("razor"), "desktop.conf")
|
||||
desktop_conf_file = os.path.join(get_config_dir("razor"), "desktop.conf")
|
||||
if os.path.isfile(desktop_conf_file):
|
||||
config_option = R"screens\1\desktops\1\wallpaper"
|
||||
else:
|
||||
|
@ -157,11 +157,11 @@ def set_wallpaper(file_loc: str, first_run: bool = True):
|
|||
pass
|
||||
else:
|
||||
# TODO: reload desktop when possible
|
||||
pass
|
||||
pass
|
||||
elif desktop_env in ["fluxbox", "jwm", "openbox", "afterstep"]:
|
||||
# http://fluxbox-wiki.org/index.php/Howto_set_the_background
|
||||
# used fbsetbg on jwm too since I am too lazy to edit the XML configuration
|
||||
# now where fbsetbg does the job excellent anyway.
|
||||
# used fbsetbg on jwm too since I am too lazy to edit the XML configuration
|
||||
# now where fbsetbg does the job excellent anyway.
|
||||
# and I have not figured out how else it can be set on Openbox and AfterSTep
|
||||
# but fbsetbg works excellent here too.
|
||||
try:
|
||||
|
@ -229,9 +229,9 @@ def set_wallpaper(file_loc: str, first_run: bool = True):
|
|||
|
||||
def get_config_dir(app_name: str) -> str:
|
||||
if "XDG_CONFIG_HOME" in os.environ:
|
||||
config_home = os.environ["XDG_CONFIG_HOME"]
|
||||
config_home = os.environ["XDG_CONFIG_HOME"]
|
||||
elif "APPDATA" in os.environ: # On Windows
|
||||
config_home = os.environ["APPDATA"]
|
||||
config_home = os.environ["APPDATA"]
|
||||
else:
|
||||
try:
|
||||
from xdg import BaseDirectory # type: ignore
|
||||
|
|
|
@ -157,7 +157,7 @@ class Window(Container):
|
|||
|
||||
def constrain_to_screen(self) -> None:
|
||||
"""Constrain window to screen, so that the title bar is always visible.
|
||||
|
||||
|
||||
This method must take into account the fact that the window is centered with `align: center middle;`
|
||||
TODO: Call this on screen resize.
|
||||
"""
|
||||
|
@ -203,7 +203,7 @@ class Window(Container):
|
|||
def on_focus(self, event: events.Focus) -> None:
|
||||
"""Called when the window is focused."""
|
||||
self.focus()
|
||||
|
||||
|
||||
def focus(self, scroll_visible: bool = True) -> Self:
|
||||
"""Focus the window. Note that scroll_visible may scroll a descendant into view, but never the window into view within the screen."""
|
||||
# Focus last focused widget if re-focusing
|
||||
|
|
Loading…
Reference in New Issue
Block a user