[GH-ISSUE #715] Documenting that app keys are not overridable by widgets #521

Closed
opened 2026-03-04 01:05:42 +03:00 by kerem · 11 comments
Owner

Originally created by @abitrolly on GitHub (Mar 22, 2022).
Original GitHub issue: https://github.com/rivo/tview/issues/715

In CustomKeys wiki page it is not mentioned that keys set by Application.SetInputCapture are always active and can not be overridden by widgets. As discussed in https://github.com/rivo/tview/issues/662#issuecomment-968158591.

EDIT: Here is where the key processing starts in tview.

github.com/rivo/tview@9994674d60/application.go (L309)

Originally created by @abitrolly on GitHub (Mar 22, 2022). Original GitHub issue: https://github.com/rivo/tview/issues/715 In [CustomKeys](https://github.com/rivo/tview/wiki/CustomKeys) wiki page it is not mentioned that keys set by [`Application.SetInputCapture`](https://pkg.go.dev/github.com/rivo/tview#Application.SetInputCapture) are always active and can not be overridden by widgets. As discussed in https://github.com/rivo/tview/issues/662#issuecomment-968158591. **EDIT**: Here is where the key processing starts in `tview`. https://github.com/rivo/tview/blob/9994674d60a85d2c18e2192ef58195fff743091f/application.go#L309
kerem closed this issue 2026-03-04 01:05:42 +03:00
Author
Owner

@abitrolly commented on GitHub (Mar 23, 2022):

It looks like nothing can be overridden by widgets. I add Modal to Pages and set input capture for both of them. When the modal is active, the key is first handled by parent Pages widget and then passed to Modal.

        // 2. modal with detailed info
        infobox := tview.NewModal().
                AddButtons([]string{"Quit", "Cancel"}).
                SetText("Lorem Ipsum")
        // 3. layout with two pages (second page is needed to show modal)
        pager := tview.NewPages().
                AddPage("list", list, Resize, Visible).
                AddPage("infobox", infobox, Resize, Hidden)
        rootpager.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
                // Key is a code
                if event.Key() == tcell.KeyESC {
                        log.Println("ESC pressed in root widget")
                        //app.Stop()
                }
                // Key is a character
                if event.Key() == tcell.KeyRune {
                        if event.Rune() == 'q' {
                                app.Stop()
                        }
                        if event.Rune() == 'i' {
                                rootpager.ShowPage("infobox")
                        }
                        if event.Rune() == 'o' {
                                rootpager.SwitchToPage("list")
                        }
                }
                return event
        })

        infobox.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
                if event.Key() == tcell.KeyESC {
                        log.Println("ESC pressed")
                        //pager.HidePage("infobox")
                        //return nil
                }
                return nil //event
        })

Pressing 'i' to show modal and then Esc gives two messages.

ESC pressed in root widget
                          ESC pressed

This makes it impossible to override shortcuts in a child widget. I expected the keys to be processed by child widgets first. With current behavior the parent widget needs to know about all keys used by child to skip handling them if the child is active.

The defaults override mentioned in CustomKeys work only to change default widget behavior, but not to change the function of keys in the parent widget.

I think that widget compositing vs inheritance is currently not covered.

<!-- gh-comment-id:1076258429 --> @abitrolly commented on GitHub (Mar 23, 2022): It looks like nothing can be overridden by widgets. I add Modal to Pages and set input capture for both of them. When the modal is active, the key is first handled by parent Pages widget and then passed to Modal. ```go // 2. modal with detailed info infobox := tview.NewModal(). AddButtons([]string{"Quit", "Cancel"}). SetText("Lorem Ipsum") // 3. layout with two pages (second page is needed to show modal) pager := tview.NewPages(). AddPage("list", list, Resize, Visible). AddPage("infobox", infobox, Resize, Hidden) ``` ```go rootpager.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { // Key is a code if event.Key() == tcell.KeyESC { log.Println("ESC pressed in root widget") //app.Stop() } // Key is a character if event.Key() == tcell.KeyRune { if event.Rune() == 'q' { app.Stop() } if event.Rune() == 'i' { rootpager.ShowPage("infobox") } if event.Rune() == 'o' { rootpager.SwitchToPage("list") } } return event }) infobox.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { if event.Key() == tcell.KeyESC { log.Println("ESC pressed") //pager.HidePage("infobox") //return nil } return nil //event }) ``` Pressing 'i' to show modal and then `Esc` gives two messages. ``` ESC pressed in root widget ESC pressed ``` This makes it impossible to override shortcuts in a child widget. I expected the keys to be processed by child widgets first. With current behavior the parent widget needs to know about all keys used by child to skip handling them if the child is active. The defaults override mentioned in [CustomKeys](https://github.com/rivo/tview/wiki/CustomKeys) work only to change default widget behavior, but not to change the function of keys in the parent widget. I think that widget compositing vs inheritance is currently not covered.
Author
Owner

@abitrolly commented on GitHub (Mar 23, 2022):

Basically the scenario I expected.

  1. Root widget gets key event
  2. If the key is overridden by focused child, it waits until the child processes the event
  3. If the child returns nil, the root widget skips processing the key
  4. If the child returns event, the root widget processes the event
<!-- gh-comment-id:1076398044 --> @abitrolly commented on GitHub (Mar 23, 2022): Basically the scenario I expected. 1. Root widget gets key event 2. If the key is overridden by focused child, it waits until the child processes the event 3. If the child returns `nil`, the root widget skips processing the key 4. If the child returns event, the root widget processes the event
Author
Owner

@darkhz commented on GitHub (Apr 16, 2022):

For the scenario you described, I think you could make use of the HasFocus() and GetInputCapture() calls appropriately, i.e. you could check if infobox is focused in the rootpager's InputCapture, and process the keyevent appropriately.

For example:

        infobox.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
               if event.Key() == tcell.KeyESC {
                        log.Println("ESC pressed")
                        //pager.HidePage("infobox")
                        //return nil
                }
                return nil //event
        })  

        rootpager.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
               // Key is a code
               // Root widget gets key event
               if event.Key() == tcell.KeyEscape {
                           // If the key is overridden by focused child, it waits until the child processes the event
                           if infobox.HasFocus() {
                              infoEvent := infobox.GetInputCapture()(event)

                              // If the child returns nil, the root widget skips processing the key
                              if infoEvent == nil {
                                    return nil
                              }

                              // If the child returns event, the root widget processes the event
                              //app.Stop()
                              return event
                        }
               }

                // Key is a character
                if event.Key() == tcell.KeyRune {
                        if event.Rune() == 'q' {
                                app.Stop()
                        }
                        if event.Rune() == 'i' {
                                rootpager.ShowPage("infobox")
                        }
                        if event.Rune() == 'o' {
                                rootpager.SwitchToPage("list")
                        }
                }
                return event
        })
<!-- gh-comment-id:1100575349 --> @darkhz commented on GitHub (Apr 16, 2022): For the scenario you described, I think you could make use of the HasFocus() and GetInputCapture() calls appropriately, i.e. you could check if infobox is focused in the rootpager's InputCapture, and process the keyevent appropriately. For example: ```go infobox.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { if event.Key() == tcell.KeyESC { log.Println("ESC pressed") //pager.HidePage("infobox") //return nil } return nil //event }) rootpager.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { // Key is a code // Root widget gets key event if event.Key() == tcell.KeyEscape { // If the key is overridden by focused child, it waits until the child processes the event if infobox.HasFocus() { infoEvent := infobox.GetInputCapture()(event) // If the child returns nil, the root widget skips processing the key if infoEvent == nil { return nil } // If the child returns event, the root widget processes the event //app.Stop() return event } } // Key is a character if event.Key() == tcell.KeyRune { if event.Rune() == 'q' { app.Stop() } if event.Rune() == 'i' { rootpager.ShowPage("infobox") } if event.Rune() == 'o' { rootpager.SwitchToPage("list") } } return event }) ```
Author
Owner

@abitrolly commented on GitHub (Apr 16, 2022):

@darkhz thanks for the snippet. It solves the problem, but the burden is that all parent widgets should be modified to account for all shortcuts in child widgets. It could work if there is a way to forward event to child widget tree without explicitly mentioning them.

<!-- gh-comment-id:1100588950 --> @abitrolly commented on GitHub (Apr 16, 2022): @darkhz thanks for the snippet. It solves the problem, but the burden is that all parent widgets should be modified to account for all shortcuts in child widgets. It could work if there is a way to forward event to child widget tree without explicitly mentioning them.
Author
Owner

@abitrolly commented on GitHub (Jun 4, 2022):

Here is the diagram of how tview currently processes key event. There is no event forwarding beyond first root widget, so it is up to the root widget to forward the event further, manage focus etc. app input doesn't do focus management or any logic for forwarding events between widgets.

flowchart LR
    A("Application.Run()") -->|event| ICSET{a.inputCapture}
    ICSET -- nil --> CtrlC{event}
    CtrlC -- KeyCtrlC --> Q(Quit)

    ICSET --> AICE{"a.inputCapture(event)"}
    AICE --> CtrlC

    CtrlC --> ARIH{a.root.inputHandler && hasFocus}
    ARIH --> ARIHE("a.root.inputHandler(event, setFocus)")
    AICE -- nil --> C(continue)
    ARIH -- nil --> C

    ARIHE --> ARICE("a.root.inputCapture(event, setFocus)")
    
    click A "https://github.com/rivo/tview/blob/9994674d60a85d2c18e2192ef58195fff743091f/application.go#L309" _blank
    classDef link color:blue
    class A link

Markdown source for the diagram.

<!-- gh-comment-id:1146668208 --> @abitrolly commented on GitHub (Jun 4, 2022): Here is the diagram of how `tview` currently processes key event. There is no event forwarding beyond first root widget, so it is up to the root widget to forward the event further, manage focus etc. `app` input doesn't do focus management or any logic for forwarding events between widgets. ```mermaid flowchart LR A("Application.Run()") -->|event| ICSET{a.inputCapture} ICSET -- nil --> CtrlC{event} CtrlC -- KeyCtrlC --> Q(Quit) ICSET --> AICE{"a.inputCapture(event)"} AICE --> CtrlC CtrlC --> ARIH{a.root.inputHandler && hasFocus} ARIH --> ARIHE("a.root.inputHandler(event, setFocus)") AICE -- nil --> C(continue) ARIH -- nil --> C ARIHE --> ARICE("a.root.inputCapture(event, setFocus)") click A "https://github.com/rivo/tview/blob/9994674d60a85d2c18e2192ef58195fff743091f/application.go#L309" _blank classDef link color:blue class A link ``` Markdown [source](https://github.com/yakshaveinc/linux/blob/master/docops/DIAGRAMS.md#tview-shortcuts-processing-httpsgithubcomrivotviewissues715issuecomment-1146668208) for the diagram.
Author
Owner

@abitrolly commented on GitHub (Jun 5, 2022):

Signature of application.inputCapture is different from widget.inputCapture. Widget handlers are passed setFocus handler. Not sure how it is supposed to be used. Looks like widgets don't have a reference to main app object to pass focus to another control, and that there is global SetFocus function.

<!-- gh-comment-id:1146747589 --> @abitrolly commented on GitHub (Jun 5, 2022): Signature of `application.inputCapture` is different from `widget.inputCapture`. Widget handlers are passed `setFocus` handler. Not sure how it is supposed to be used. Looks like widgets don't have a reference to main app object to pass focus to another control, and that there is global `SetFocus` function.
Author
Owner

@abitrolly commented on GitHub (Jun 5, 2022):

Yes, it is a role of root widget to forward keyboard event to the next. So the next widget decides what of its children have focus to react to.

github.com/rivo/tview@9994674d60/grid.go (L266)

github.com/rivo/tview@9994674d60/pages.go (L308-L310)

So it looks like only the widget knows what element holds the focus, and there is no way to track where focus is globally, like from Application. Or do I miss something?

<!-- gh-comment-id:1146752255 --> @abitrolly commented on GitHub (Jun 5, 2022): Yes, it is a role of `root` widget to forward keyboard event to the next. So the next widget decides what of its children have focus to react to. https://github.com/rivo/tview/blob/9994674d60a85d2c18e2192ef58195fff743091f/grid.go#L266 https://github.com/rivo/tview/blob/9994674d60a85d2c18e2192ef58195fff743091f/pages.go#L308-L310 So it looks like only the widget knows what element holds the focus, and there is no way to track where focus is globally, like from Application. Or do I miss something?
Author
Owner

@abitrolly commented on GitHub (Jun 5, 2022):

So for every widget there are two handlers.

  • widget.GetInputCapture - empty by default, set by user as described in CustomKeys
  • widget.InputHandler - implements widget keys

Now the problem is that widget.InputHandler doesn't return anything, so it is impossible to tell if a widget processed the key. So my scenario where event of forwarding unprocessed events to parents doesn't work.

<!-- gh-comment-id:1146804869 --> @abitrolly commented on GitHub (Jun 5, 2022): So for every widget there are two handlers. * `widget.GetInputCapture` - empty by default, set by user as described in [CustomKeys](https://github.com/rivo/tview/wiki/CustomKeys) * `widget.InputHandler` - implements widget keys Now the problem is that `widget.InputHandler` doesn't return anything, so it is impossible to tell if a widget processed the key. So my scenario where event of forwarding unprocessed events to parents doesn't work.
Author
Owner

@abitrolly commented on GitHub (Jun 5, 2022):

Another problem is that app.GetFocus() returns Primitive, which doesn't have GetInputCapture method. So I am blocked with my top-down event processing idea.

github.com/rivo/tview@9994674d60/application.go (L726)

<!-- gh-comment-id:1146812893 --> @abitrolly commented on GitHub (Jun 5, 2022): Another problem is that `app.GetFocus()` returns `Primitive`, which doesn't have `GetInputCapture` method. So I am blocked with my top-down event processing idea. https://github.com/rivo/tview/blob/9994674d60a85d2c18e2192ef58195fff743091f/application.go#L726
Author
Owner

@rivo commented on GitHub (Jun 10, 2022):

The top-down implementation was made based on #421 and I have to agree it should be possible to intercept key presses at any level of the widget hierarchy regardless of what's happening further down.

Going back to the original question, I'm trying to understand your reasoning for giving a key a different function in a higher-up "container" class such as Pages. It looks like you want Esc to close the application but not if the infobox is open. I have a feeling such a context-dependent redefinition of a key can lead to frustrations of the user (e.g. accidentally closing the application) — but let's go with it for now. Why do you process Esc in Pages then instead of in Application?

<!-- gh-comment-id:1152473398 --> @rivo commented on GitHub (Jun 10, 2022): The top-down implementation was made based on #421 and I have to agree it should be possible to intercept key presses at any level of the widget hierarchy regardless of what's happening further down. Going back to the original question, I'm trying to understand your reasoning for giving a key a different function in a higher-up "container" class such as `Pages`. It looks like you want <kbd>Esc</kbd> to close the application but not if the `infobox` is open. I have a feeling such a context-dependent redefinition of a key can lead to frustrations of the user (e.g. accidentally closing the application) — but let's go with it for now. Why do you process <kbd>Esc</kbd> in `Pages` then instead of in `Application`?
Author
Owner

@rivo commented on GitHub (Dec 17, 2022):

@abitrolly Is this issue still relevant?

<!-- gh-comment-id:1356378101 --> @rivo commented on GitHub (Dec 17, 2022): @abitrolly Is this issue still relevant?
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/tview#521
No description provided.