diff --git a/ui/menuutil/menu.go b/ui/menuutil/menu.go index 80e079c..f61a16e 100644 --- a/ui/menuutil/menu.go +++ b/ui/menuutil/menu.go @@ -22,6 +22,7 @@ import ( "github.com/gcla/gowid/widgets/styled" "github.com/gcla/gowid/widgets/text" "github.com/gcla/gowid/widgets/vpadding" + "github.com/gcla/termshark/v2/widgets" "github.com/gcla/termshark/v2/widgets/appkeys" "github.com/gdamore/tcell" ) @@ -186,11 +187,12 @@ func makeMenuWithHotKeys(items []SimpleMenuItem, showKeys bool) (gowid.IWidget, //====================================================================== type NextMenu struct { - Cur *menu.Widget - Next *menu.Widget // nil if menu is nil - Site *menu.SiteWidget - Container gowid.IFocus // container holding menu buttons, etc - Focus int // index of next menu in container + Cur *menu.Widget + Next *menu.Widget // nil if menu is nil + Site *menu.SiteWidget + Container gowid.IFocus // container holding menu buttons, etc + Focus int // index of next menu in container + MenuOpener widgets.IMenuOpener // For integrating with UI app - the menu needs to be told what's underneath when opened } func MakeMenuNavigatingKeyPress(left *NextMenu, right *NextMenu) appkeys.KeyInputFn { @@ -204,15 +206,15 @@ func MenuNavigatingKeyPress(evk *tcell.EventKey, left *NextMenu, right *NextMenu switch evk.Key() { case tcell.KeyLeft: if left != nil { - left.Cur.Close(app) - left.Next.Open(left.Site, app) + left.MenuOpener.CloseMenu(left.Cur, app) + left.MenuOpener.OpenMenu(left.Next, left.Site, app) left.Container.SetFocus(app, left.Focus) // highlight next menu selector res = true } case tcell.KeyRight: if right != nil { - right.Cur.Close(app) - right.Next.Open(right.Site, app) + right.MenuOpener.CloseMenu(right.Cur, app) + right.MenuOpener.OpenMenu(right.Next, right.Site, app) right.Container.SetFocus(app, right.Focus) // highlight next menu selector res = true } diff --git a/ui/streamui.go b/ui/streamui.go index 6151f78..d6d0bea 100644 --- a/ui/streamui.go +++ b/ui/streamui.go @@ -22,6 +22,7 @@ import ( "github.com/gcla/termshark/v2/pcap" "github.com/gcla/termshark/v2/pdmltree" "github.com/gcla/termshark/v2/streams" + "github.com/gcla/termshark/v2/widgets" "github.com/gcla/termshark/v2/widgets/appkeys" "github.com/gcla/termshark/v2/widgets/streamwidget" "github.com/gdamore/tcell" @@ -319,7 +320,7 @@ func (t *streamParseHandler) TrackPayloadPacket(packet int) { func (t *streamParseHandler) OnStreamHeader(hdr streams.FollowHeader) { t.app.Run(gowid.RunFunction(func(app gowid.IApp) { - t.wid.AddHeader(hdr) + t.wid.AddHeader(hdr, app) })) } @@ -439,6 +440,7 @@ func makeStreamWidget(previousFilter string, filter string, cap string, proto st return streamwidget.New(filter, cap, proto, conversationMenu, conversationMenuHolder, &keyState, streamwidget.Options{ + MenuOpener: widgets.MenuOpenerFunc(openTermsharkMenu), DefaultDisplay: func() streamwidget.DisplayFormat { view := streamwidget.Hex choice := termshark.ConfString("main.stream-view", "hex") diff --git a/ui/ui.go b/ui/ui.go index 0d96660..ffc7566 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -131,6 +131,13 @@ var savedListBoxWidgetHolder *holder.Widget var singlePacketViewMsgHolder *holder.Widget // either empty or "loading..." var keyMapper *mapkeys.Widget +type MenuHolder struct { + gowid.IMenuCompatible +} + +var multiMenu *MenuHolder = &MenuHolder{} +var multiMenuWidget *holder.Widget + var tabViewsForward map[gowid.IWidget]gowid.IWidget var tabViewsBackward map[gowid.IWidget]gowid.IWidget @@ -218,6 +225,35 @@ func (g getMappings) None() bool { return len(termshark.LoadKeyMappings()) == 0 } +//====================================================================== + +// openTermsharkMenu implements widgets.IMenuHolder. It is used to integrate widgets +// that need to open menus into the overall application. This is needed because the menu +// widget needs to be rendered first - specifically the screen under the menu - so that +// the menu can read out of the resulting canvas the coordinates on the screen at which +// it should open. +func openTermsharkMenu(open bool, m *menu.Widget, site *menu.SiteWidget, app gowid.IApp) bool { + if open { + if multiMenu.IMenuCompatible != m { + multiMenu.IMenuCompatible = m + m.SetSubWidget(appView, app) + m.Open(site, app) + app.Redraw() + return true + } else { + return false + } + } else { + if multiMenu.IMenuCompatible == m { + m.Close(app) + multiMenu.IMenuCompatible = holder.New(appView) + return true + } else { + return false + } + } +} + func RequestQuit() { select { case QuitRequestedChan <- struct{}{}: @@ -1680,7 +1716,7 @@ func focusOnMenuButton(app gowid.IApp) { func openGeneralMenu(app gowid.IApp) { focusOnMenuButton(app) - generalMenu.Open(openMenuSite, app) + openTermsharkMenu(true, generalMenu, openMenuSite, app) } // Keys for the whole app, applicable whichever view is frontmost @@ -2494,7 +2530,7 @@ func makeRecentMenuWidget() (gowid.IWidget, int) { Txt: s, Key: gowid.MakeKey('a' + rune(i)), CB: func(app gowid.IApp, w gowid.IWidget) { - savedMenu.Close(app) + openTermsharkMenu(false, savedMenu, nil, app) // capFilter global, set up in cmain() RequestLoadPcapWithCheck(scopy, FilterWidget.Value(), NoGlobalJump, app) }, @@ -2722,7 +2758,7 @@ func Build() (*gowid.App, error) { openMenuSite = menu.NewSite(menu.SiteOptions{YOffset: 1}) openMenu.OnClick(gowid.MakeWidgetCallback(gowid.ClickCB{}, func(app gowid.IApp, target gowid.IWidget) { - generalMenu.Open(openMenuSite, app) + openTermsharkMenu(true, generalMenu, openMenuSite, app) })) //====================================================================== @@ -2734,7 +2770,7 @@ func Build() (*gowid.App, error) { Txt: "Refresh Screen", Key: gowid.MakeKeyExt2(0, tcell.KeyCtrlL, ' '), CB: func(app gowid.IApp, w gowid.IWidget) { - generalMenu.Close(app) + openTermsharkMenu(false, generalMenu, nil, app) app.Sync() }, }, @@ -2743,7 +2779,7 @@ func Build() (*gowid.App, error) { Txt: "Toggle Dark Mode", Key: gowid.MakeKey('d'), CB: func(app gowid.IApp, w gowid.IWidget) { - generalMenu.Close(app) + openTermsharkMenu(false, generalMenu, nil, app) DarkMode = !DarkMode termshark.SetConf("main.dark-mode", DarkMode) }, @@ -2753,7 +2789,7 @@ func Build() (*gowid.App, error) { Txt: "Clear Packets", Key: gowid.MakeKeyExt2(0, tcell.KeyCtrlW, ' '), CB: func(app gowid.IApp, w gowid.IWidget) { - generalMenu.Close(app) + openTermsharkMenu(false, generalMenu, nil, app) reallyClear(app) }, }, @@ -2761,7 +2797,7 @@ func Build() (*gowid.App, error) { Txt: "Edit Columns", Key: gowid.MakeKey('e'), CB: func(app gowid.IApp, w gowid.IWidget) { - generalMenu.Close(app) + openTermsharkMenu(false, generalMenu, nil, app) openEditColumns(app) }, }}...) @@ -2771,7 +2807,7 @@ func Build() (*gowid.App, error) { Txt: "Show Log", Key: gowid.MakeKey('l'), CB: func(app gowid.IApp, w gowid.IWidget) { - analysisMenu.Close(app) + openTermsharkMenu(false, analysisMenu, nil, app) openLogsUi(app) }, }) @@ -2783,7 +2819,7 @@ func Build() (*gowid.App, error) { Txt: "Help", Key: gowid.MakeKey('?'), CB: func(app gowid.IApp, w gowid.IWidget) { - generalMenu.Close(app) + openTermsharkMenu(false, generalMenu, nil, app) OpenTemplatedDialog(appView, "UIHelp", app) }, }, @@ -2791,7 +2827,7 @@ func Build() (*gowid.App, error) { Txt: "User Guide", Key: gowid.MakeKey('u'), CB: func(app gowid.IApp, w gowid.IWidget) { - generalMenu.Close(app) + openTermsharkMenu(false, generalMenu, nil, app) if !termshark.RunningRemotely() { termshark.BrowseUrl(termshark.UserGuideURL) } @@ -2802,7 +2838,7 @@ func Build() (*gowid.App, error) { Txt: "FAQ", Key: gowid.MakeKey('f'), CB: func(app gowid.IApp, w gowid.IWidget) { - generalMenu.Close(app) + openTermsharkMenu(false, generalMenu, nil, app) if !termshark.RunningRemotely() { termshark.BrowseUrl(termshark.FAQURL) } @@ -2814,7 +2850,7 @@ func Build() (*gowid.App, error) { Txt: "Found a Bug?", Key: gowid.MakeKey('b'), CB: func(app gowid.IApp, w gowid.IWidget) { - generalMenu.Close(app) + openTermsharkMenu(false, generalMenu, nil, app) if !termshark.RunningRemotely() { termshark.BrowseUrl(termshark.BugURL) } @@ -2825,7 +2861,7 @@ func Build() (*gowid.App, error) { Txt: "Feature Request?", Key: gowid.MakeKey('f'), CB: func(app gowid.IApp, w gowid.IWidget) { - generalMenu.Close(app) + openTermsharkMenu(false, generalMenu, nil, app) if !termshark.RunningRemotely() { termshark.BrowseUrl(termshark.FeatureURL) } @@ -2837,7 +2873,7 @@ func Build() (*gowid.App, error) { Txt: "Quit", Key: gowid.MakeKey('q'), CB: func(app gowid.IApp, w gowid.IWidget) { - generalMenu.Close(app) + openTermsharkMenu(false, generalMenu, nil, app) reallyQuit(app) }, }, @@ -2852,7 +2888,7 @@ func Build() (*gowid.App, error) { Txt: "Toggle Packet Colors", Key: gowid.MakeKey('c'), CB: func(app gowid.IApp, w gowid.IWidget) { - generalMenu.Close(app) + openTermsharkMenu(false, generalMenu, nil, app) PacketColors = !PacketColors termshark.SetConf("main.packet-colors", PacketColors) }, @@ -2900,7 +2936,7 @@ func Build() (*gowid.App, error) { openAnalysisSite = menu.NewSite(menu.SiteOptions{XOffset: -12, YOffset: 1}) openAnalysis.OnClick(gowid.MakeWidgetCallback(gowid.ClickCB{}, func(app gowid.IApp, target gowid.IWidget) { - analysisMenu.Open(openAnalysisSite, app) + openTermsharkMenu(true, analysisMenu, openAnalysisSite, app) })) analysisMenuItems := []menuutil.SimpleMenuItem{ @@ -2908,7 +2944,7 @@ func Build() (*gowid.App, error) { Txt: "Capture file properties", Key: gowid.MakeKey('p'), CB: func(app gowid.IApp, w gowid.IWidget) { - analysisMenu.Close(app) + openTermsharkMenu(false, analysisMenu, nil, app) startCapinfo(app) }, }, @@ -2916,7 +2952,7 @@ func Build() (*gowid.App, error) { Txt: "Reassemble stream", Key: gowid.MakeKey('f'), CB: func(app gowid.IApp, w gowid.IWidget) { - analysisMenu.Close(app) + openTermsharkMenu(false, analysisMenu, nil, app) startStreamReassembly(app) }, }, @@ -2924,7 +2960,7 @@ func Build() (*gowid.App, error) { Txt: "Conversations", Key: gowid.MakeKey('c'), CB: func(app gowid.IApp, w gowid.IWidget) { - analysisMenu.Close(app) + openTermsharkMenu(false, analysisMenu, nil, app) openConvsUi(app) }, }, @@ -3015,6 +3051,7 @@ func Build() (*gowid.App, error) { generalNext.Next = analysisMenu generalNext.Site = openAnalysisSite generalNext.Container = titleCols + generalNext.MenuOpener = widgets.MenuOpenerFunc(openTermsharkMenu) generalNext.Focus = 4 // should really find by ID // <> @@ -3022,6 +3059,7 @@ func Build() (*gowid.App, error) { analysisNext.Next = generalMenu analysisNext.Site = openMenuSite analysisNext.Container = titleCols + analysisNext.MenuOpener = widgets.MenuOpenerFunc(openTermsharkMenu) analysisNext.Focus = 6 // should really find by ID packetListViewHolder = holder.New(nullw) @@ -3041,8 +3079,9 @@ func Build() (*gowid.App, error) { ), ) - FilterWidget = filter.New(filter.Options{ - Completer: savedCompleter{def: termshark.NewFields()}, + FilterWidget = filter.New("filter", filter.Options{ + Completer: savedCompleter{def: termshark.NewFields()}, + MenuOpener: widgets.MenuOpenerFunc(openTermsharkMenu), }) validFilterCb := gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w gowid.IWidget) { @@ -3076,7 +3115,7 @@ func Build() (*gowid.App, error) { ) savedBtnSite := menu.NewSite(menu.SiteOptions{YOffset: 1}) savedw.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w gowid.IWidget) { - savedMenu.Open(savedBtnSite, app) + openTermsharkMenu(true, savedMenu, savedBtnSite, app) })) progWidgetIdx = 7 // adjust this if nullw moves position in filterCols @@ -3492,20 +3531,20 @@ func Build() (*gowid.App, error) { appView = holder.New(mbView) } + // A restriction on the multiMenu is that it only holds one open menu, so using + // this trick, only one menu can be open at a time. + multiMenu.IMenuCompatible = holder.New(appView) + var lastMenu gowid.IWidget = appView menus := []gowid.IMenuCompatible{ - savedMenu, - analysisMenu, - generalMenu, - conversationMenu, + // These menus can both be open at the same time, so I have special + // handling here. I should use a more general method for all menus. The + // current method only allows one menu to be open at a time. filterConvsMenu1, filterConvsMenu2, - colNamesMenu, - colFieldsMenu, + multiMenu, } - menus = append(menus, FilterWidget.Menus()...) - for _, w := range menus { w.SetSubWidget(lastMenu, app) lastMenu = w diff --git a/widgets/filter/filter.go b/widgets/filter/filter.go index 6cc19b1..51595b1 100644 --- a/widgets/filter/filter.go +++ b/widgets/filter/filter.go @@ -2,7 +2,7 @@ // code is governed by the MIT license that can be found in the LICENSE // file. -// Package filter prpvides a termshark-specific edit widget which changes +// Package filter provides a termshark-specific edit widget which changes // color according to the validity of its input, and which activates a // drop-down menu of possible completions for the term at point. package filter @@ -32,6 +32,7 @@ import ( "github.com/gcla/gowid/widgets/styled" "github.com/gcla/gowid/widgets/text" "github.com/gcla/termshark/v2" + "github.com/gcla/termshark/v2/widgets" "github.com/gcla/termshark/v2/widgets/appkeys" "github.com/gdamore/tcell" ) @@ -84,6 +85,7 @@ type SubmitCB struct{} type Options struct { Completer termshark.IPrefixCompleter + MenuOpener widgets.IMenuOpener MaxCompletions int } @@ -107,6 +109,10 @@ func New(opt Options) *Widget { opt.MaxCompletions = 20 } + if opt.MenuOpener == nil { + opt.MenuOpener = widgets.MenuOpenerFunc(widgets.OpenSimpleMenu) + } + menuListBox2 := styled.New( framed.NewUnicode(cellmod.Opaque(filterActivator)), gowid.MakePaletteRef("filter-menu"), @@ -625,9 +631,9 @@ func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid. // be submitted. Then the best UX is to not display the drop down until further input // or cursor movement. if focus.Focus && len(w.completions) > 0 && !*w.temporarilyDisabled { - w.dropDown.Open(w.dropDownSite, app) + w.opts.MenuOpener.OpenMenu(w.dropDown, w.dropDownSite, app) } else { - w.dropDown.Close(app) + w.opts.MenuOpener.CloseMenu(w.dropDown, app) } return w.wrapped.Render(size, focus, app) } diff --git a/widgets/streamwidget/streamwidget.go b/widgets/streamwidget/streamwidget.go index e5c851f..2eb3c1a 100644 --- a/widgets/streamwidget/streamwidget.go +++ b/widgets/streamwidget/streamwidget.go @@ -98,6 +98,7 @@ type Options struct { ChunkClicker IChunkClicked // UI changes to make when stream chunk in table is clicked ErrorHandler IOnError // UI action to take on error CopyModeWidget gowid.IWidget // What to display when copy-mode is started. + MenuOpener widgets.IMenuOpener // For integrating with UI app - the menu needs to be told what's underneath when opened } //====================================================================== @@ -123,7 +124,9 @@ type Widget struct { convMenu *menu.Widget // the menu that opens when you hit the conversation button (entire, client, server) clickActive bool // if true, clicking in stream list will display packet selected keyState *termshark.KeyState // for vim key chords that are intended for table navigation - searchState // track the current highlighted search term + doMenuUpdate bool // Set to true if new data has arrived and the menu needs to be regenerated. Do this + // because if I regenerate each click, I lose the list state which shows the item I last clicked on. + searchState // track the current highlighted search term } func New(displayFilter string, captureDevice string, proto streams.Protocol, @@ -139,6 +142,10 @@ func New(displayFilter string, captureDevice string, proto streams.Protocol, mode = opt.DefaultDisplay() } + if opt.MenuOpener == nil { + opt.MenuOpener = widgets.MenuOpenerFunc(widgets.OpenSimpleMenu) + } + res := &Widget{ opt: opt, displayFilter: displayFilter, @@ -248,6 +255,7 @@ func (w *Widget) updateConvMenuWidget(app gowid.IApp) { w.convMenuHolder.SetSubWidget(convListBox, app) w.setConvButtonText(app) w.setTurnText(app) + w.doMenuUpdate = false } func (w *Widget) makeConvMenuWidget() (gowid.IWidget, int) { @@ -258,7 +266,7 @@ func (w *Widget) makeConvMenuWidget() (gowid.IWidget, int) { Txt: w.getConvButtonText(Entire), Key: gowid.MakeKey('e'), CB: func(app gowid.IApp, w2 gowid.IWidget) { - w.convMenu.Close(app) + w.opt.MenuOpener.CloseMenu(w.convMenu, app) w.selectedConv = Entire w.tableHolder.SetSubWidget(w.viewWidgets[w.selectedConv], app) w.setConvButtonText(app) @@ -273,7 +281,7 @@ func (w *Widget) makeConvMenuWidget() (gowid.IWidget, int) { Txt: w.getConvButtonText(ClientOnly), Key: gowid.MakeKey('c'), CB: func(app gowid.IApp, w2 gowid.IWidget) { - w.convMenu.Close(app) + w.opt.MenuOpener.CloseMenu(w.convMenu, app) w.selectedConv = ClientOnly w.tableHolder.SetSubWidget(w.viewWidgets[w.selectedConv], app) w.setConvButtonText(app) @@ -286,7 +294,7 @@ func (w *Widget) makeConvMenuWidget() (gowid.IWidget, int) { Txt: w.getConvButtonText(ServerOnly), Key: gowid.MakeKey('s'), CB: func(app gowid.IApp, w2 gowid.IWidget) { - w.convMenu.Close(app) + w.opt.MenuOpener.CloseMenu(w.convMenu, app) w.selectedConv = ServerOnly w.tableHolder.SetSubWidget(w.viewWidgets[w.selectedConv], app) w.setConvButtonText(app) @@ -440,7 +448,10 @@ func (w *Widget) construct() { convBtnSite := menu.NewSite(menu.SiteOptions{YOffset: -5}) w.convBtn = button.New(text.New(w.getConvButtonText(Entire))) w.convBtn.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w2 gowid.IWidget) { - w.convMenu.Open(convBtnSite, app) + if w.doMenuUpdate { + w.updateConvMenuWidget(app) + } + w.opt.MenuOpener.OpenMenu(w.convMenu, convBtnSite, app) })) //styledConvBtn := styled.NewInvertedFocus(w.convBtn, gowid.MakePaletteRef("default")) styledConvBtn := styled.NewExt( @@ -450,7 +461,7 @@ func (w *Widget) construct() { ) // After making button - w.updateConvMenuWidget(nil) + w.doMenuUpdate = true convCols := columns.NewFixed(convBtnSite, styledConvBtn) @@ -830,8 +841,9 @@ func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.S return w.IWidget.UserInput(ev, size, focus, app) } -func (w *Widget) AddHeader(hdr streams.FollowHeader) { +func (w *Widget) AddHeader(hdr streams.FollowHeader, app gowid.IApp) { w.streamHeader = hdr + w.doMenuUpdate = true } func (w *Widget) MapChunkToTableRow(chunk int) (int, error) { @@ -888,7 +900,7 @@ func (w *Widget) AddChunkEntire(ch streams.IChunk, app gowid.IApp) { w.updateChunkModel(i, w.displayAs, app) } - w.updateConvMenuWidget(app) + w.doMenuUpdate = true w.data.currentChunk++ } diff --git a/widgets/utils.go b/widgets/utils.go index f815f08..38dbd2f 100644 --- a/widgets/utils.go +++ b/widgets/utils.go @@ -6,6 +6,7 @@ package widgets import ( "github.com/gcla/gowid" + "github.com/gcla/gowid/widgets/menu" "github.com/gdamore/tcell" ) @@ -36,6 +37,34 @@ func SwallowMovementKeys(ev *tcell.EventKey, app gowid.IApp) bool { return res } +//====================================================================== + +type IMenuOpener interface { + OpenMenu(*menu.Widget, *menu.SiteWidget, gowid.IApp) bool + CloseMenu(*menu.Widget, gowid.IApp) +} + +// Return false if it was already open +type MenuOpenerFunc func(bool, *menu.Widget, *menu.SiteWidget, gowid.IApp) bool + +func (m MenuOpenerFunc) OpenMenu(mu *menu.Widget, site *menu.SiteWidget, app gowid.IApp) bool { + return m(true, mu, site, app) +} + +func (m MenuOpenerFunc) CloseMenu(mu *menu.Widget, app gowid.IApp) { + m(false, mu, nil, app) +} + +func OpenSimpleMenu(open bool, mu *menu.Widget, site *menu.SiteWidget, app gowid.IApp) bool { + if open { + mu.Open(site, app) + return true + } else { + mu.Close(app) + return true + } +} + //====================================================================== // Local Variables: // mode: Go