Remove all trailing whitespace

using regular expression: \s+$
This commit is contained in:
Isaiah Odhner 2023-09-04 21:43:57 -04:00
parent bd6ce9b3a7
commit c9757a4549
17 changed files with 180 additions and 180 deletions

View File

@ -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:

View File

@ -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)

View File

@ -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')

View File

@ -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",
"",

View File

@ -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:

View File

@ -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

View File

@ -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"):

View File

@ -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)

View File

@ -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

View File

@ -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.
"""

View File

@ -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

View File

@ -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):

View File

@ -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] = []

View File

@ -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.

View File

@ -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`.

View File

@ -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

View File

@ -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