From 851cb7bd330b86935b4a2953dd9bbb243c92002f Mon Sep 17 00:00:00 2001 From: Graham Clark Date: Sun, 21 Feb 2021 19:50:01 -0500 Subject: [PATCH] Consolidation of termshark's menu code I was accumulating menus in the widget hierarchy, so made things simpler. Instead there is now a struct called multiMenu which is essentially a menu holder. Other parts of termshark that want to open a menu are now provided with a IMenuOpener type. This is implemented by the openTermsharkMenu() function. This indirection is needed because the menu being opened has to be provided with the widget rendering the rest of the UI underneath because the menu widget needs to render the underlying widget so that it can inspect the canvas to find the coordinates at which it should open. So the IMenuOpener provides the link between the widget and the application it's opening inside, and so behind the scenes will adjust the menu being opened with the correct lower layer. Note that multiMenu only supports one open menu at a time. The conversations UI allows a cascaded menu to be opened, so I'm doing that the old way for now. --- ui/menuutil/menu.go | 20 +++--- ui/streamui.go | 4 +- ui/ui.go | 99 +++++++++++++++++++--------- widgets/filter/filter.go | 12 +++- widgets/streamwidget/streamwidget.go | 28 +++++--- widgets/utils.go | 29 ++++++++ 6 files changed, 141 insertions(+), 51 deletions(-) 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