[GH-ISSUE #525] keyboard events to primitive with table failed after commit #379

Closed
opened 2026-03-04 01:04:29 +03:00 by kerem · 2 comments
Owner

Originally created by @askovpen on GitHub (Nov 11, 2020).
Original GitHub issue: https://github.com/rivo/tview/issues/525

after commit deb54e1422 i can't get keyboards event on primitive with active table.
before this commit it works fine.

type ModalMenu struct {
        *tview.Box
        table     *tview.Table
        frame     *tview.Frame
        textColor tcell.Color
        title     string
        done      func(buttonIndex int)
        y         int
        width     int
}

// NewModalMenu returns a new modal message window.
func NewModalMenu() *ModalMenu {
        m := &ModalMenu{
                Box:       tview.NewBox(),
                textColor: tview.Styles.PrimaryTextColor,
                y:         1,
                width:     0,
        }
        m.table = tview.NewTable().
                SetSelectable(true, false).
                SetSelectedStyle(tcell.ColorWhite, tcell.ColorNavy, tcell.AttrBold).
                SetSelectedFunc(func(row int, column int) {
                        m.done(row)
                })
        m.frame = tview.NewFrame(m.table).SetBorders(0, 0, 1, 0, 0, 0)
        m.frame.SetBorder(true).
                SetBackgroundColor(tcell.ColorBlack).
                SetBorderPadding(0, 0, 1, 1).SetBorderColor(tcell.ColorRed).SetBorderAttributes(tcell.AttrBold).SetTitleColor(tcell.ColorYellow)
        return m
}

// SetTextColor sets the color of the message text.
func (m *ModalMenu) SetTextColor(color tcell.Color) *ModalMenu {
        m.textColor = color
        return m
}

// SetDoneFunc sets a handler which is called when one of the buttons was
// pressed. It receives the index of the button as well as its label text. The
// handler is also called when the user presses the Escape key. The index will
// then be negative and the label text an emptry string.
func (m *ModalMenu) SetDoneFunc(handler func(buttonIndex int)) *ModalMenu {
        m.done = handler
        return m
}

// SetText sets the message text of the window. The text may contain line
// breaks. Note that words are wrapped, too, based on the final size of the
// window.
func (m *ModalMenu) SetText(text string) *ModalMenu {
        m.title = text
        m.frame.SetTitle(text)
        return m
}

// SetY set Y
func (m *ModalMenu) SetY(y int) *ModalMenu {
        m.y = y
        return m
}

// AddButtons adds buttons to the window. There must be at least one button and
// a "done" handler so the window can be closed again.
func (m *ModalMenu) AddButtons(labels []string) *ModalMenu {
        for index, label := range labels {
                func(i int, l string) {
                        //m.list.AddItem(label,"",0,func() {m.done(i,l)})
                        m.table.SetCell(i, 0, tview.NewTableCell(label).SetTextColor(tcell.ColorSilver))
                        if m.width < len(label) {
                                m.width = len(label)
                        }
                }(index, label)
        }
        return m
}

// Focus is called when this primitive receives focus.
func (m *ModalMenu) Focus(delegate func(p tview.Primitive)) {
        //delegate(m.form)
        delegate(m.table)
}

// HasFocus returns whether or not this primitive has focus.
func (m *ModalMenu) HasFocus() bool {
        //return m.form.HasFocus()
        return m.table.HasFocus()
}

// Draw draws this primitive onto the screen.
func (m *ModalMenu) Draw(screen tcell.Screen) {
        height := m.table.GetRowCount() + 2
        width := m.width + 4
        if len(m.title) > width-2 {
                width = len(m.title) + 2
        }
        m.frame.Clear()
        x := 1
        y := m.y
        m.SetRect(x, y, width, height)

        // Draw the frame.
        m.frame.SetRect(x, y, width, height)
        m.frame.Draw(screen)
}

Originally created by @askovpen on GitHub (Nov 11, 2020). Original GitHub issue: https://github.com/rivo/tview/issues/525 after commit deb54e1422f84a9e8489ae2ff8889b8077ff8ba0 i can't get keyboards event on primitive with active table. before this commit it works fine. ```go type ModalMenu struct { *tview.Box table *tview.Table frame *tview.Frame textColor tcell.Color title string done func(buttonIndex int) y int width int } // NewModalMenu returns a new modal message window. func NewModalMenu() *ModalMenu { m := &ModalMenu{ Box: tview.NewBox(), textColor: tview.Styles.PrimaryTextColor, y: 1, width: 0, } m.table = tview.NewTable(). SetSelectable(true, false). SetSelectedStyle(tcell.ColorWhite, tcell.ColorNavy, tcell.AttrBold). SetSelectedFunc(func(row int, column int) { m.done(row) }) m.frame = tview.NewFrame(m.table).SetBorders(0, 0, 1, 0, 0, 0) m.frame.SetBorder(true). SetBackgroundColor(tcell.ColorBlack). SetBorderPadding(0, 0, 1, 1).SetBorderColor(tcell.ColorRed).SetBorderAttributes(tcell.AttrBold).SetTitleColor(tcell.ColorYellow) return m } // SetTextColor sets the color of the message text. func (m *ModalMenu) SetTextColor(color tcell.Color) *ModalMenu { m.textColor = color return m } // SetDoneFunc sets a handler which is called when one of the buttons was // pressed. It receives the index of the button as well as its label text. The // handler is also called when the user presses the Escape key. The index will // then be negative and the label text an emptry string. func (m *ModalMenu) SetDoneFunc(handler func(buttonIndex int)) *ModalMenu { m.done = handler return m } // SetText sets the message text of the window. The text may contain line // breaks. Note that words are wrapped, too, based on the final size of the // window. func (m *ModalMenu) SetText(text string) *ModalMenu { m.title = text m.frame.SetTitle(text) return m } // SetY set Y func (m *ModalMenu) SetY(y int) *ModalMenu { m.y = y return m } // AddButtons adds buttons to the window. There must be at least one button and // a "done" handler so the window can be closed again. func (m *ModalMenu) AddButtons(labels []string) *ModalMenu { for index, label := range labels { func(i int, l string) { //m.list.AddItem(label,"",0,func() {m.done(i,l)}) m.table.SetCell(i, 0, tview.NewTableCell(label).SetTextColor(tcell.ColorSilver)) if m.width < len(label) { m.width = len(label) } }(index, label) } return m } // Focus is called when this primitive receives focus. func (m *ModalMenu) Focus(delegate func(p tview.Primitive)) { //delegate(m.form) delegate(m.table) } // HasFocus returns whether or not this primitive has focus. func (m *ModalMenu) HasFocus() bool { //return m.form.HasFocus() return m.table.HasFocus() } // Draw draws this primitive onto the screen. func (m *ModalMenu) Draw(screen tcell.Screen) { height := m.table.GetRowCount() + 2 width := m.width + 4 if len(m.title) > width-2 { width = len(m.title) + 2 } m.frame.Clear() x := 1 y := m.y m.SetRect(x, y, width, height) // Draw the frame. m.frame.SetRect(x, y, width, height) m.frame.Draw(screen) } ```
kerem closed this issue 2026-03-04 01:04:29 +03:00
Author
Owner

@rivo commented on GitHub (Nov 17, 2020):

The internal handling of key events has changed slightly. They are now passed top-down instead of directly to the element that has focus. This also means that every Primitive must implement the InputHandler() function (and it should also implement the MouseHandler() function). I've pasted a modification of your code that should work.

However, I should note that for a simple modal menu like this, I would not suggest writing your own Primitive as you did. Writing one's own primitive is complex as you can see. And it requires you to use some "internal" functions and types (used only for custom primitives) that I cannot promise will always be backwards compatible.

Please read to the following wiki entry:

https://github.com/rivo/tview/wiki/Primitives

For your specific example, it should be much easier to instantiate a Table directly and then use its available functions to achieve your goal. If you need a primitive that contains multiple other primitives, you can use Flex or Grid.

Anyway, here's the code. Please update to the latest tview version to make it work.

package main

import (
	"github.com/gdamore/tcell/v2"
	"github.com/rivo/tview"
)

type ModalMenu struct {
	*tview.Box
	table     *tview.Table
	frame     *tview.Frame
	textColor tcell.Color
	title     string
	done      func(buttonIndex int)
	y         int
	width     int
}

// NewModalMenu returns a new modal message window.
func NewModalMenu() *ModalMenu {
	m := &ModalMenu{
		Box:       tview.NewBox(),
		textColor: tview.Styles.PrimaryTextColor,
		y:         1,
		width:     0,
	}
	m.table = tview.NewTable().
		SetSelectable(true, false).
		SetSelectedStyle(tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorNavy).Attributes(tcell.AttrBold)).
		SetSelectedFunc(func(row int, column int) {
			m.done(row)
		})
	m.frame = tview.NewFrame(m.table).SetBorders(0, 0, 1, 0, 0, 0)
	m.frame.SetBorder(true).
		SetBackgroundColor(tcell.ColorBlack).
		SetBorderPadding(0, 0, 1, 1).SetBorderColor(tcell.ColorRed).SetBorderAttributes(tcell.AttrBold).SetTitleColor(tcell.ColorYellow)
	return m
}

// SetTextColor sets the color of the message text.
func (m *ModalMenu) SetTextColor(color tcell.Color) *ModalMenu {
	m.textColor = color
	return m
}

// SetDoneFunc sets a handler which is called when one of the buttons was
// pressed. It receives the index of the button as well as its label text. The
// handler is also called when the user presses the Escape key. The index will
// then be negative and the label text an emptry string.
func (m *ModalMenu) SetDoneFunc(handler func(buttonIndex int)) *ModalMenu {
	m.done = handler
	return m
}

// SetText sets the message text of the window. The text may contain line
// breaks. Note that words are wrapped, too, based on the final size of the
// window.
func (m *ModalMenu) SetText(text string) *ModalMenu {
	m.title = text
	m.frame.SetTitle(text)
	return m
}

// SetY set Y
func (m *ModalMenu) SetY(y int) *ModalMenu {
	m.y = y
	return m
}

// AddButtons adds buttons to the window. There must be at least one button and
// a "done" handler so the window can be closed again.
func (m *ModalMenu) AddButtons(labels []string) *ModalMenu {
	for index, label := range labels {
		func(i int, l string) {
			//m.list.AddItem(label,"",0,func() {m.done(i,l)})
			m.table.SetCell(i, 0, tview.NewTableCell(label).SetTextColor(tcell.ColorSilver))
			if m.width < len(label) {
				m.width = len(label)
			}
		}(index, label)
	}
	return m
}

// Focus is called when this primitive receives focus.
func (m *ModalMenu) Focus(delegate func(p tview.Primitive)) {
	//delegate(m.form)
	delegate(m.table)
}

// HasFocus returns whether or not this primitive has focus.
func (m *ModalMenu) HasFocus() bool {
	//return m.form.HasFocus()
	return m.table.HasFocus()
}

// Draw draws this primitive onto the screen.
func (m *ModalMenu) Draw(screen tcell.Screen) {
	height := m.table.GetRowCount() + 2
	width := m.width + 4
	if len(m.title) > width-2 {
		width = len(m.title) + 2
	}
	m.frame.Clear()
	x := 1
	y := m.y
	m.SetRect(x, y, width, height)

	// Draw the frame.
	m.frame.SetRect(x, y, width, height)
	m.frame.Draw(screen)
}

// InputHandler returns the handler for this primitive.
func (m *ModalMenu) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
	return m.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
		if m.table.HasFocus() {
			if handler := m.table.InputHandler(); handler != nil {
				handler(event, setFocus)
			}
			return
		}
	})
}

// MouseHandler returns the mouse handler for this primitive.
func (m *ModalMenu) MouseHandler() func(action tview.MouseAction, event *tcell.EventMouse, setFocus func(p tview.Primitive)) (consumed bool, capture tview.Primitive) {
	return m.WrapMouseHandler(func(action tview.MouseAction, event *tcell.EventMouse, setFocus func(p tview.Primitive)) (consumed bool, capture tview.Primitive) {
		if !m.InRect(event.Position()) {
			return false, nil
		}

		consumed, capture = m.table.MouseHandler()(action, event, setFocus)
		if consumed {
			return
		}

		return
	})
}

func main() {
	app := tview.NewApplication()
	m := NewModalMenu()
	m.done = func(buttonIndex int) {
		app.Stop()
	}
	m.AddButtons([]string{"test1", "test2", "test3"})
	if err := app.SetRoot(m, true).EnableMouse(true).Run(); err != nil {
		panic(err)
	}
}
<!-- gh-comment-id:729127635 --> @rivo commented on GitHub (Nov 17, 2020): The internal handling of key events has changed slightly. They are now passed top-down instead of directly to the element that has focus. This also means that every Primitive must implement the `InputHandler()` function (and it should also implement the `MouseHandler()` function). I've pasted a modification of your code that should work. However, I should note that for a simple modal menu like this, I would not suggest writing your own Primitive as you did. Writing one's own primitive is complex as you can see. And it requires you to use some "internal" functions and types (used only for custom primitives) that I cannot promise will always be backwards compatible. Please read to the following wiki entry: https://github.com/rivo/tview/wiki/Primitives For your specific example, it should be much easier to instantiate a `Table` directly and then use its available functions to achieve your goal. If you need a primitive that contains multiple other primitives, you can use `Flex` or `Grid`. Anyway, here's the code. Please update to the latest `tview` version to make it work. ```go package main import ( "github.com/gdamore/tcell/v2" "github.com/rivo/tview" ) type ModalMenu struct { *tview.Box table *tview.Table frame *tview.Frame textColor tcell.Color title string done func(buttonIndex int) y int width int } // NewModalMenu returns a new modal message window. func NewModalMenu() *ModalMenu { m := &ModalMenu{ Box: tview.NewBox(), textColor: tview.Styles.PrimaryTextColor, y: 1, width: 0, } m.table = tview.NewTable(). SetSelectable(true, false). SetSelectedStyle(tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorNavy).Attributes(tcell.AttrBold)). SetSelectedFunc(func(row int, column int) { m.done(row) }) m.frame = tview.NewFrame(m.table).SetBorders(0, 0, 1, 0, 0, 0) m.frame.SetBorder(true). SetBackgroundColor(tcell.ColorBlack). SetBorderPadding(0, 0, 1, 1).SetBorderColor(tcell.ColorRed).SetBorderAttributes(tcell.AttrBold).SetTitleColor(tcell.ColorYellow) return m } // SetTextColor sets the color of the message text. func (m *ModalMenu) SetTextColor(color tcell.Color) *ModalMenu { m.textColor = color return m } // SetDoneFunc sets a handler which is called when one of the buttons was // pressed. It receives the index of the button as well as its label text. The // handler is also called when the user presses the Escape key. The index will // then be negative and the label text an emptry string. func (m *ModalMenu) SetDoneFunc(handler func(buttonIndex int)) *ModalMenu { m.done = handler return m } // SetText sets the message text of the window. The text may contain line // breaks. Note that words are wrapped, too, based on the final size of the // window. func (m *ModalMenu) SetText(text string) *ModalMenu { m.title = text m.frame.SetTitle(text) return m } // SetY set Y func (m *ModalMenu) SetY(y int) *ModalMenu { m.y = y return m } // AddButtons adds buttons to the window. There must be at least one button and // a "done" handler so the window can be closed again. func (m *ModalMenu) AddButtons(labels []string) *ModalMenu { for index, label := range labels { func(i int, l string) { //m.list.AddItem(label,"",0,func() {m.done(i,l)}) m.table.SetCell(i, 0, tview.NewTableCell(label).SetTextColor(tcell.ColorSilver)) if m.width < len(label) { m.width = len(label) } }(index, label) } return m } // Focus is called when this primitive receives focus. func (m *ModalMenu) Focus(delegate func(p tview.Primitive)) { //delegate(m.form) delegate(m.table) } // HasFocus returns whether or not this primitive has focus. func (m *ModalMenu) HasFocus() bool { //return m.form.HasFocus() return m.table.HasFocus() } // Draw draws this primitive onto the screen. func (m *ModalMenu) Draw(screen tcell.Screen) { height := m.table.GetRowCount() + 2 width := m.width + 4 if len(m.title) > width-2 { width = len(m.title) + 2 } m.frame.Clear() x := 1 y := m.y m.SetRect(x, y, width, height) // Draw the frame. m.frame.SetRect(x, y, width, height) m.frame.Draw(screen) } // InputHandler returns the handler for this primitive. func (m *ModalMenu) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { return m.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { if m.table.HasFocus() { if handler := m.table.InputHandler(); handler != nil { handler(event, setFocus) } return } }) } // MouseHandler returns the mouse handler for this primitive. func (m *ModalMenu) MouseHandler() func(action tview.MouseAction, event *tcell.EventMouse, setFocus func(p tview.Primitive)) (consumed bool, capture tview.Primitive) { return m.WrapMouseHandler(func(action tview.MouseAction, event *tcell.EventMouse, setFocus func(p tview.Primitive)) (consumed bool, capture tview.Primitive) { if !m.InRect(event.Position()) { return false, nil } consumed, capture = m.table.MouseHandler()(action, event, setFocus) if consumed { return } return }) } func main() { app := tview.NewApplication() m := NewModalMenu() m.done = func(buttonIndex int) { app.Stop() } m.AddButtons([]string{"test1", "test2", "test3"}) if err := app.SetRoot(m, true).EnableMouse(true).Run(); err != nil { panic(err) } } ```
Author
Owner

@askovpen commented on GitHub (Nov 20, 2020):

this example work, but not work in my project. how to debug, who break inputhandler event?

<!-- gh-comment-id:731384439 --> @askovpen commented on GitHub (Nov 20, 2020): this example work, but not work in my project. how to debug, who break inputhandler event?
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#379
No description provided.