golang.zx2c4.com/wireguard/windows@v0.5.4-0.20230123132234-dcc0eb72a04b/ui/confview.go (about) 1 /* SPDX-License-Identifier: MIT 2 * 3 * Copyright (C) 2019-2022 WireGuard LLC. All Rights Reserved. 4 */ 5 6 package ui 7 8 import ( 9 "strconv" 10 "strings" 11 "time" 12 13 "github.com/lxn/walk" 14 "github.com/lxn/win" 15 16 "golang.zx2c4.com/wireguard/windows/conf" 17 "golang.zx2c4.com/wireguard/windows/l18n" 18 "golang.zx2c4.com/wireguard/windows/manager" 19 ) 20 21 type widgetsLine interface { 22 widgets() (walk.Widget, walk.Widget) 23 } 24 25 type widgetsLinesView interface { 26 widgetsLines() []widgetsLine 27 } 28 29 type labelStatusLine struct { 30 label *walk.TextLabel 31 statusComposite *walk.Composite 32 statusImage *walk.ImageView 33 statusLabel *walk.LineEdit 34 } 35 36 type labelTextLine struct { 37 label *walk.TextLabel 38 text *walk.TextEdit 39 } 40 41 type toggleActiveLine struct { 42 composite *walk.Composite 43 button *walk.PushButton 44 } 45 46 type interfaceView struct { 47 status *labelStatusLine 48 publicKey *labelTextLine 49 listenPort *labelTextLine 50 mtu *labelTextLine 51 addresses *labelTextLine 52 dns *labelTextLine 53 scripts *labelTextLine 54 table *labelTextLine 55 toggleActive *toggleActiveLine 56 lines []widgetsLine 57 } 58 59 type peerView struct { 60 publicKey *labelTextLine 61 presharedKey *labelTextLine 62 allowedIPs *labelTextLine 63 endpoint *labelTextLine 64 persistentKeepalive *labelTextLine 65 latestHandshake *labelTextLine 66 transfer *labelTextLine 67 lines []widgetsLine 68 } 69 70 type ConfView struct { 71 *walk.ScrollView 72 name *walk.GroupBox 73 interfaze *interfaceView 74 peers map[conf.Key]*peerView 75 tunnelChangedCB *manager.TunnelChangeCallback 76 tunnel *manager.Tunnel 77 updateTicker *time.Ticker 78 } 79 80 func (lsl *labelStatusLine) widgets() (walk.Widget, walk.Widget) { 81 return lsl.label, lsl.statusComposite 82 } 83 84 func (lsl *labelStatusLine) update(state manager.TunnelState) { 85 icon, err := iconForState(state, 14) 86 if err == nil { 87 lsl.statusImage.SetImage(icon) 88 } else { 89 lsl.statusImage.SetImage(nil) 90 } 91 92 s, e := lsl.statusLabel.TextSelection() 93 lsl.statusLabel.SetText(textForState(state, false)) 94 lsl.statusLabel.SetTextSelection(s, e) 95 } 96 97 func (lsl *labelStatusLine) Dispose() { 98 lsl.label.Dispose() 99 lsl.statusComposite.Dispose() 100 } 101 102 func newLabelStatusLine(parent walk.Container) (*labelStatusLine, error) { 103 var err error 104 var disposables walk.Disposables 105 defer disposables.Treat() 106 107 lsl := new(labelStatusLine) 108 109 if lsl.label, err = walk.NewTextLabel(parent); err != nil { 110 return nil, err 111 } 112 disposables.Add(lsl.label) 113 lsl.label.SetText(l18n.Sprintf("Status:")) 114 lsl.label.SetTextAlignment(walk.AlignHFarVNear) 115 116 if lsl.statusComposite, err = walk.NewComposite(parent); err != nil { 117 return nil, err 118 } 119 disposables.Add(lsl.statusComposite) 120 layout := walk.NewHBoxLayout() 121 layout.SetMargins(walk.Margins{}) 122 layout.SetAlignment(walk.AlignHNearVNear) 123 layout.SetSpacing(0) 124 lsl.statusComposite.SetLayout(layout) 125 126 if lsl.statusImage, err = walk.NewImageView(lsl.statusComposite); err != nil { 127 return nil, err 128 } 129 disposables.Add(lsl.statusImage) 130 lsl.statusImage.SetMargin(2) 131 lsl.statusImage.SetMode(walk.ImageViewModeIdeal) 132 133 if lsl.statusLabel, err = walk.NewLineEdit(lsl.statusComposite); err != nil { 134 return nil, err 135 } 136 disposables.Add(lsl.statusLabel) 137 win.SetWindowLong(lsl.statusLabel.Handle(), win.GWL_EXSTYLE, win.GetWindowLong(lsl.statusLabel.Handle(), win.GWL_EXSTYLE)&^win.WS_EX_CLIENTEDGE) 138 lsl.statusLabel.SetReadOnly(true) 139 lsl.statusLabel.SetBackground(walk.NullBrush()) 140 lsl.statusLabel.FocusedChanged().Attach(func() { 141 lsl.statusLabel.SetTextSelection(0, 0) 142 }) 143 lsl.update(manager.TunnelUnknown) 144 lsl.statusLabel.Accessibility().SetRole(walk.AccRoleStatictext) 145 146 disposables.Spare() 147 148 return lsl, nil 149 } 150 151 func (lt *labelTextLine) widgets() (walk.Widget, walk.Widget) { 152 return lt.label, lt.text 153 } 154 155 func (lt *labelTextLine) show(text string) { 156 s, e := lt.text.TextSelection() 157 lt.text.SetText(text) 158 lt.label.SetVisible(true) 159 lt.text.SetVisible(true) 160 lt.text.SetTextSelection(s, e) 161 } 162 163 func (lt *labelTextLine) hide() { 164 lt.text.SetText("") 165 lt.label.SetVisible(false) 166 lt.text.SetVisible(false) 167 } 168 169 func (lt *labelTextLine) Dispose() { 170 lt.label.Dispose() 171 lt.text.Dispose() 172 } 173 174 func newLabelTextLine(fieldName string, parent walk.Container) (*labelTextLine, error) { 175 var err error 176 var disposables walk.Disposables 177 defer disposables.Treat() 178 179 lt := new(labelTextLine) 180 181 if lt.label, err = walk.NewTextLabel(parent); err != nil { 182 return nil, err 183 } 184 disposables.Add(lt.label) 185 lt.label.SetText(fieldName) 186 lt.label.SetTextAlignment(walk.AlignHFarVNear) 187 lt.label.SetVisible(false) 188 189 if lt.text, err = walk.NewTextEdit(parent); err != nil { 190 return nil, err 191 } 192 disposables.Add(lt.text) 193 win.SetWindowLong(lt.text.Handle(), win.GWL_EXSTYLE, win.GetWindowLong(lt.text.Handle(), win.GWL_EXSTYLE)&^win.WS_EX_CLIENTEDGE) 194 lt.text.SetCompactHeight(true) 195 lt.text.SetReadOnly(true) 196 lt.text.SetBackground(walk.NullBrush()) 197 lt.text.SetVisible(false) 198 lt.text.FocusedChanged().Attach(func() { 199 lt.text.SetTextSelection(0, 0) 200 }) 201 lt.text.Accessibility().SetRole(walk.AccRoleStatictext) 202 203 disposables.Spare() 204 205 return lt, nil 206 } 207 208 func (tal *toggleActiveLine) widgets() (walk.Widget, walk.Widget) { 209 return nil, tal.composite 210 } 211 212 func (tal *toggleActiveLine) updateGlobal(globalState manager.TunnelState) { 213 tal.button.SetEnabled(globalState == manager.TunnelStarted || globalState == manager.TunnelStopped) 214 } 215 216 func (tal *toggleActiveLine) update(state manager.TunnelState) { 217 var text string 218 219 switch state { 220 case manager.TunnelStarted: 221 text = l18n.Sprintf("&Deactivate") 222 case manager.TunnelStopped: 223 text = l18n.Sprintf("&Activate") 224 case manager.TunnelStarting, manager.TunnelStopping: 225 text = textForState(state, true) 226 default: 227 text = "" 228 } 229 230 tal.button.SetText(text) 231 tal.button.SetVisible(state != manager.TunnelUnknown) 232 } 233 234 func (tal *toggleActiveLine) Dispose() { 235 tal.composite.Dispose() 236 } 237 238 func newToggleActiveLine(parent walk.Container) (*toggleActiveLine, error) { 239 var err error 240 var disposables walk.Disposables 241 defer disposables.Treat() 242 243 tal := new(toggleActiveLine) 244 245 if tal.composite, err = walk.NewComposite(parent); err != nil { 246 return nil, err 247 } 248 disposables.Add(tal.composite) 249 layout := walk.NewHBoxLayout() 250 layout.SetMargins(walk.Margins{0, 0, 0, 6}) 251 tal.composite.SetLayout(layout) 252 253 if tal.button, err = walk.NewPushButton(tal.composite); err != nil { 254 return nil, err 255 } 256 disposables.Add(tal.button) 257 walk.NewHSpacer(tal.composite) 258 tal.update(manager.TunnelStopped) 259 260 disposables.Spare() 261 262 return tal, nil 263 } 264 265 type labelTextLineItem struct { 266 label string 267 ptr **labelTextLine 268 } 269 270 func createLabelTextLines(items []labelTextLineItem, parent walk.Container, disposables *walk.Disposables) ([]widgetsLine, error) { 271 var err error 272 var disps walk.Disposables 273 defer disps.Treat() 274 275 wls := make([]widgetsLine, len(items)) 276 for i, item := range items { 277 if *item.ptr, err = newLabelTextLine(item.label, parent); err != nil { 278 return nil, err 279 } 280 disps.Add(*item.ptr) 281 if disposables != nil { 282 disposables.Add(*item.ptr) 283 } 284 wls[i] = *item.ptr 285 } 286 287 disps.Spare() 288 289 return wls, nil 290 } 291 292 func newInterfaceView(parent walk.Container) (*interfaceView, error) { 293 var err error 294 var disposables walk.Disposables 295 defer disposables.Treat() 296 297 iv := new(interfaceView) 298 299 if iv.status, err = newLabelStatusLine(parent); err != nil { 300 return nil, err 301 } 302 disposables.Add(iv.status) 303 304 items := []labelTextLineItem{ 305 {l18n.Sprintf("Public key:"), &iv.publicKey}, 306 {l18n.Sprintf("Listen port:"), &iv.listenPort}, 307 {l18n.Sprintf("MTU:"), &iv.mtu}, 308 {l18n.Sprintf("Addresses:"), &iv.addresses}, 309 {l18n.Sprintf("DNS servers:"), &iv.dns}, 310 {l18n.Sprintf("Scripts:"), &iv.scripts}, 311 {l18n.Sprintf("Table:"), &iv.table}, 312 } 313 if iv.lines, err = createLabelTextLines(items, parent, &disposables); err != nil { 314 return nil, err 315 } 316 317 if iv.toggleActive, err = newToggleActiveLine(parent); err != nil { 318 return nil, err 319 } 320 disposables.Add(iv.toggleActive) 321 322 iv.lines = append([]widgetsLine{iv.status}, append(iv.lines, iv.toggleActive)...) 323 324 layoutInGrid(iv, parent.Layout().(*walk.GridLayout)) 325 326 disposables.Spare() 327 328 return iv, nil 329 } 330 331 func newPeerView(parent walk.Container) (*peerView, error) { 332 pv := new(peerView) 333 334 items := []labelTextLineItem{ 335 {l18n.Sprintf("Public key:"), &pv.publicKey}, 336 {l18n.Sprintf("Preshared key:"), &pv.presharedKey}, 337 {l18n.Sprintf("Allowed IPs:"), &pv.allowedIPs}, 338 {l18n.Sprintf("Endpoint:"), &pv.endpoint}, 339 {l18n.Sprintf("Persistent keepalive:"), &pv.persistentKeepalive}, 340 {l18n.Sprintf("Latest handshake:"), &pv.latestHandshake}, 341 {l18n.Sprintf("Transfer:"), &pv.transfer}, 342 } 343 var err error 344 if pv.lines, err = createLabelTextLines(items, parent, nil); err != nil { 345 return nil, err 346 } 347 348 layoutInGrid(pv, parent.Layout().(*walk.GridLayout)) 349 350 return pv, nil 351 } 352 353 func layoutInGrid(view widgetsLinesView, layout *walk.GridLayout) { 354 for i, l := range view.widgetsLines() { 355 w1, w2 := l.widgets() 356 357 if w1 != nil { 358 layout.SetRange(w1, walk.Rectangle{0, i, 1, 1}) 359 } 360 if w2 != nil { 361 layout.SetRange(w2, walk.Rectangle{2, i, 1, 1}) 362 } 363 } 364 } 365 366 func (iv *interfaceView) widgetsLines() []widgetsLine { 367 return iv.lines 368 } 369 370 func (iv *interfaceView) apply(c *conf.Interface) { 371 if IsAdmin { 372 iv.publicKey.show(c.PrivateKey.Public().String()) 373 } else { 374 iv.publicKey.hide() 375 } 376 377 if c.ListenPort > 0 { 378 iv.listenPort.show(strconv.Itoa(int(c.ListenPort))) 379 } else { 380 iv.listenPort.hide() 381 } 382 383 if c.MTU > 0 { 384 iv.mtu.show(strconv.Itoa(int(c.MTU))) 385 } else { 386 iv.mtu.hide() 387 } 388 389 if len(c.Addresses) > 0 { 390 addrStrings := make([]string, len(c.Addresses)) 391 for i, address := range c.Addresses { 392 addrStrings[i] = address.String() 393 } 394 iv.addresses.show(strings.Join(addrStrings[:], l18n.EnumerationSeparator())) 395 } else { 396 iv.addresses.hide() 397 } 398 399 if len(c.DNS)+len(c.DNSSearch) > 0 { 400 addrStrings := make([]string, 0, len(c.DNS)+len(c.DNSSearch)) 401 for _, address := range c.DNS { 402 addrStrings = append(addrStrings, address.String()) 403 } 404 addrStrings = append(addrStrings, c.DNSSearch...) 405 iv.dns.show(strings.Join(addrStrings[:], l18n.EnumerationSeparator())) 406 } else { 407 iv.dns.hide() 408 } 409 410 var scriptsInUse []string 411 if len(c.PreUp) > 0 { 412 scriptsInUse = append(scriptsInUse, l18n.Sprintf("pre-up")) 413 } 414 if len(c.PostUp) > 0 { 415 scriptsInUse = append(scriptsInUse, l18n.Sprintf("post-up")) 416 } 417 if len(c.PreDown) > 0 { 418 scriptsInUse = append(scriptsInUse, l18n.Sprintf("pre-down")) 419 } 420 if len(c.PostDown) > 0 { 421 scriptsInUse = append(scriptsInUse, l18n.Sprintf("post-down")) 422 } 423 if len(scriptsInUse) > 0 { 424 if conf.AdminBool("DangerousScriptExecution") { 425 iv.scripts.show(strings.Join(scriptsInUse, l18n.EnumerationSeparator())) 426 } else { 427 iv.scripts.show(l18n.Sprintf("disabled, per policy")) 428 } 429 } else { 430 iv.scripts.hide() 431 } 432 433 if c.TableOff { 434 iv.table.show(l18n.Sprintf("off")) 435 } else { 436 iv.table.hide() 437 } 438 } 439 440 func (pv *peerView) widgetsLines() []widgetsLine { 441 return pv.lines 442 } 443 444 func (pv *peerView) apply(c *conf.Peer) { 445 if IsAdmin { 446 pv.publicKey.show(c.PublicKey.String()) 447 } else { 448 pv.publicKey.hide() 449 } 450 451 if !c.PresharedKey.IsZero() && IsAdmin { 452 pv.presharedKey.show(l18n.Sprintf("enabled")) 453 } else { 454 pv.presharedKey.hide() 455 } 456 457 if len(c.AllowedIPs) > 0 { 458 addrStrings := make([]string, len(c.AllowedIPs)) 459 for i, address := range c.AllowedIPs { 460 addrStrings[i] = address.String() 461 } 462 pv.allowedIPs.show(strings.Join(addrStrings[:], l18n.EnumerationSeparator())) 463 } else { 464 pv.allowedIPs.hide() 465 } 466 467 if !c.Endpoint.IsEmpty() { 468 pv.endpoint.show(c.Endpoint.String()) 469 } else { 470 pv.endpoint.hide() 471 } 472 473 if c.PersistentKeepalive > 0 { 474 pv.persistentKeepalive.show(strconv.Itoa(int(c.PersistentKeepalive))) 475 } else { 476 pv.persistentKeepalive.hide() 477 } 478 479 if !c.LastHandshakeTime.IsEmpty() { 480 pv.latestHandshake.show(c.LastHandshakeTime.String()) 481 } else { 482 pv.latestHandshake.hide() 483 } 484 485 if c.RxBytes > 0 || c.TxBytes > 0 { 486 pv.transfer.show(l18n.Sprintf("%s received, %s sent", c.RxBytes.String(), c.TxBytes.String())) 487 } else { 488 pv.transfer.hide() 489 } 490 } 491 492 func newPaddedGroupGrid(parent walk.Container) (group *walk.GroupBox, err error) { 493 group, err = walk.NewGroupBox(parent) 494 if err != nil { 495 return nil, err 496 } 497 defer func() { 498 if err != nil { 499 group.Dispose() 500 } 501 }() 502 layout := walk.NewGridLayout() 503 layout.SetMargins(walk.Margins{10, 5, 10, 5}) 504 layout.SetSpacing(0) 505 err = group.SetLayout(layout) 506 if err != nil { 507 return nil, err 508 } 509 spacer, err := walk.NewSpacerWithCfg(group, &walk.SpacerCfg{walk.GrowableHorz | walk.GreedyHorz, walk.Size{10, 0}, false}) 510 if err != nil { 511 return nil, err 512 } 513 layout.SetRange(spacer, walk.Rectangle{1, 0, 1, 1}) 514 return group, nil 515 } 516 517 func NewConfView(parent walk.Container) (*ConfView, error) { 518 var err error 519 var disposables walk.Disposables 520 defer disposables.Treat() 521 522 cv := new(ConfView) 523 if cv.ScrollView, err = walk.NewScrollView(parent); err != nil { 524 return nil, err 525 } 526 disposables.Add(cv) 527 vlayout := walk.NewVBoxLayout() 528 vlayout.SetMargins(walk.Margins{5, 0, 5, 0}) 529 cv.SetLayout(vlayout) 530 if cv.name, err = newPaddedGroupGrid(cv); err != nil { 531 return nil, err 532 } 533 if cv.interfaze, err = newInterfaceView(cv.name); err != nil { 534 return nil, err 535 } 536 cv.interfaze.toggleActive.button.Clicked().Attach(cv.onToggleActiveClicked) 537 cv.peers = make(map[conf.Key]*peerView) 538 cv.tunnelChangedCB = manager.IPCClientRegisterTunnelChange(cv.onTunnelChanged) 539 cv.SetTunnel(nil) 540 globalState, err := manager.IPCClientGlobalState() 541 if err != nil { 542 return nil, err 543 } 544 cv.interfaze.toggleActive.updateGlobal(globalState) 545 546 if err := walk.InitWrapperWindow(cv); err != nil { 547 return nil, err 548 } 549 cv.SetDoubleBuffering(true) 550 cv.updateTicker = time.NewTicker(time.Second) 551 go func() { 552 for range cv.updateTicker.C { 553 if !cv.Visible() || !cv.Form().Visible() || win.IsIconic(cv.Form().Handle()) { 554 continue 555 } 556 if cv.tunnel != nil { 557 tunnel := cv.tunnel 558 var state manager.TunnelState 559 var config conf.Config 560 if state, _ = tunnel.State(); state == manager.TunnelStarted { 561 config, _ = tunnel.RuntimeConfig() 562 } 563 if config.Name == "" { 564 config, _ = tunnel.StoredConfig() 565 } 566 cv.Synchronize(func() { 567 cv.setTunnel(tunnel, &config, state) 568 }) 569 } 570 } 571 }() 572 573 disposables.Spare() 574 575 return cv, nil 576 } 577 578 func (cv *ConfView) Dispose() { 579 if cv.tunnelChangedCB != nil { 580 cv.tunnelChangedCB.Unregister() 581 cv.tunnelChangedCB = nil 582 } 583 if cv.updateTicker != nil { 584 cv.updateTicker.Stop() 585 cv.updateTicker = nil 586 } 587 cv.ScrollView.Dispose() 588 } 589 590 func (cv *ConfView) onToggleActiveClicked() { 591 cv.interfaze.toggleActive.button.SetEnabled(false) 592 go func() { 593 oldState, err := cv.tunnel.Toggle() 594 if err != nil { 595 cv.Synchronize(func() { 596 if oldState == manager.TunnelUnknown { 597 showErrorCustom(cv.Form(), l18n.Sprintf("Failed to determine tunnel state"), err.Error()) 598 } else if oldState == manager.TunnelStopped { 599 showErrorCustom(cv.Form(), l18n.Sprintf("Failed to activate tunnel"), err.Error()) 600 } else if oldState == manager.TunnelStarted { 601 showErrorCustom(cv.Form(), l18n.Sprintf("Failed to deactivate tunnel"), err.Error()) 602 } 603 }) 604 } 605 }() 606 } 607 608 func (cv *ConfView) onTunnelChanged(tunnel *manager.Tunnel, state, globalState manager.TunnelState, err error) { 609 cv.Synchronize(func() { 610 cv.interfaze.toggleActive.updateGlobal(globalState) 611 if cv.tunnel != nil && cv.tunnel.Name == tunnel.Name { 612 cv.interfaze.status.update(state) 613 cv.interfaze.toggleActive.update(state) 614 } 615 }) 616 if cv.tunnel != nil && cv.tunnel.Name == tunnel.Name { 617 var config conf.Config 618 if state == manager.TunnelStarted { 619 config, _ = tunnel.RuntimeConfig() 620 } 621 if config.Name == "" { 622 config, _ = tunnel.StoredConfig() 623 } 624 cv.Synchronize(func() { 625 cv.setTunnel(tunnel, &config, state) 626 }) 627 } 628 } 629 630 func (cv *ConfView) SetTunnel(tunnel *manager.Tunnel) { 631 cv.tunnel = tunnel // XXX: This races with the read in the updateTicker, but it's pointer-sized! 632 633 var config conf.Config 634 var state manager.TunnelState 635 if tunnel != nil { 636 go func() { 637 if state, _ = tunnel.State(); state == manager.TunnelStarted { 638 config, _ = tunnel.RuntimeConfig() 639 } 640 if config.Name == "" { 641 config, _ = tunnel.StoredConfig() 642 } 643 cv.Synchronize(func() { 644 cv.setTunnel(tunnel, &config, state) 645 }) 646 }() 647 } else { 648 cv.setTunnel(tunnel, &config, state) 649 } 650 } 651 652 func (cv *ConfView) setTunnel(tunnel *manager.Tunnel, config *conf.Config, state manager.TunnelState) { 653 if !(cv.tunnel == nil || tunnel == nil || tunnel.Name == cv.tunnel.Name) { 654 return 655 } 656 657 title := l18n.Sprintf("Interface: %s", config.Name) 658 if cv.name.Title() != title { 659 cv.SetSuspended(true) 660 defer cv.SetSuspended(false) 661 cv.name.SetTitle(title) 662 } 663 cv.name.SetVisible(tunnel != nil) 664 665 cv.interfaze.apply(&config.Interface) 666 cv.interfaze.status.update(state) 667 cv.interfaze.toggleActive.update(state) 668 inverse := make(map[*peerView]bool, len(cv.peers)) 669 all := make([]*peerView, 0, len(cv.peers)) 670 for _, pv := range cv.peers { 671 inverse[pv] = true 672 all = append(all, pv) 673 } 674 someMatch := false 675 for _, peer := range config.Peers { 676 _, ok := cv.peers[peer.PublicKey] 677 if ok { 678 someMatch = true 679 break 680 } 681 } 682 for _, peer := range config.Peers { 683 if pv := cv.peers[peer.PublicKey]; (!someMatch && len(all) > 0) || pv != nil { 684 if pv == nil { 685 pv = all[0] 686 all = all[1:] 687 k, e := conf.NewPrivateKeyFromString(pv.publicKey.text.Text()) 688 if e != nil { 689 continue 690 } 691 delete(cv.peers, *k) 692 cv.peers[peer.PublicKey] = pv 693 } 694 pv.apply(&peer) 695 inverse[pv] = false 696 } else { 697 group, err := newPaddedGroupGrid(cv) 698 if err != nil { 699 continue 700 } 701 group.SetTitle(l18n.Sprintf("Peer")) 702 pv, err := newPeerView(group) 703 if err != nil { 704 group.Dispose() 705 continue 706 } 707 pv.apply(&peer) 708 cv.peers[peer.PublicKey] = pv 709 } 710 } 711 for pv, remove := range inverse { 712 if !remove { 713 continue 714 } 715 k, e := conf.NewPrivateKeyFromString(pv.publicKey.text.Text()) 716 if e != nil { 717 continue 718 } 719 delete(cv.peers, *k) 720 groupBox := pv.publicKey.label.Parent().AsContainerBase().Parent().(*walk.GroupBox) 721 groupBox.SetVisible(false) 722 groupBox.Parent().Children().Remove(groupBox) 723 groupBox.Dispose() 724 } 725 }