func (p *psmlColumnsModel) CellWidgets(row table.RowId) []gowid.IWidget {
	rowi := int(row)
	res := make([]gowid.IWidget, 0)
	pad := func(w gowid.IWidget, pos gowid.IHAlignment, r int, fn func(int) gowid.WidgetChangedFunction) gowid.IWidget {
		btn := button.NewAlt(w)
		btn.OnClick(gowid.MakeWidgetCallback(gowid.ClickCB{}, fn(r)))
		return hpadding.New(
			clicktracker.New(
				styled.NewExt(
					btn,
					gowid.MakePaletteRef("dialog"),
					gowid.MakePaletteRef("dialog-button"),
				),
			),
			pos,
			fixed,
		)
	}
	colsMenuFieldsSite := menu.NewSite(menu.SiteOptions{YOffset: 1})
	colsMenuFieldsButton := button.NewBare(text.New(p.spec[rowi].Field.Token))
	colsMenuFieldsButton.OnClick(gowid.MakeWidgetCallback(gowid.ClickCB{}, func(app gowid.IApp, target gowid.IWidget) {
		wid, hei := rebuildPsmlFieldListBox(app)
		colsCurrentModelRow = rowi
		colFieldsMenu.SetWidth(units(wid), app)
		colFieldsMenu.SetHeight(units(hei), app)
		multiMenu1Opener.OpenMenu(colFieldsMenu, colsMenuFieldsSite, app)
	}))
	if rowi == 0 {
		res = append(res, nullw)
	} else {
		res = append(res, pad(text.New("^"), left, rowi, func(r int) gowid.WidgetChangedFunction {
			return func(app gowid.IApp, target gowid.IWidget) {
				p.moveUp(r, app)
			}
		}))
	}
	if rowi == len(p.spec)-1 {
		res = append(res, nullw)
	} else {
		res = append(res, pad(text.New("v"), left, rowi, func(r int) gowid.WidgetChangedFunction {
			return func(app gowid.IApp, target gowid.IWidget) {
				p.moveDown(r, app)
			}
		}))
	}
	res = append(res, hpadding.New(
		clicktracker.New(
			styled.NewExt(
				p.widgets[row].visible,
				gowid.MakePaletteRef("dialog"),
				gowid.MakePaletteRef("dialog-button"),
			),
		),
		left,
		fixed,
	))
	fcols := columns.New([]gowid.IContainerWidget{
		&gowid.ContainerWidget{
			IWidget: colsMenuFieldsSite,
			D:       fixed,
		},
		&gowid.ContainerWidget{
			IWidget: clicktracker.New(
				styled.NewExt(
					colsMenuFieldsButton,
					gowid.MakePaletteRef("button"),
					gowid.MakePaletteRef("button-focus"),
				),
			),
			D: fixed,
		},
	})
	res = append(res, fcols)
	if p.haveCustom {
		if p.spec[row].Field.Token == "%Cus" {
			res = append(res, p.widgets[row].customFilter)
			res = append(res, hpadding.New(p.widgets[row].occurrence, mid, fixed))
		} else {
			res = append(res, nullw)
			res = append(res, nullw)
		}
	}
	res = append(res, p.widgets[row].customName)
	colsMenuSite := menu.NewSite(menu.SiteOptions{YOffset: 1})
	colsMenuButton := button.NewBare(text.New(shark.AllowedColumnFormats[p.spec[row].Field.Token].Long))
	colsMenuButton.OnClick(gowid.MakeWidgetCallback(gowid.ClickCB{}, func(app gowid.IApp, target gowid.IWidget) {
		wid, hei := rebuildPsmlNamesListBox(p, app)
		colsCurrentModelRow = rowi
		colNamesMenu.SetWidth(units(wid), app)
		colNamesMenu.SetHeight(units(hei), app)
		multiMenu1Opener.OpenMenu(colNamesMenu, colsMenuSite, app)
	}))
	res = append(res,
		columns.NewFixed(
			colsMenuSite,
			clicktracker.New(
				styled.NewExt(
					colsMenuButton,
					gowid.MakePaletteRef("button"),
					gowid.MakePaletteRef("button-focus"),
				),
			),
		),
	)
	if len(p.spec) <= 1 {
		res = append(res, nullw)
	} else {
		res = append(res, pad(text.New("X"), mid, rowi, func(r int) gowid.WidgetChangedFunction {
			return func(app gowid.IApp, target gowid.IWidget) {
				p.deleteRow(row, app)
			}
		}))
	}
	return res
}
construct the widgets for each row in the dialog used to configure PSML
columns.
package ui
import (
	"fmt"
	"github.com/gcla/gowid"
	"github.com/gcla/gowid/gwutil"
	"github.com/gcla/gowid/widgets/button"
	"github.com/gcla/gowid/widgets/checkbox"
	"github.com/gcla/gowid/widgets/clicktracker"
	"github.com/gcla/gowid/widgets/columns"
	"github.com/gcla/gowid/widgets/edit"
	"github.com/gcla/gowid/widgets/fill"
	"github.com/gcla/gowid/widgets/hpadding"
	"github.com/gcla/gowid/widgets/menu"
	"github.com/gcla/gowid/widgets/styled"
	"github.com/gcla/gowid/widgets/table"
	"github.com/gcla/gowid/widgets/text"
	"github.com/gcla/gowid/widgets/vpadding"
	"github.com/gcla/termshark/v2/shark"
	"github.com/gcla/termshark/v2/shark/wiresharkcfg"
	"github.com/gcla/termshark/v2/widgets/filter"
	"github.com/gcla/termshark/v2/widgets/number"
	log "github.com/sirupsen/logrus"
)
var ColumnsFormatError = fmt.Errorf("The supplied list of columns and names is invalid")
var filler *fill.Widget
type psmlDialogWidgets struct {
	customName   *edit.Widget  
	customFilter *filter.Widget
	occurrence   *number.Widget
	visible      *checkbox.Widget  
}
type psmlColumnsModel struct {
	spec       []shark.PsmlColumnSpec  
	widgets    []psmlDialogWidgets
	haveCustom bool
	*table.Widget
}
var _ table.IBoundedModel = (*psmlColumnsModel)(nil)
var _ table.IInvertible = (*psmlColumnsModel)(nil)
var (
	left  = gowid.HAlignLeft{}
	mid   = gowid.HAlignMiddle{}
	right = gowid.HAlignRight{}
)
func init() {
	filler = fill.New(' ')
}
func NewDefaultPsmlColumnsModel(app gowid.IApp) *psmlColumnsModel {
	spec := shark.DefaultPsmlColumnSpec
	specCopy := make([]shark.PsmlColumnSpec, len(spec))
	for i := 0; i < len(spec); i++ {
		specCopy[i] = spec[i]
	}
	res := &psmlColumnsModel{
		spec: specCopy,
	}
	res.fixup(app)
	return res
}
func NewPsmlColumnsModel(app gowid.IApp) *psmlColumnsModel {
	spec := shark.GetPsmlColumnFormat()
	res := &psmlColumnsModel{
		spec: spec,
	}
	res.fixup(app)
	return res
}
func NewPsmlColumnsModelFrom(colsKey string, app gowid.IApp) *psmlColumnsModel {
	spec := shark.GetPsmlColumnFormatFrom(colsKey)
	res := &psmlColumnsModel{
		spec: spec,
	}
	res.fixup(app)
	return res
}
func (p *psmlColumnsModel) Close() error {
	var err error
	for i := 0; i < len(p.widgets); i++ {
		if p.widgets[i].customFilter != nil {
			err2 := p.widgets[i].customFilter.Close()
			if err == nil {
				err = err2
			}
		}
	}
	return err
}
func (p *psmlColumnsModel) String() string {
	return fmt.Sprintf("%v", p.spec)
}
func (p *psmlColumnsModel) FieldToString(i int) string {
	field := p.spec[i].Field.Token
	if field == "%Cus" {
		field = fmt.Sprintf("%s:%s:%d:R",
			field,
			p.widgets[i].customFilter.Value(),
			p.widgets[i].occurrence.Value,
		)
	}
	return field
}
func (p *psmlColumnsModel) ToConfigList() []string {
	res := make([]string, 0, len(p.spec))
	for i := 0; i < len(p.spec); i++ {
		res = append(res, p.FieldToString(i))
		res = append(res, p.widgets[i].customName.Text())
		res = append(res, fmt.Sprintf("%v", p.widgets[i].visible.IsChecked()))
	}
	return res
}
type specToWidgets shark.PsmlColumnSpec
func (sp *specToWidgets) widgets() psmlDialogWidgets {
	return psmlDialogWidgets{
		customName: edit.New(edit.Options{
			Text: sp.Name,
		}),
		customFilter: filter.New("psmlfilter", filter.Options{
			Completer:  savedCompleter{def: FieldCompleter},
			MenuOpener: &multiMenu1Opener,
			Position:   filter.Below,
		}),
		visible: checkbox.New(!sp.Hidden),
		occurrence: number.New(number.Options{
			Value: sp.Field.Occurrence,
			Min:   gwutil.SomeInt(0),
			Styler: func(w gowid.IWidget) gowid.IWidget {
				return styled.NewInvertedFocus(w, gowid.MakePaletteRef("dialog"))
			},
		}),
	}
}
func (p *psmlColumnsModel) AddRow() {
	p.spec = append(p.spec, shark.PsmlColumnSpec{Field: shark.PsmlField{Token: "%m"}, Name: "No."})
	sp := specToWidgets(p.spec[len(p.spec)-1])
	w := sp.widgets()
	p.widgets = append(p.widgets, w)
	if sp.Field.Token == "%Cus" {
		p.haveCustom = true
	}
	p.Widget = table.New(p)
}
func (p *psmlColumnsModel) cacheHaveCustom() {
	for _, w := range p.spec {
		if w.Field.Token == "%Cus" {
			p.haveCustom = true
			return
		}
	}
	p.haveCustom = false
}
func (p *psmlColumnsModel) fixup(app gowid.IApp) {
	p.haveCustom = false
	p.widgets = make([]psmlDialogWidgets, 0)
	for i := 0; i < len(p.spec); i++ {
		sp := specToWidgets(p.spec[i])
		w := sp.widgets()
		if p.spec[i].Field.Token == "%Cus" {
			p.haveCustom = true
			w.customFilter.SetValue(p.spec[i].Field.Filter, app)
		}
		p.widgets = append(p.widgets, w)
	}
	p.Widget = table.New(p)
}
func stripQuotes(s string) string {
	if len(s) > 0 && s[0] == '"' {
		s = s[1:]
	}
	if len(s) > 0 && s[len(s)-1] == '"' {
		s = s[:len(s)-1]
	}
	return s
}
func (p *psmlColumnsModel) ReadFromWireshark(app gowid.IApp) error {
	wcfg, err := wiresharkcfg.NewDefault()
	if err != nil {
		return err
	}
	wcols := wcfg.ColumnFormat()
	if wcols == nil {
		return fmt.Errorf("Could not read Wireshark column preferences")
	}
	if (len(wcols)/2)*2 != len(wcols) {
		return gowid.WithKVs(ColumnsFormatError, map[string]interface{}{
			"columns": wcols,
		})
	}
	specs := make([]shark.PsmlColumnSpec, 0)
	for i := 0; i < len(wcols); i += 2 {
		var field shark.PsmlField
		err := field.FromString(stripQuotes(wcols[i+1]))
		if err != nil {
			return err
		}
		specs = append(specs, shark.PsmlColumnSpec{
			Field: field,
			Name:  stripQuotes(wcols[i]),
		})
	}
	p.spec = specs
	p.fixup(app)
	return nil
}
func (p *psmlColumnsModel) UpdateFromField(field string, idx int, app gowid.IApp) {
	if p.spec[idx].Field.Token == "%Cus" && field != "%Cus" {
		err := p.widgets[idx].customFilter.Close()
		if err != nil {
			log.Warnf("Unexpected response when closing filter: %v", err)
		}
	}
	p.spec[idx].Field.Token = field
	p.spec[idx].Name = shark.AllowedColumnFormats[field].Long
	p.cacheHaveCustom()
	p.Widget = table.New(p)
}
func (m *psmlColumnsModel) moveDown(row int, app gowid.IApp) {
	i := row
	j := row + 1
	m.spec[i], m.spec[j] = m.spec[j], m.spec[i]
	m.widgets[i], m.widgets[j] = m.widgets[j], m.widgets[i]
	m.Widget = table.New(m)
}
func (m *psmlColumnsModel) moveUp(row int, app gowid.IApp) {
	i := row
	j := row - 1
	m.spec[i], m.spec[j] = m.spec[j], m.spec[i]
	m.widgets[i], m.widgets[j] = m.widgets[j], m.widgets[i]
	m.Widget = table.New(m)
}
func (m *psmlColumnsModel) deleteRow(trow table.RowId, app gowid.IApp) {
	row := int(trow)
	err := m.widgets[row].customFilter.Close()
	if err != nil {
		log.Warnf("Unexpected response when closing filter: %v", err)
	}
	m.spec = append(m.spec[:row], m.spec[row+1:]...)
	m.widgets = append(m.widgets[:row], m.widgets[row+1:]...)
	m.cacheHaveCustom()
	m.Widget = table.New(m)
}