-
Notifications
You must be signed in to change notification settings - Fork 814
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
[Improvement] Add/expose API to update Footer's Bindings #1057
Comments
I wouldn't want to update they keys. Bindings should be relatively static, i.e. not change dynamically. But I agree about the meta-data. For instance, a toggle key that changes its description based on current state. Bindings do update according to the focused widgets. Textual will merge the bindings for the focus widget up through the DOM, to Screen and then App. The footer should update when focus changes. If something isn't working, could you prepare a short demo app for sake of discussion? |
Agreed on the keys/actions/etc being static, and yes, I'm specifically interested in updating the metadata. Things are working as intended from the docs and your description, it's more a matter of a little more flexibility. The code below shows that once you Additionally, it shows how we could use existing things to dynamically update a binding's name. Based on dark/light mode, we could dynamically update the description to reflect the one we'd swap to if pressed. side note: from textual.widgets import Header, Footer, Static, Input
from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.screen import Screen
from textual.containers import Vertical
class DemoInput(Input):
BINDINGS = [Binding("escape", "lose_focus", "Unfocus input")]
async def lose_focus(self) -> None:
self.app.set_focus(None)
class HelpMenu(Screen):
BINDINGS = [("escape", "app.pop_screen", "Exit help")]
def compose(self) -> ComposeResult:
yield Header()
yield Static("Help!")
yield Footer()
class MainMenu(Screen):
BINDINGS = [("question_mark", "app.push_screen('help')", "Help")]
def compose(self) -> ComposeResult:
yield Header()
yield Vertical(Static("Howdy!"), DemoInput(placeholder="Type here"))
yield Footer()
class Demo(App):
SCREENS = {"help": HelpMenu()}
def on_mount(self) -> None:
self.push_screen(MainMenu())
def action_toggle_dark(self) -> None:
self.dark = not self.dark
def watch_dark(self, dark: bool) -> None:
"""Watches the dark bool."""
self.set_class(dark, "-dark-mode")
self.set_class(not dark, "-light-mode")
self.refresh_css()
# Update text to 'Dark mode' when we're in light
# mode and vice versa
choice: str = "Light" if self.dark else "Dark"
self.update_binding("m", description=f"{choice} mode")
def update_binding(
self, key: str, description: str, visibility: bool = True
) -> None:
"""Updates binding metadata."""
# do stuff ...
if __name__ == "__main__":
Demo().run() |
I would love to be able to ESC when on any Widget. In addition, being able to take away focus by clicking elsewhere on the Screen is the normal behaviour of other apps. I have tried the likes of:
On a Widget, with a combination with:
But I think due to the
|
@geekscrapy You can from textual.app import App, ComposeResult
from textual.widgets import Header, Footer, Input
class RemoveFocusApp( App[ None ] ):
BINDINGS = [
( "escape", "unfocus", "Unfocus" ),
]
CSS = """
Screen {
align: center middle;
}
"""
def compose( self ) -> ComposeResult:
yield Header()
yield Input( placeholder="Here is the input" )
yield Footer()
def action_unfocus( self ) -> None:
self.set_focus( None )
if __name__ == "__main__":
RemoveFocusApp().run() |
I think that bindings should be dynamic. Even if it is true that bindings can change with the focus, it is not enough. But not only Bindings should be dynamic, the descriptions should not be only strings, but renderables customizable and with a dynamic style linked to certain states of the binding label. A binding could be a switch, a toggle, a menu, a color palette, a draggable handle, a news ticker with scrolling text, a progress bar, a tab, breadcrumbs, filenames mask, a git branch, a search and replace input field, an undo/redo button, a toogle for Bold or Italic, etc. If you are editing a markdown file and your cursor moves inside a table, an option to add or remove a column (or a row) should appear. All those things should be possible with bindings and made easy to write thanks to the bindings quick syntax used by Textual, one of the best things I ever seen for rapid development. Please give more consideration to this proposal! 🙏 |
I would also appreciate dynamic bindings: #3041 (comment) which comes from #1792 |
Dynamic bindings would be so helpful, i also had a problem with this: #3419 |
Also related: #2006 |
I would also very much appreciate having dynamic bindings. I have a simple screen that can have multiple states and a different set of controls in each state. It makes sense to me to just rewire bindings and not to have multiple screens with identical UI. Right now I'm doing this, but I do realize that I live in a state of sin and deserve whatever happens to me. :) def update_bindings(self, new_bindings: list[tuple[str, str, str]]):
default_bindings = {b.key for b in App.BINDINGS} # type: ignore
# Clear current bindings, preserving the default ones.
for key in list(self._bindings.keys):
if key not in default_bindings:
self._bindings.keys.pop(key)
# Assign new bindings.
for key, action, description in new_bindings:
self.bind(key, action, description=description)
# Notify the footer about binding changes.
self._footer._bindings_changed(None) |
A "blessed" way to do this would be great. A similar workaround that I've discovered for my use-case is to call Full micro-example, hit from textual import events
from textual.app import App
from textual.widgets import Footer, Placeholder
class TestApp(App):
updating = False
def compose(self):
yield Placeholder()
yield Footer()
def action_toggle_updating(self):
self.updating = not self.updating
desc = "Stop updating" if self.updating else "Start updating"
self.bind("s", "app.toggle_updating", description=desc)
self.screen.post_message(events.ScreenResume())
def on_mount(self) -> None:
self.action_toggle_updating()
if __name__ == "__main__":
TestApp().run() |
This is planned. Update coming soon. |
Don't forget to star the repository! Follow @textualizeio for Textual updates. |
Excellent news! How was it fixed? Does calling |
Lets goooo, I have been waiting for this feature for such a long time! |
@willmcgugan how was this fixed? I looked at the related issue and therefore the changelog for 0.72.0, and I find nothing regarding this. |
First off, thank you for textual, it's pretty darn great!
Second, this isn't a bug, nor is it a full feature.
It would be awesome if there were a way to easily update the footer's bindings, specifically their attributes like description/visibility. I was definitely surprised at the lack of control over this particular piece of textual.
I have some hacky workarounds that kind of work and kind of don't. I think asyncio/non-determinism is a part of it, but not all of my issue.
use cases:
Dark mode
when in light mode, and vice versa, tying the modificiation to the normal action/watch?
for a help menu, for instance), and the Input has bindings that don't make sense when it's not focused (ESC
to drop focus). In this instance, the trick of splitting Bindings across Screens doesn't help, as the Input still inherits those that are in the parent Screen.Ideally, we could easily update the footer's visuals to have finer-grained control over the footer's visuals.
The text was updated successfully, but these errors were encountered: