mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Merge branch 'main' into bug/1342/inherited-movement-keys
This commit is contained in:
@@ -27,6 +27,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
- Fixed visibility not affecting children https://github.com/Textualize/textual/issues/1313
|
- Fixed visibility not affecting children https://github.com/Textualize/textual/issues/1313
|
||||||
- Fixed issue with auto width/height and relative children https://github.com/Textualize/textual/issues/1319
|
- Fixed issue with auto width/height and relative children https://github.com/Textualize/textual/issues/1319
|
||||||
- Fixed issue with offset applied to containers https://github.com/Textualize/textual/issues/1256
|
- Fixed issue with offset applied to containers https://github.com/Textualize/textual/issues/1256
|
||||||
|
- Fixed default CSS retrieval for widgets with no `DEFAULT_CSS` that inherited from widgets with `DEFAULT_CSS` https://github.com/Textualize/textual/issues/1335
|
||||||
|
- Fixed merging of `BINDINGS` when binding inheritance is set to `None` https://github.com/Textualize/textual/issues/1351
|
||||||
|
|
||||||
## [0.5.0] - 2022-11-20
|
## [0.5.0] - 2022-11-20
|
||||||
|
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ The animation below shows that we can still change colours while the application
|
|||||||
=== "Code"
|
=== "Code"
|
||||||
|
|
||||||
```py hl_lines="40 41 42"
|
```py hl_lines="40 41 42"
|
||||||
--8<-- "docs\blog\snippets\2022-12-07-responsive-app-background-task\nonblocking01.py"
|
--8<-- "docs/blog/snippets/2022-12-07-responsive-app-background-task/nonblocking01.py"
|
||||||
```
|
```
|
||||||
|
|
||||||
1. We create a label that tells the user that we are starting our time-consuming operation.
|
1. We create a label that tells the user that we are starting our time-consuming operation.
|
||||||
|
|||||||
@@ -1996,9 +1996,9 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
await self._prune_nodes(widgets)
|
await self._prune_nodes(widgets)
|
||||||
finally:
|
finally:
|
||||||
finished_event.set()
|
finished_event.set()
|
||||||
|
self.refresh(layout=True)
|
||||||
|
|
||||||
removed_widgets = self._detach_from_dom(widgets)
|
removed_widgets = self._detach_from_dom(widgets)
|
||||||
self.refresh(layout=True)
|
|
||||||
|
|
||||||
finished_event = asyncio.Event()
|
finished_event = asyncio.Event()
|
||||||
asyncio.create_task(prune_widgets_task(removed_widgets, finished_event))
|
asyncio.create_task(prune_widgets_task(removed_widgets, finished_event))
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ class DOMNode(MessagePump):
|
|||||||
if issubclass(base, DOMNode):
|
if issubclass(base, DOMNode):
|
||||||
if not base._inherit_bindings:
|
if not base._inherit_bindings:
|
||||||
bindings.clear()
|
bindings.clear()
|
||||||
bindings.append(Bindings(base.BINDINGS))
|
bindings.append(Bindings(base.__dict__.get("BINDINGS", [])))
|
||||||
keys = {}
|
keys = {}
|
||||||
for bindings_ in bindings:
|
for bindings_ in bindings:
|
||||||
keys.update(bindings_.keys)
|
keys.update(bindings_.keys)
|
||||||
@@ -266,7 +266,7 @@ class DOMNode(MessagePump):
|
|||||||
return f"{base.__name__}"
|
return f"{base.__name__}"
|
||||||
|
|
||||||
for tie_breaker, base in enumerate(self._node_bases):
|
for tie_breaker, base in enumerate(self._node_bases):
|
||||||
css = base.DEFAULT_CSS.strip()
|
css = base.__dict__.get("DEFAULT_CSS", "").strip()
|
||||||
if css:
|
if css:
|
||||||
css_stack.append((get_path(base), css, -tie_breaker))
|
css_stack.append((get_path(base), css, -tie_breaker))
|
||||||
|
|
||||||
|
|||||||
@@ -151,6 +151,12 @@ class Button(Static, can_focus=True):
|
|||||||
"""When buttons are clicked they get the `-active` class for this duration (in seconds)"""
|
"""When buttons are clicked they get the `-active` class for this duration (in seconds)"""
|
||||||
|
|
||||||
class Pressed(Message, bubble=True):
|
class Pressed(Message, bubble=True):
|
||||||
|
"""Event sent when a `Button` is pressed.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
button (Button): The button that was pressed.
|
||||||
|
"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def button(self) -> Button:
|
def button(self) -> Button:
|
||||||
return cast(Button, self.sender)
|
return cast(Button, self.sender)
|
||||||
|
|||||||
@@ -123,7 +123,12 @@ class Checkbox(Widget, can_focus=True):
|
|||||||
self.value = not self.value
|
self.value = not self.value
|
||||||
|
|
||||||
class Changed(Message, bubble=True):
|
class Changed(Message, bubble=True):
|
||||||
"""Checkbox was toggled."""
|
"""Checkbox was toggled.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
value (bool): The value that the checkbox was changed to.
|
||||||
|
input (Checkbox): The `Checkbox` widget that was changed.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, sender: Checkbox, value: bool) -> None:
|
def __init__(self, sender: Checkbox, value: bool) -> None:
|
||||||
super().__init__(sender)
|
super().__init__(sender)
|
||||||
|
|||||||
@@ -314,7 +314,12 @@ class Input(Widget, can_focus=True):
|
|||||||
await self.emit(self.Submitted(self, self.value))
|
await self.emit(self.Submitted(self, self.value))
|
||||||
|
|
||||||
class Changed(Message, bubble=True):
|
class Changed(Message, bubble=True):
|
||||||
"""Value was changed."""
|
"""Value was changed.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
value (str): The value that the input was changed to.
|
||||||
|
input (Input): The `Input` widget that was changed.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, sender: Input, value: str) -> None:
|
def __init__(self, sender: Input, value: str) -> None:
|
||||||
super().__init__(sender)
|
super().__init__(sender)
|
||||||
@@ -322,7 +327,12 @@ class Input(Widget, can_focus=True):
|
|||||||
self.input = sender
|
self.input = sender
|
||||||
|
|
||||||
class Submitted(Message, bubble=True):
|
class Submitted(Message, bubble=True):
|
||||||
"""Value was updated via enter key or blur."""
|
"""Sent when the enter key is pressed within an `Input`.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
value (str): The value of the `Input` being submitted..
|
||||||
|
input (Input): The `Input` widget that is being submitted.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, sender: Input, value: str) -> None:
|
def __init__(self, sender: Input, value: str) -> None:
|
||||||
super().__init__(sender)
|
super().__init__(sender)
|
||||||
|
|||||||
@@ -143,14 +143,22 @@ class ListView(Vertical, can_focus=True, can_focus_children=False):
|
|||||||
return len(self.children)
|
return len(self.children)
|
||||||
|
|
||||||
class Highlighted(Message, bubble=True):
|
class Highlighted(Message, bubble=True):
|
||||||
"""Emitted when the highlighted item changes. Highlighted item is controlled using up/down keys"""
|
"""Emitted when the highlighted item changes. Highlighted item is controlled using up/down keys.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
item (ListItem | None): The highlighted item, if there is one highlighted.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, sender: ListView, item: ListItem | None) -> None:
|
def __init__(self, sender: ListView, item: ListItem | None) -> None:
|
||||||
super().__init__(sender)
|
super().__init__(sender)
|
||||||
self.item = item
|
self.item = item
|
||||||
|
|
||||||
class Selected(Message, bubble=True):
|
class Selected(Message, bubble=True):
|
||||||
"""Emitted when a list item is selected, e.g. when you press the enter key on it"""
|
"""Emitted when a list item is selected, e.g. when you press the enter key on it
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
item (ListItem): The selected item.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, sender: ListView, item: ListItem) -> None:
|
def __init__(self, sender: ListView, item: ListItem) -> None:
|
||||||
super().__init__(sender)
|
super().__init__(sender)
|
||||||
|
|||||||
@@ -232,21 +232,21 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
|
|||||||
color: $text;
|
color: $text;
|
||||||
}
|
}
|
||||||
Tree > .tree--label {
|
Tree > .tree--label {
|
||||||
|
|
||||||
}
|
}
|
||||||
Tree > .tree--guides {
|
Tree > .tree--guides {
|
||||||
color: $success-darken-3;
|
color: $success-darken-3;
|
||||||
}
|
}
|
||||||
|
|
||||||
Tree > .tree--guides-hover {
|
Tree > .tree--guides-hover {
|
||||||
color: $success;
|
color: $success;
|
||||||
text-style: bold;
|
text-style: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
Tree > .tree--guides-selected {
|
Tree > .tree--guides-selected {
|
||||||
color: $warning;
|
color: $warning;
|
||||||
text-style: bold;
|
text-style: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
Tree > .tree--cursor {
|
Tree > .tree--cursor {
|
||||||
background: $secondary;
|
background: $secondary;
|
||||||
@@ -254,11 +254,11 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
|
|||||||
text-style: bold;
|
text-style: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
Tree > .tree--highlight {
|
Tree > .tree--highlight {
|
||||||
text-style: underline;
|
text-style: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
Tree > .tree--highlight-line {
|
Tree > .tree--highlight-line {
|
||||||
background: $boost;
|
background: $boost;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,7 +309,11 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
|
|||||||
}
|
}
|
||||||
|
|
||||||
class NodeSelected(Generic[EventTreeDataType], Message, bubble=True):
|
class NodeSelected(Generic[EventTreeDataType], Message, bubble=True):
|
||||||
"""Event sent when a node is selected."""
|
"""Event sent when a node is selected.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
TreeNode[EventTreeDataType]: The node that was selected.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, sender: MessageTarget, node: TreeNode[EventTreeDataType]
|
self, sender: MessageTarget, node: TreeNode[EventTreeDataType]
|
||||||
@@ -318,7 +322,11 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
|
|||||||
super().__init__(sender)
|
super().__init__(sender)
|
||||||
|
|
||||||
class NodeExpanded(Generic[EventTreeDataType], Message, bubble=True):
|
class NodeExpanded(Generic[EventTreeDataType], Message, bubble=True):
|
||||||
"""Event sent when a node is expanded."""
|
"""Event sent when a node is expanded.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
TreeNode[EventTreeDataType]: The node that was expanded.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, sender: MessageTarget, node: TreeNode[EventTreeDataType]
|
self, sender: MessageTarget, node: TreeNode[EventTreeDataType]
|
||||||
@@ -327,7 +335,11 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
|
|||||||
super().__init__(sender)
|
super().__init__(sender)
|
||||||
|
|
||||||
class NodeCollapsed(Generic[EventTreeDataType], Message, bubble=True):
|
class NodeCollapsed(Generic[EventTreeDataType], Message, bubble=True):
|
||||||
"""Event sent when a node is collapsed."""
|
"""Event sent when a node is collapsed.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
TreeNode[EventTreeDataType]: The node that was collapsed.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, sender: MessageTarget, node: TreeNode[EventTreeDataType]
|
self, sender: MessageTarget, node: TreeNode[EventTreeDataType]
|
||||||
|
|||||||
@@ -45,6 +45,76 @@ def test_validate():
|
|||||||
node.toggle_class("1")
|
node.toggle_class("1")
|
||||||
|
|
||||||
|
|
||||||
|
def test_inherited_bindings():
|
||||||
|
"""Test if binding merging is done correctly when (not) inheriting bindings."""
|
||||||
|
class A(DOMNode):
|
||||||
|
BINDINGS = [("a", "a", "a")]
|
||||||
|
|
||||||
|
class B(A):
|
||||||
|
BINDINGS = [("b", "b", "b")]
|
||||||
|
|
||||||
|
class C(B, inherit_bindings=False):
|
||||||
|
BINDINGS = [("c", "c", "c")]
|
||||||
|
|
||||||
|
class D(C, inherit_bindings=False):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class E(D):
|
||||||
|
BINDINGS = [("e", "e", "e")]
|
||||||
|
|
||||||
|
a = A()
|
||||||
|
assert list(a._bindings.keys.keys()) == ["a"]
|
||||||
|
|
||||||
|
b = B()
|
||||||
|
assert list(b._bindings.keys.keys()) == ["a", "b"]
|
||||||
|
|
||||||
|
c = C()
|
||||||
|
assert list(c._bindings.keys.keys()) == ["c"]
|
||||||
|
|
||||||
|
d = D()
|
||||||
|
assert not list(d._bindings.keys.keys())
|
||||||
|
|
||||||
|
e = E()
|
||||||
|
assert list(e._bindings.keys.keys()) == ["e"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_default_css():
|
||||||
|
class A(DOMNode):
|
||||||
|
pass
|
||||||
|
class B(A):
|
||||||
|
pass
|
||||||
|
class C(B):
|
||||||
|
DEFAULT_CSS = "C"
|
||||||
|
class D(C):
|
||||||
|
pass
|
||||||
|
class E(D):
|
||||||
|
DEFAULT_CSS = "E"
|
||||||
|
node = DOMNode()
|
||||||
|
node_css = node.get_default_css()
|
||||||
|
a = A()
|
||||||
|
a_css = a.get_default_css()
|
||||||
|
b = B()
|
||||||
|
b_css = b.get_default_css()
|
||||||
|
c = C()
|
||||||
|
c_css = c.get_default_css()
|
||||||
|
d = D()
|
||||||
|
d_css = d.get_default_css()
|
||||||
|
e = E()
|
||||||
|
e_css = e.get_default_css()
|
||||||
|
|
||||||
|
# Descendants that don't assign to DEFAULT_CSS don't add new CSS to the stack.
|
||||||
|
assert len(node_css) == len(a_css) == len(b_css) == 0
|
||||||
|
assert len(c_css) == len(d_css) == 1
|
||||||
|
assert len(e_css) == 2
|
||||||
|
|
||||||
|
# Descendants do push the priority of the ancestors' rules down.
|
||||||
|
assert c_css[0][2] == d_css[0][2] + 1 == 0
|
||||||
|
|
||||||
|
# The CSS on the stack is the correct one.
|
||||||
|
assert e_css[0][1:] == ("E", 0)
|
||||||
|
assert e_css[1][1:] == ("C", -2)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def search():
|
def search():
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user