Files
vnak/req.go

838 lines
22 KiB
Go

package main
import (
"context"
"encoding/json"
"os"
"strconv"
"strings"
"sync/atomic"
"time"
"fiatjaf.com/nostr"
qt "github.com/mappu/miqt/qt6"
"github.com/mappu/miqt/qt6/mainthread"
"golang.org/x/exp/slices"
)
type reqVars struct {
tab *qt.QWidget
authorsVBox *qt.QVBoxLayout
authorsEdits []*qt.QLineEdit
idsVBox *qt.QVBoxLayout
idsEdits []*qt.QLineEdit
kindsVBox *qt.QVBoxLayout
kindRows []reqKindRow
tagsVBox *qt.QVBoxLayout
tagRows []reqTagRow
tagsLayout *qt.QVBoxLayout
relaysHBox *qt.QHBoxLayout
relaysEdits []*qt.QLineEdit
sinceEdit *qt.QDateTimeEdit
sinceCheck *qt.QCheckBox
untilEdit *qt.QDateTimeEdit
untilCheck *qt.QCheckBox
limitSpin *qt.QSpinBox
limitCheck *qt.QCheckBox
filter nostr.Filter
outputEdit *qt.QTextEdit
resultsList *qt.QListWidget
sendButton *qt.QPushButton
stopButton *qt.QPushButton
clearButton *qt.QPushButton
exportButton *qt.QPushButton
subCancel context.CancelCauseFunc
}
type reqKindRow struct {
hbox *qt.QHBoxLayout
edit *qt.QLineEdit
label *qt.QLabel
}
type reqTagRow struct {
hbox *qt.QHBoxLayout
key *qt.QLineEdit
separator *qt.QLabel
vals []*qt.QLineEdit
}
var req = &reqVars{}
func (req *reqVars) addRelayEdit() {
edit := qt.NewQLineEdit(req.tab)
req.relaysEdits = append(req.relaysEdits, edit)
req.relaysHBox.AddWidget(edit.QWidget)
edit.OnTextChanged(func(text string) {
if strings.TrimSpace(text) != "" {
if edit == req.relaysEdits[len(req.relaysEdits)-1] {
req.addRelayEdit()
}
} else {
n := len(req.relaysEdits)
if n >= 2 && strings.TrimSpace(req.relaysEdits[n-1].Text()) == "" && strings.TrimSpace(req.relaysEdits[n-2].Text()) == "" {
req.relaysHBox.RemoveWidget(req.relaysEdits[n-1].QWidget)
req.relaysEdits[n-1].DeleteLater()
req.relaysEdits = req.relaysEdits[0 : n-1]
}
}
// update sendButton state
hasRelays := false
for _, edit := range req.relaysEdits {
if strings.TrimSpace(edit.Text()) != "" {
hasRelays = true
break
}
}
req.sendButton.SetEnabled(hasRelays)
})
edit.OnReturnPressed(func() {
req.subscribe()
})
}
func setupReqTab() *qt.QWidget {
req.tab = qt.NewQWidget(window.QWidget)
layout := qt.NewQVBoxLayout2()
req.tab.SetLayout(layout.QLayout)
// ids
idsLabel := qt.NewQLabel2()
idsLabel.SetText("ids:")
layout.AddWidget(idsLabel.QWidget)
req.idsVBox = qt.NewQVBoxLayout2()
layout.AddLayout(req.idsVBox.QLayout)
req.idsEdits = []*qt.QLineEdit{}
req.addId("")
// authors
authorsLabel := qt.NewQLabel2()
authorsLabel.SetText("authors:")
layout.AddWidget(authorsLabel.QWidget)
req.authorsVBox = qt.NewQVBoxLayout2()
layout.AddLayout(req.authorsVBox.QLayout)
req.authorsEdits = []*qt.QLineEdit{}
req.addAuthor("")
// kinds
kindsLabel := qt.NewQLabel2()
kindsLabel.SetText("kinds:")
layout.AddWidget(kindsLabel.QWidget)
req.kindsVBox = qt.NewQVBoxLayout2()
layout.AddLayout(req.kindsVBox.QLayout)
req.kindRows = []reqKindRow{}
req.addKind("")
// tags
tagsLabel := qt.NewQLabel2()
tagsLabel.SetText("tags:")
layout.AddWidget(tagsLabel.QWidget)
req.tagsLayout = qt.NewQVBoxLayout2()
layout.AddLayout(req.tagsLayout.QLayout)
req.tagRows = make([]reqTagRow, 0, 2)
req.addTagRow("", []string{""})
// numeric and boolean inputs
qtime := qt.NewQDateTime()
qtime.SetMSecsSinceEpoch(time.Now().UnixMilli())
inputsHBox := qt.NewQHBoxLayout2()
layout.AddLayout(inputsHBox.QLayout)
// limit
limitHBox := qt.NewQHBoxLayout2()
inputsHBox.AddLayout(limitHBox.QLayout)
limitLabel := qt.NewQLabel2()
limitLabel.SetText("limit:")
limitHBox.AddWidget(limitLabel.QWidget)
req.limitCheck = qt.NewQCheckBox(req.tab)
req.limitCheck.SetChecked(true)
limitHBox.AddWidget(req.limitCheck.QWidget)
req.limitSpin = qt.NewQSpinBox(req.tab)
req.limitSpin.SetEnabled(true)
req.limitSpin.SetMinimum(0)
req.limitSpin.SetMaximum(1000)
req.limitSpin.SetValue(10)
limitHBox.AddWidget(req.limitSpin.QWidget)
req.limitCheck.OnStateChanged(func(state int) {
req.limitSpin.SetEnabled(state == 2) // 2 is checked
req.updateReq()
})
req.limitSpin.OnValueChanged(func(int) {
req.updateReq()
})
// since
sinceHBox := qt.NewQHBoxLayout2()
sinceHBox.AddStretch()
inputsHBox.AddLayout(sinceHBox.QLayout)
sinceLabel := qt.NewQLabel2()
sinceLabel.SetText("since:")
sinceHBox.AddWidget(sinceLabel.QWidget)
req.sinceCheck = qt.NewQCheckBox(req.tab)
req.sinceCheck.SetChecked(false)
sinceHBox.AddWidget(req.sinceCheck.QWidget)
req.sinceEdit = qt.NewQDateTimeEdit(req.tab)
req.sinceEdit.SetEnabled(false)
req.sinceEdit.SetDateTime(qtime)
sinceHBox.AddWidget(req.sinceEdit.QWidget)
req.sinceCheck.OnStateChanged(func(state int) {
req.sinceEdit.SetEnabled(state == 2) // 2 is checked
req.updateReq()
})
req.sinceEdit.OnDateTimeChanged(func(*qt.QDateTime) {
req.updateReq()
})
// until
untilHBox := qt.NewQHBoxLayout2()
untilHBox.AddStretch()
inputsHBox.AddLayout(untilHBox.QLayout)
untilLabel := qt.NewQLabel2()
untilLabel.SetText("until:")
untilHBox.AddWidget(untilLabel.QWidget)
req.untilCheck = qt.NewQCheckBox(req.tab)
req.untilCheck.SetChecked(false)
untilHBox.AddWidget(req.untilCheck.QWidget)
req.untilEdit = qt.NewQDateTimeEdit(req.tab)
req.untilEdit.SetEnabled(false)
req.untilEdit.SetDateTime(qtime)
untilHBox.AddWidget(req.untilEdit.QWidget)
req.untilCheck.OnStateChanged(func(state int) {
req.untilEdit.SetEnabled(state == 2) // 2 is checked
req.updateReq()
})
req.untilEdit.OnDateTimeChanged(func(*qt.QDateTime) {
req.updateReq()
})
// output
outputHBox := qt.NewQHBoxLayout2()
layout.AddLayout(outputHBox.QLayout)
outputLabel := qt.NewQLabel2()
outputLabel.SetText("filter:")
outputHBox.AddWidget(outputLabel.QWidget)
req.outputEdit = qt.NewQTextEdit(req.tab)
req.outputEdit.SetReadOnly(true)
req.outputEdit.SetMaximumHeight(100)
outputHBox.AddWidget(req.outputEdit.QWidget)
req.sendButton = qt.NewQPushButton5("send request", req.tab)
req.sendButton.OnClicked(req.subscribe)
req.sendButton.SetEnabled(false)
req.stopButton = qt.NewQPushButton5("stop", req.tab)
req.stopButton.OnClicked(req.cancelSubscription)
req.stopButton.SetEnabled(false)
req.clearButton = qt.NewQPushButton5("clear", req.tab)
req.clearButton.OnClicked(func() {
req.resultsList.Clear()
})
req.exportButton = qt.NewQPushButton5("export", req.tab)
req.exportButton.OnClicked(func() {
if req.resultsList.Count() == 0 {
setStatus(tabs.req, "no results to export")
return
}
fileDialog := qt.NewQFileDialog(req.tab)
fileDialog.SetWindowTitle("Export Results")
fileDialog.SetAcceptMode(qt.QFileDialog__AcceptSave)
fileDialog.SetNameFilter("JSONL Files (*.jsonl)")
if fileDialog.Exec() == int(qt.QDialog__Accepted) {
selectedFiles := fileDialog.SelectedFiles()
if len(selectedFiles) > 0 {
filePath := selectedFiles[0]
go func() {
file, err := os.Create(filePath)
if err != nil {
mainthread.Wait(func() {
setStatus(tabs.req, "failed to create file: %v", err)
})
return
}
defer file.Close()
for i := 0; i < req.resultsList.Count(); i++ {
item := req.resultsList.Item(i)
if item != nil {
line := item.Text() + "\n"
file.WriteString(line)
}
}
mainthread.Wait(func() {
setStatus(tabs.req, "exported %d events to %s", req.resultsList.Count(), filePath)
})
}()
}
}
})
// relays
relaysHBox := qt.NewQHBoxLayout2()
req.relaysHBox = relaysHBox
layout.AddLayout(relaysHBox.QLayout)
relaysLabel := qt.NewQLabel2()
relaysLabel.SetText("relays:")
relaysHBox.AddWidget(relaysLabel.QWidget)
req.relaysEdits = []*qt.QLineEdit{}
req.addRelayEdit()
// results
resultsVBox := qt.NewQVBoxLayout2()
resultsLabel := qt.NewQLabel2()
resultsLabel.SetText("results:")
req.resultsList = qt.NewQListWidget(req.tab)
resultsVBox.AddWidget(resultsLabel.QWidget)
resultsVBox.AddWidget(req.resultsList.QWidget)
subscribeHBox := qt.NewQHBoxLayout2()
layout.AddLayout(subscribeHBox.QLayout)
buttonsVBox := qt.NewQVBoxLayout2()
subscribeHBox.AddLayout(buttonsVBox.QLayout)
buttonsVBox.AddWidget(req.sendButton.QWidget)
buttonsVBox.AddWidget(req.stopButton.QWidget)
buttonsVBox.AddWidget(req.clearButton.QWidget)
buttonsVBox.AddWidget(req.exportButton.QWidget)
subscribeHBox.AddLayout(resultsVBox.QLayout)
// double-click to show pretty JSON
req.resultsList.OnItemDoubleClicked(func(item *qt.QListWidgetItem) {
var evt nostr.Event
if err := json.Unmarshal([]byte(item.Text()), &evt); err != nil {
return
}
pretty, _ := json.MarshalIndent(evt, "", " ")
dialog := qt.NewQDialog(window.QWidget)
dialog.SetWindowTitle("event")
dialog.SetMinimumWidth(400)
dialog.SetMinimumHeight(500)
dlayout := qt.NewQVBoxLayout2()
dialog.SetLayout(dlayout.QLayout)
textEdit := qt.NewQTextEdit(dialog.QWidget)
textEdit.SetReadOnly(true)
textEdit.SetPlainText(string(pretty))
dlayout.AddWidget(textEdit.QWidget)
// buttons
buttonsHBox := qt.NewQHBoxLayout2()
dlayout.AddLayout(buttonsHBox.QLayout)
editButton := qt.NewQPushButton5("manual edit "+ARROW, dialog.QWidget)
editButton.OnClicked(func() {
paste.inputEdit.SetPlainText(string(pretty))
tabWidget.SetCurrentIndex(tabs.paste)
dialog.Close()
})
buttonsHBox.AddWidget(editButton.QWidget)
publishButton := qt.NewQPushButton5("structured edit "+ARROW, dialog.QWidget)
publishButton.OnClicked(func() {
event.populate(evt)
tabWidget.SetCurrentIndex(tabs.event)
dialog.Close()
})
buttonsHBox.AddWidget(publishButton.QWidget)
closeButton := qt.NewQPushButton5("close", dialog.QWidget)
closeButton.OnClicked(func() { dialog.Close() })
buttonsHBox.AddWidget(closeButton.QWidget)
dialog.Exec()
})
return req.tab
}
func (req *reqVars) updateReq() {
req.filter = nostr.Filter{}
// collect authors
authors := []nostr.PubKey{}
for _, edit := range req.authorsEdits {
if pk, err := parsePubKey(strings.TrimSpace(edit.Text())); err == nil {
authors = append(authors, pk)
}
}
if len(authors) > 0 {
req.filter.Authors = authors
}
// collect ids
ids := []nostr.ID{}
for _, edit := range req.idsEdits {
if id, err := parseEventID(strings.TrimSpace(edit.Text())); err == nil {
ids = append(ids, id)
}
}
if len(ids) > 0 {
req.filter.IDs = ids
}
// collect kinds
kinds := []nostr.Kind{}
for _, kindRow := range req.kindRows {
text := strings.TrimSpace(kindRow.edit.Text())
kindRow.label.SetText("")
if k, err := strconv.Atoi(text); err == nil {
kind := nostr.Kind(k)
kinds = append(kinds, kind)
// update kind label
name := kind.Name()
if name != "unknown" {
kindRow.label.SetText(name)
}
}
}
if len(kinds) > 0 {
req.filter.Kinds = kinds
}
// collect tags
tags := make(map[string][]string)
for _, tagRow := range req.tagRows {
key := strings.TrimSpace(tagRow.key.Text())
if key == "" || len(tagRow.vals) == 0 {
continue
}
values := make([]string, 0, len(tagRow.vals))
for _, edit := range tagRow.vals {
text := strings.TrimSpace(edit.Text())
if text != "" {
values = append(values, text)
}
}
if len(values) > 0 {
tags[key] = values
}
}
if len(tags) > 0 {
req.filter.Tags = tags
}
// since
if req.sinceCheck.IsChecked() && req.sinceEdit.DateTime().IsValid() {
ts := nostr.Timestamp(req.sinceEdit.DateTime().ToMSecsSinceEpoch() / 1000)
req.filter.Since = ts
}
// until
if req.untilCheck.IsChecked() && req.untilEdit.DateTime().IsValid() {
ts := nostr.Timestamp(req.untilEdit.DateTime().ToMSecsSinceEpoch() / 1000)
req.filter.Until = ts
}
// limit
if req.limitCheck.IsChecked() {
if req.limitSpin.Value() > 0 {
req.filter.Limit = req.limitSpin.Value()
} else {
req.filter.LimitZero = true
}
}
jsonBytes, _ := json.Marshal(req.filter)
req.outputEdit.SetPlainText(string(jsonBytes))
}
func (req *reqVars) cancelSubscription() {
if req.subCancel != nil {
req.subCancel(manualCancel)
req.subCancel = nil
req.stopButton.SetEnabled(false)
}
}
func (req *reqVars) subscribe() {
req.cancelSubscription()
req.stopButton.SetEnabled(true)
// collect relays
relays := []string{}
for _, edit := range req.relaysEdits {
url := strings.TrimSpace(edit.Text())
if url != "" {
relays = append(relays, url)
}
}
if len(relays) == 0 {
setStatus(tabs.req, "no relays specified")
return
}
// subscribe
var eoseChan chan struct{}
var eventsChan chan nostr.RelayEvent
ctx, cancel := context.WithCancelCause(ctx)
req.subCancel = cancel
if len(relays) == 1 {
relay, err := sys.Pool.EnsureRelay(relays[0])
if err != nil {
setStatus(tabs.req, "failed to connect to %s: %s", niceRelayURL(relays[0]), err)
return
}
setStatus(tabs.req, "subscribed to "+niceRelayURL(relay.URL))
sub, err := relay.Subscribe(ctx, req.filter, nostr.SubscriptionOptions{
Label: "vnak-req-1",
})
if err != nil {
setStatus(tabs.req, "failed to subscribe to %s: %s", niceRelayURL(relay.URL), err)
return
}
eventsChan = make(chan nostr.RelayEvent)
go func() {
for event := range sub.Events {
eventsChan <- nostr.RelayEvent{
Event: event,
Relay: relay,
}
}
}()
eoseChan = sub.EndOfStoredEvents
go func() {
reason := <-sub.ClosedReason
req.subCancel = nil
mainthread.Wait(func() {
setStatus(tabs.req, "subscription closed: %s", reason)
req.stopButton.SetEnabled(false)
})
}()
} else {
setStatus(tabs.req, "subscribed to "+strings.Join(niceRelayURLs(relays), ", "))
eventsChan, eoseChan = sys.Pool.SubscribeManyNotifyEOSE(ctx, relays, req.filter, nostr.SubscriptionOptions{Label: "vnak-req"})
}
// collect events
var eosed atomic.Bool
go func() {
for ie := range eventsChan {
jsonBytes, _ := json.Marshal(ie.Event)
mainthread.Wait(func() {
item := qt.NewQListWidgetItem2(string(jsonBytes))
if eosed.Load() {
req.resultsList.InsertItem(0, item)
} else {
req.resultsList.AddItemWithItem(item)
}
})
}
req.subCancel = nil
mainthread.Wait(func() {
setStatus(tabs.req, "subscription ended")
req.stopButton.SetEnabled(false)
})
}()
go func() {
<-eoseChan
eosed.Store(true)
}()
}
func (req *reqVars) populate(filter nostr.Filter, relays []string) {
// clear all authors except the first, set the first to ""
for _, authorEdit := range req.authorsEdits {
req.authorsVBox.RemoveWidget(authorEdit.QWidget)
authorEdit.DeleteLater()
}
req.authorsEdits = req.authorsEdits[:0]
// add authors
for _, author := range filter.Authors {
req.addAuthor(author.Hex())
}
req.addAuthor("") // extra
// clear all ids except the first, set the first to ""
for _, idEdit := range req.idsEdits {
req.idsVBox.RemoveWidget(idEdit.QWidget)
idEdit.DeleteLater()
}
req.idsEdits = req.idsEdits[:0]
// add ids
for _, id := range filter.IDs {
req.addId(id.Hex())
}
req.addId("") // extra
// clear all kinds except the first, set the first to ""
for _, kindRow := range req.kindRows {
kindRow.hbox.RemoveWidget(kindRow.label.QWidget)
kindRow.hbox.RemoveWidget(kindRow.edit.QWidget)
kindRow.label.DeleteLater()
kindRow.edit.DeleteLater()
req.kindsVBox.RemoveItem(kindRow.hbox.QLayoutItem)
kindRow.hbox.DeleteLater()
}
req.kindRows = req.kindRows[:0]
// add kinds
for _, kind := range filter.Kinds {
req.addKind(strconv.Itoa(int(kind)))
}
req.addKind("") // extra
// clear all tag items and rows
for _, tagRow := range req.tagRows {
tagRow.hbox.RemoveWidget(tagRow.key.QWidget)
tagRow.key.QWidget.DeleteLater()
tagRow.hbox.RemoveWidget(tagRow.separator.QWidget)
tagRow.separator.DeleteLater()
for _, item := range tagRow.vals {
tagRow.hbox.RemoveWidget(item.QWidget)
item.DeleteLater()
}
req.tagsLayout.RemoveItem(tagRow.hbox.QLayoutItem)
tagRow.hbox.DeleteLater()
}
req.tagRows = req.tagRows[:0]
// add tags according to filter
for key, values := range filter.Tags {
req.addTagRow(key, values)
}
req.addTagRow("", []string{""}) // extra
// set since, until, limit
if filter.Since != 0 {
req.sinceCheck.SetChecked(true)
dt := qt.NewQDateTime()
dt.SetMSecsSinceEpoch(int64(filter.Since) * 1000)
req.sinceEdit.SetDateTime(dt)
} else {
req.sinceCheck.SetChecked(false)
}
if filter.Until != 0 {
req.untilCheck.SetChecked(true)
dt := qt.NewQDateTime()
dt.SetMSecsSinceEpoch(int64(filter.Until) * 1000)
req.untilEdit.SetDateTime(dt)
} else {
req.untilCheck.SetChecked(false)
}
if filter.Limit != 0 {
req.limitCheck.SetChecked(true)
req.limitSpin.SetValue(filter.Limit)
} else if filter.LimitZero {
req.limitCheck.SetChecked(true)
req.limitSpin.SetValue(0)
} else {
req.limitCheck.SetChecked(false)
}
// set relays
for _, relayEdit := range req.relaysEdits {
req.relaysHBox.RemoveWidget(relayEdit.QWidget)
relayEdit.DeleteLater()
}
req.relaysEdits = req.relaysEdits[:0]
for i, relay := range relays {
req.addRelayEdit()
req.relaysEdits[i].SetText(relay)
}
req.updateReq()
}
func (req *reqVars) addAuthor(value string) {
edit := qt.NewQLineEdit(req.tab)
edit.SetText(value)
req.authorsEdits = append(req.authorsEdits, edit)
req.authorsVBox.AddWidget(edit.QWidget)
edit.OnTextChanged(func(text string) {
if strings.TrimSpace(text) != "" {
if edit == req.authorsEdits[len(req.authorsEdits)-1] {
req.addAuthor("")
}
} else {
n := len(req.authorsEdits)
if n >= 2 && strings.TrimSpace(req.authorsEdits[n-1].Text()) == "" && strings.TrimSpace(req.authorsEdits[n-2].Text()) == "" {
req.authorsVBox.RemoveWidget(req.authorsEdits[n-1].QWidget)
req.authorsEdits[n-1].DeleteLater()
req.authorsEdits = req.authorsEdits[0 : n-1]
}
}
req.updateReq()
})
}
func (req *reqVars) addId(text string) {
edit := qt.NewQLineEdit(req.tab)
edit.SetText(text)
req.idsEdits = append(req.idsEdits, edit)
req.idsVBox.AddWidget(edit.QWidget)
edit.OnTextChanged(func(text string) {
if strings.TrimSpace(text) != "" {
if edit == req.idsEdits[len(req.idsEdits)-1] {
req.addId("")
}
} else {
n := len(req.idsEdits)
if n >= 2 && strings.TrimSpace(req.idsEdits[n-1].Text()) == "" && strings.TrimSpace(req.idsEdits[n-2].Text()) == "" {
req.idsVBox.RemoveWidget(req.idsEdits[n-1].QWidget)
req.idsEdits[n-1].DeleteLater()
req.idsEdits = req.idsEdits[0 : n-1]
}
}
req.updateReq()
})
}
func (req *reqVars) addKind(text string) {
hbox := qt.NewQHBoxLayout2()
req.kindsVBox.AddLayout(hbox.QLayout)
edit := qt.NewQLineEdit(req.tab)
edit.SetText(text)
hbox.AddWidget(edit.QWidget)
label := qt.NewQLabel2()
hbox.AddWidget(label.QWidget)
req.kindRows = append(req.kindRows, reqKindRow{
hbox: hbox,
label: label,
edit: edit,
})
edit.OnTextChanged(func(text string) {
if strings.TrimSpace(text) != "" {
if edit == req.kindRows[len(req.kindRows)-1].edit {
req.addKind("")
}
} else {
n := len(req.kindRows)
if n >= 2 && strings.TrimSpace(req.kindRows[n-1].edit.Text()) == "" && strings.TrimSpace(req.kindRows[n-2].edit.Text()) == "" {
last := req.kindRows[n-1]
req.kindsVBox.RemoveItem(last.hbox.QLayoutItem)
last.hbox.RemoveWidget(req.kindRows[n-1].edit.QWidget)
last.hbox.RemoveWidget(req.kindRows[n-1].label.QWidget)
last.edit.DeleteLater()
last.label.DeleteLater()
last.hbox.DeleteLater()
req.kindRows = req.kindRows[:n-1]
}
}
req.updateReq()
})
}
func (req *reqVars) addTagRow(name string, values []string) {
hbox := qt.NewQHBoxLayout2()
req.tagsLayout.AddLayout(hbox.QLayout)
valuesEdits := []*qt.QLineEdit{}
y := len(req.tagRows)
var perhapsDeleteRow func()
// name
editName := qt.NewQLineEdit(req.tab)
editName.SetText(name)
hbox.AddWidget(editName.QWidget)
editName.OnTextChanged(func(text string) {
if strings.TrimSpace(text) != "" {
if y == len(req.tagRows)-1 {
req.addTagRow("", []string{""})
}
} else {
perhapsDeleteRow()
}
req.updateReq()
})
// separator
separator := qt.NewQLabel2()
separator.SetText(": ")
hbox.AddWidget(separator.QWidget)
// keep track of everything
req.tagRows = append(req.tagRows, reqTagRow{
hbox: hbox,
key: editName,
separator: separator,
})
var addValue func(string)
addValue = func(val string) {
edit := qt.NewQLineEdit(req.tab)
edit.SetText(val)
hbox.AddWidget(edit.QWidget)
x := len(valuesEdits)
valuesEdits = append(valuesEdits, edit)
req.tagRows[y].vals = valuesEdits
edit.OnTextChanged(func(text string) {
if strings.TrimSpace(text) != "" {
if x == len(valuesEdits)-1 {
addValue("")
}
} else {
nItems := len(valuesEdits)
if nItems >= 2 &&
strings.TrimSpace(valuesEdits[nItems-1].Text()) == "" &&
strings.TrimSpace(valuesEdits[nItems-2].Text()) == "" {
// when the last values of a tag row are empty remove the last one
last := valuesEdits[nItems-1]
hbox.RemoveWidget(last.QWidget)
last.DeleteLater()
valuesEdits = valuesEdits[0 : nItems-1]
req.tagRows[y].vals = valuesEdits
}
perhapsDeleteRow()
}
req.updateReq()
})
}
perhapsDeleteRow = func() {
nRows := len(req.tagRows)
hasValue := func(e *qt.QLineEdit) bool { return strings.TrimSpace(e.Text()) != "" }
if nRows >= 2 &&
!slices.ContainsFunc(req.tagRows[nRows-1].vals, hasValue) &&
!slices.ContainsFunc(req.tagRows[nRows-2].vals, hasValue) &&
strings.TrimSpace(req.tagRows[nRows-1].key.Text()) == "" &&
strings.TrimSpace(req.tagRows[nRows-2].key.Text()) == "" {
// when the two last rows are completely empty remove the last one:
last := req.tagRows[nRows-1]
last.hbox.RemoveWidget(last.separator.QWidget)
last.separator.DeleteLater()
last.hbox.RemoveWidget(last.key.QWidget)
last.key.DeleteLater()
for _, val := range last.vals {
last.hbox.RemoveWidget(val.QWidget)
val.DeleteLater()
}
req.tagsLayout.RemoveItem(last.hbox.QLayoutItem)
req.tagRows = req.tagRows[0 : nRows-1]
}
}
for _, val := range values {
addValue(val)
}
}