mirror of
https://github.com/koho/frpmgr.git
synced 2025-10-20 16:03:47 +08:00
Add list editor for array fields (#229)
* Add list editor for array fields * Disable move button in edit mode
This commit is contained in:
2
go.mod
2
go.mod
@ -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
4
go.sum
@ -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=
|
||||
|
1788
i18n/catalog.go
1788
i18n/catalog.go
File diff suppressed because it is too large
Load Diff
157
ui/composite.go
157
ui/composite.go
@ -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
|
||||
|
@ -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"),
|
||||
|
@ -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()
|
||||
}
|
||||
|
49
ui/model.go
49
ui/model.go
@ -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) != ""
|
||||
}), ",")
|
||||
}
|
||||
|
@ -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{},
|
||||
|
Reference in New Issue
Block a user