Add list editor for array fields (#229)

* Add list editor for array fields

* Disable move button in edit mode
This commit is contained in:
Gerhard Tan
2025-04-13 18:58:17 +08:00
committed by GitHub
parent 86cf25174b
commit 54f94e829f
8 changed files with 1089 additions and 927 deletions

2
go.mod
View File

@ -64,4 +64,4 @@ require (
sigs.k8s.io/yaml v1.3.0 // indirect
)
replace github.com/lxn/walk => github.com/koho/frpmgr v0.0.0-20250406073618-38a03e8c80a6
replace github.com/lxn/walk => github.com/koho/frpmgr v0.0.0-20250413103505-f0a017b962b3

4
go.sum
View File

@ -66,8 +66,8 @@ github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/4
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/reedsolomon v1.12.0 h1:I5FEp3xSwVCcEh3F5A7dofEfhXdF/bWhQWPH+XwBFno=
github.com/klauspost/reedsolomon v1.12.0/go.mod h1:EPLZJeh4l27pUGC3aXOjheaoh1I9yut7xTURiW3LQ9Y=
github.com/koho/frpmgr v0.0.0-20250406073618-38a03e8c80a6 h1:dH1t2r48JzVxOKrcGdt7604JkpZP/a99adFOHcVmt2I=
github.com/koho/frpmgr v0.0.0-20250406073618-38a03e8c80a6/go.mod h1:BEvTAgZsEET00wLNsOhKg8fX//k6l4b5INTzX2APBb8=
github.com/koho/frpmgr v0.0.0-20250413103505-f0a017b962b3 h1:UTMmkSnq0+SWSGoIyrklIJvCYcm7zYfX2r6SVDxcLy0=
github.com/koho/frpmgr v0.0.0-20250413103505-f0a017b962b3/go.mod h1:BEvTAgZsEET00wLNsOhKg8fX//k6l4b5INTzX2APBb8=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,8 @@
package ui
import (
"strings"
"github.com/lxn/walk"
. "github.com/lxn/walk/declarative"
@ -16,7 +18,7 @@ func NewBrowseLineEdit(assignTo **walk.LineEdit, visible, enable, text Property,
}
return Composite{
Visible: visible,
Layout: HBox{MarginsZero: true, SpacingZero: false, Spacing: 3},
Layout: HBox{MarginsZero: true},
Children: []Widget{
LineEdit{Enabled: enable, AssignTo: assignTo, Text: text},
ToolButton{Enabled: enable, Text: "...", MaxSize: Size{Width: 24}, OnClicked: func() {
@ -103,36 +105,43 @@ func AlignGrid(page TabPage, n int) TabPage {
// It provides the ability to edit cells by double-clicking.
func NewAttributeTable(m *AttributeModel, nameWidth, valueWidth int) Composite {
var tv *walk.TableView
fc := func(value interface{}) string {
return *value.(*string)
}
return Composite{
Layout: HBox{MarginsZero: true},
Children: []Widget{
TableView{
AssignTo: &tv,
Name: "attr",
Columns: []TableViewColumn{
{Title: i18n.Sprintf("Name"), Width: nameWidth, FormatFunc: fc},
{Title: i18n.Sprintf("Value"), Width: valueWidth, FormatFunc: fc},
{Title: i18n.Sprintf("Name"), Width: nameWidth},
{Title: i18n.Sprintf("Value"), Width: valueWidth},
},
Model: m,
Editable: true,
Model: m,
Editable: true,
ColumnsOrderable: false,
},
Composite{
Layout: VBox{MarginsZero: true},
Children: []Widget{
PushButton{Text: i18n.Sprintf("Add"), OnClicked: func() {
m.Add("", "")
}},
PushButton{Text: i18n.Sprintf("Delete"), OnClicked: func() {
if i := tv.CurrentIndex(); i >= 0 {
m.Delete(i)
}
}},
PushButton{
Text: i18n.Sprintf("Add"),
OnClicked: func() {
m.Add("", "")
},
},
PushButton{
Enabled: Bind("attr.CurrentIndex >= 0"),
Text: i18n.Sprintf("Delete"),
OnClicked: func() {
if i := tv.CurrentIndex(); i >= 0 {
m.Delete(i)
}
},
},
VSpacer{Size: 16},
PushButton{Text: i18n.Sprintf("Clear All"), OnClicked: func() {
m.Clear()
}},
PushButton{
Text: i18n.Sprintf("Clear All"),
OnClicked: m.Clear,
},
VSpacer{},
},
},
@ -149,11 +158,121 @@ func NewAttributeDialog(title string, data *map[string]string) Dialog {
p.Accept()
},
NewAttributeTable(m, 120, 120),
VSpacer{},
)
dlg.MinSize = Size{Width: 420, Height: 280}
return dlg
}
// NewListEditDialog returns a dialog box with the values displayed in the list box.
// It provides the ability to edit rows by double-clicking.
func NewListEditDialog(title string, values []string, cb func(string) error) Dialog {
var p *walk.Dialog
var tv *walk.TableView
m := NewListEditModel(values)
move := func(delta int) {
curIdx := tv.CurrentIndex()
if curIdx < 0 || curIdx >= m.RowCount() {
return
}
targetIdx := curIdx + delta
if targetIdx < 0 || targetIdx >= m.RowCount() {
return
}
m.Move(curIdx, targetIdx)
tv.SetCurrentIndex(targetIdx)
}
dlg := NewBasicDialog(&p, title, loadIcon(res.IconFile, 32), DataBinder{}, func() {
if err := cb(m.AsString()); err != nil {
return
}
p.Accept()
}, Composite{
Layout: HBox{MarginsZero: true},
Children: []Widget{
TableView{
AssignTo: &tv,
Name: "tv",
Columns: []TableViewColumn{{}},
Model: m,
Editable: true,
HeaderHidden: true,
LastColumnStretched: true,
},
Composite{
Layout: VBox{MarginsZero: true},
Children: []Widget{
PushButton{
Text: i18n.Sprintf("Add"),
OnClicked: func() {
m.Add("")
},
},
PushButton{
Enabled: Bind("tv.CurrentIndex >= 0"),
Text: i18n.Sprintf("Delete"),
OnClicked: func() {
if i := tv.CurrentIndex(); i >= 0 {
m.Delete(i)
}
},
},
PushButton{
Text: i18n.Sprintf("Clear All"),
OnClicked: m.Clear,
},
VSpacer{},
PushButton{
Enabled: Bind("!tv.BeginEdit && tv.CurrentIndex > 0"),
Text: i18n.Sprintf("Move Up"),
OnClicked: func() {
move(-1)
},
},
PushButton{
Enabled: Bind("!tv.BeginEdit && tv.CurrentIndex >= 0 && tv.CurrentIndex < tv.ItemCount - 1"),
Text: i18n.Sprintf("Move Down"),
OnClicked: func() {
move(1)
},
},
},
},
},
}, VSpacer{})
dlg.MinSize = Size{Width: 350, Height: 300}
return dlg
}
// NewListEdit places a tool button at the tail of a LineEdit, and opens a list edit dialog when the button is clicked.
func NewListEdit(owner walk.Window, visible, text Property, title string, widget ...Widget) Composite {
var editView *walk.LineEdit
children := []Widget{
LineEdit{
AssignTo: &editView,
Text: text,
CueBanner: "a,b,c...",
},
ToolButton{
Text: "...",
MaxSize: Size{Width: 24},
OnClicked: func() {
var values []string
if input := strings.TrimSpace(editView.Text()); input != "" {
values = strings.Split(input, ",")
}
NewListEditDialog(title, values, editView.SetText).Run(owner.Form())
},
},
}
children = append(children, widget...)
return Composite{
Visible: visible,
Layout: HBox{MarginsZero: true},
Children: children,
}
}
type NIOption struct {
Title string
Value Property

View File

@ -235,7 +235,7 @@ func (pd *EditProxyDialog) basicProxyPage() TabPage {
Label{Visible: Bind("vm.RemotePortVisible"), Text: i18n.SprintfColon("Remote Port")},
LineEdit{Visible: Bind("vm.RemotePortVisible"), Text: Bind("RemotePort")},
Label{Visible: Bind("vm.RoleVisible && !vm.ServerNameVisible"), Text: i18n.SprintfColon("Allow Users")},
LineEdit{Visible: Bind("vm.RoleVisible && !vm.ServerNameVisible"), Text: Bind("AllowUsers"), CueBanner: "a,b,c..."},
NewListEdit(pd, Bind("vm.RoleVisible && !vm.ServerNameVisible"), Bind("AllowUsers"), i18n.Sprintf("Allow Users")),
Label{Visible: Bind("vm.BindAddrVisible"), Text: i18n.SprintfColon("Bind Address")},
LineEdit{Visible: Bind("vm.BindAddrVisible"), Text: Bind("BindAddr"), CueBanner: "127.0.0.1"},
Label{Visible: Bind("vm.BindPortVisible"), Text: i18n.SprintfColon("Bind Port")},
@ -247,16 +247,9 @@ func (pd *EditProxyDialog) basicProxyPage() TabPage {
Label{Visible: Bind("vm.DomainVisible"), Text: i18n.SprintfColon("Subdomain")},
LineEdit{Visible: Bind("vm.DomainVisible"), Text: Bind("SubDomain")},
Label{Visible: Bind("vm.DomainVisible"), Text: i18n.SprintfColon("Custom Domains")},
LineEdit{Visible: Bind("vm.DomainVisible"), Text: Bind("CustomDomains"), CueBanner: "a,b,c..."},
NewListEdit(pd, Bind("vm.DomainVisible"), Bind("CustomDomains"), i18n.Sprintf("Custom Domains")),
Label{Visible: Bind("vm.HTTPVisible"), Text: i18n.SprintfColon("Locations")},
Composite{
Visible: Bind("vm.HTTPVisible"),
Layout: HBox{MarginsZero: true},
Children: []Widget{
LineEdit{Text: Bind("Locations"), CueBanner: "a,b,c..."},
headerBtn,
},
},
NewListEdit(pd, Bind("vm.HTTPVisible"), Bind("Locations"), i18n.Sprintf("Locations"), headerBtn),
Label{Visible: Bind("vm.MuxVisible"), Text: i18n.SprintfColon("Multiplexer")},
ComboBox{
Visible: Bind("vm.MuxVisible"),

View File

@ -208,7 +208,7 @@ func (lp *LogPage) OnCreate() {
func (lp *LogPage) refreshLog() {
lp.logView.Synchronize(func() {
if lp.logModel != nil {
scroll := lp.logModel.RowCount() == 0 || lp.logView.ItemVisible(lp.logModel.RowCount()-1)
scroll := lp.logModel.RowCount() == 0 || (lp.logView.ItemVisible(lp.logModel.RowCount()-1) && len(lp.logView.SelectedIndexes()) <= 1)
if n, err := lp.logModel.ReadMore(); err == nil && n > 0 && scroll {
lp.scrollToBottom()
}

View File

@ -457,3 +457,52 @@ func (a *AttributeModel) AsMap() map[string]string {
}
return m
}
// ListEditModel is a list of strings, but supports editing.
type ListEditModel struct {
walk.ReflectTableModelBase
values []string
}
func NewListEditModel(values []string) *ListEditModel {
return &ListEditModel{values: values}
}
func (m *ListEditModel) Value(row, col int) interface{} {
return &m.values[row]
}
func (m *ListEditModel) RowCount() int {
return len(m.values)
}
func (m *ListEditModel) Add(value string) {
m.values = append(m.values, value)
i := len(m.values) - 1
m.PublishRowsInserted(i, i)
}
func (m *ListEditModel) Delete(i int) {
m.values = append(m.values[:i], m.values[i+1:]...)
m.PublishRowsRemoved(i, i)
}
func (m *ListEditModel) Clear() {
m.values = nil
m.PublishRowsReset()
}
func (m *ListEditModel) Move(i, j int) {
util.MoveSlice(m.values, i, j)
m.PublishRowsChanged(min(i, j), max(i, j))
}
func (m *ListEditModel) AsString() string {
if len(m.values) == 0 {
return ""
}
return strings.Join(lo.Filter(m.values, func(item string, index int) bool {
return strings.TrimSpace(item) != ""
}), ",")
}

View File

@ -46,6 +46,7 @@ func (nd *NATDiscoveryDialog) Run(owner walk.Form) (int, error) {
{Title: i18n.Sprintf("Item"), DataMember: "Title", Width: 180},
{Title: i18n.Sprintf("Value"), DataMember: "Value", Width: 180},
},
ColumnsOrderable: false,
},
ProgressBar{AssignTo: &nd.barView, Visible: Bind("!tb.Visible"), MarqueeMode: true},
VSpacer{},