Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tree fix #744

Merged
merged 8 commits into from
Sep 7, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 1 addition & 7 deletions sandbox/will/tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,10 @@


class TreeApp(App):
DEFAULT_CSS = """
Screen {
overflow: auto;

}
"""

def compose(self):
tree = DirectoryTree("~/projects")
yield Container(tree)
tree.focus()


app = TreeApp()
Expand Down
2 changes: 1 addition & 1 deletion src/textual/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ def __init_subclass__(

title: Reactive[str] = Reactive("Textual")
sub_title: Reactive[str] = Reactive("")
dark: Reactive[bool] = Reactive(False)
dark: Reactive[bool] = Reactive(True)

@property
def devtools_enabled(self) -> bool:
Expand Down
3 changes: 2 additions & 1 deletion src/textual/dom.py
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,8 @@ def rich_style(self) -> Style:
"""Get a Rich Style object for this DOMNode."""
_, _, background, color = self.colors
style = (
Style.from_color(color.rich_color, background.rich_color) + self.text_style
Style.from_color((background + color).rich_color, background.rich_color)
+ self.text_style
)
return style

Expand Down
8 changes: 5 additions & 3 deletions src/textual/message_pump.py
Original file line number Diff line number Diff line change
Expand Up @@ -522,10 +522,12 @@ async def dispatch_key(self, event: events.Key) -> None:
Args:
event (events.Key): A key event.
"""
key_method = getattr(self, f"key_{event.key_name}", None)
key_method = getattr(self, f"key_{event.key_name}", None) or getattr(
self, f"_key_{event.key_name}", None
)
if key_method is not None:
if await invoke(key_method, event):
event.prevent_default()
await invoke(key_method, event)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sight change in semantics here. If there is a key_ method defined for a given key then it won't attempt to try the base classes.

event.prevent_default()

async def on_timer(self, event: events.Timer) -> None:
event.prevent_default()
Expand Down
10 changes: 5 additions & 5 deletions src/textual/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import rich.repr

from .geometry import Region
from ._types import CallbackType
from .message import Message

Expand Down Expand Up @@ -39,7 +40,7 @@ def can_replace(self, message: Message) -> bool:


@rich.repr.auto
class InvokeLater(Message, verbose=True):
class InvokeLater(Message, verbose=True, bubble=False):
def __init__(self, sender: MessagePump, callback: CallbackType) -> None:
self.callback = callback
super().__init__(sender)
Expand All @@ -48,11 +49,10 @@ def __rich_repr__(self) -> rich.repr.Result:
yield "callback", self.callback


# TODO: This should really be an Event
@rich.repr.auto
class CursorMove(Message):
def __init__(self, sender: MessagePump, line: int) -> None:
self.line = line
class ScrollToRegion(Message, bubble=False):
def __init__(self, sender: MessagePump, region: Region) -> None:
self.region = region
super().__init__(sender)


Expand Down
50 changes: 37 additions & 13 deletions src/textual/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,18 @@ def region(self) -> Region:
except errors.NoWidget:
return Region()

@property
def container_viewport(self) -> Region:
"""The viewport region (parent window)

Returns:
Region: The region that contains this widget.
"""
if self.parent is None:
return self.size.region
assert isinstance(self.parent, Widget)
return self.parent.region

@property
def virtual_region(self) -> Region:
"""The widget region relative to it's container. Which may not be visible,
Expand Down Expand Up @@ -1070,6 +1082,7 @@ def scroll_to_region(
window = self.content_region.at_offset(self.scroll_offset)
if spacing is not None:
window = window.shrink(spacing)
self.log(window=window, region=region)
delta_x, delta_y = Region.get_scroll_to_visible(window, region)
scroll_x, scroll_y = self.scroll_offset
delta = Offset(
Expand All @@ -1080,7 +1093,7 @@ def scroll_to_region(
self.scroll_relative(
delta.x or None,
delta.y or None,
animate=animate,
animate=animate if abs(delta_y) > 1 else False,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't animate movement of a single line.

duration=0.2,
)
return delta
Expand All @@ -1093,13 +1106,21 @@ def scroll_visible(self) -> None:

def __init_subclass__(
cls,
can_focus: bool = False,
can_focus_children: bool = True,
can_focus: bool | None = None,
can_focus_children: bool | None = None,
inherit_css: bool = True,
) -> None:

base = cls.__mro__[0]
super().__init_subclass__(inherit_css=inherit_css)
cls.can_focus = can_focus
cls.can_focus_children = can_focus_children
if issubclass(base, Widget):

cls.can_focus = base.can_focus if can_focus is None else can_focus
cls.can_focus_children = (
base.can_focus_children
if can_focus_children is None
else can_focus_children
)

def __rich_repr__(self) -> rich.repr.Result:
yield "id", self.id, None
Expand Down Expand Up @@ -1529,49 +1550,52 @@ def _on_hide(self, event: events.Hide) -> None:
if self.has_focus:
self.app._reset_focus(self)

def key_home(self) -> bool:
def _on_scroll_to_region(self, message: messages.ScrollToRegion) -> None:
self.scroll_to_region(message.region, animate=True)

def _key_home(self) -> bool:
if self._allow_scroll:
self.scroll_home()
return True
return False

def key_end(self) -> bool:
def _key_end(self) -> bool:
if self._allow_scroll:
self.scroll_end()
return True
return False

def key_left(self) -> bool:
def _key_left(self) -> bool:
if self.allow_horizontal_scroll:
self.scroll_left()
return True
return False

def key_right(self) -> bool:
def _key_right(self) -> bool:
if self.allow_horizontal_scroll:
self.scroll_right()
return True
return False

def key_down(self) -> bool:
def _key_down(self) -> bool:
if self.allow_vertical_scroll:
self.scroll_down()
return True
return False

def key_up(self) -> bool:
def _key_up(self) -> bool:
if self.allow_vertical_scroll:
self.scroll_up()
return True
return False

def key_pagedown(self) -> bool:
def _key_pagedown(self) -> bool:
if self.allow_vertical_scroll:
self.scroll_page_down()
return True
return False

def key_pageup(self) -> bool:
def _key_pageup(self) -> bool:
if self.allow_vertical_scroll:
self.scroll_page_up()
return True
Expand Down
31 changes: 9 additions & 22 deletions src/textual/widgets/_directory_tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,21 +44,6 @@ def __init__(
super().__init__(label, data, name=name, id=id, classes=classes)
self.root.tree.guide_style = "black"

has_focus: Reactive[bool] = Reactive(False)

def on_focus(self) -> None:
self.has_focus = True

def on_blur(self) -> None:
self.has_focus = False

async def watch_hover_node(self, hover_node: NodeID) -> None:
for node in self.nodes.values():
node.tree.guide_style = (
"bold not dim red" if node.id == hover_node else "black"
)
self.refresh()

def render_node(self, node: TreeNode[DirEntry]) -> RenderableType:
return self.render_tree_label(
node,
Expand Down Expand Up @@ -99,36 +84,38 @@ def render_tree_label(
label.stylize("dim")

if is_cursor and has_focus:
label.stylize("reverse")
cursor_style = self.get_component_styles("tree--cursor").rich_style
label.stylize(cursor_style)

icon_label = Text(f"{icon} ", no_wrap=True, overflow="ellipsis") + label
icon_label.apply_meta(meta)
return icon_label

async def on_mount(self, event: events.Mount) -> None:
def on_mount(self) -> None:
self.call_later(self.load_directory, self.root)

async def load_directory(self, node: TreeNode[DirEntry]):
path = node.data.path
directory = sorted(
list(scandir(path)), key=lambda entry: (not entry.is_dir(), entry.name)
)
self.log(directory)
for entry in directory:
await node.add(entry.name, DirEntry(entry.path, entry.is_dir()))
node.add(entry.name, DirEntry(entry.path, entry.is_dir()))
node.loaded = True
await node.expand()
node.expand()
self.refresh(layout=True)

async def on_tree_click(self, message: TreeClick[DirEntry]) -> None:
async def on_tree_control_node_selected(self, message: TreeClick[DirEntry]) -> None:
dir_entry = message.node.data
if not dir_entry.is_dir:
await self.emit(FileClick(self, dir_entry.path))
else:
if not message.node.loaded:
await self.load_directory(message.node)
await message.node.expand()
message.node.expand()
else:
await message.node.toggle()
message.node.toggle()


if __name__ == "__main__":
Expand Down
Loading