From 84d9174fc01c223dc7935c2b2bc6f977e6c46331 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Tue, 2 Apr 2019 18:05:12 +0200 Subject: ui: add initial support for [de]activating tunnels Signed-off-by: Alexander Neumann --- ui/confview.go | 119 ++++++++++++++++++++++++++++++++++++++++++++++----- ui/manage_tunnels.go | 24 ++++------- ui/tunnelsview.go | 64 ++++++++++++++++++++++++--- 3 files changed, 176 insertions(+), 31 deletions(-) diff --git a/ui/confview.go b/ui/confview.go index 761db562..215d2889 100644 --- a/ui/confview.go +++ b/ui/confview.go @@ -16,6 +16,7 @@ import ( "github.com/lxn/win" "golang.org/x/sys/windows" "golang.zx2c4.com/wireguard/windows/conf" + "golang.zx2c4.com/wireguard/windows/service" ) type labelTextLine struct { @@ -43,10 +44,13 @@ type peerView struct { type ConfView struct { *walk.ScrollView - name *walk.GroupBox - interfaze *interfaceView - peers map[conf.Key]*peerView + name *walk.GroupBox + status *walk.CustomWidget + interfaze *interfaceView + toggleActive *walk.PushButton + peers map[conf.Key]*peerView + tunnel *service.Tunnel originalWndProc uintptr creatingThread uint32 } @@ -228,10 +232,22 @@ func NewConfView(parent walk.Container) (*ConfView, error) { cv.SetLayout(walk.NewVBoxLayout()) cv.name, _ = newPaddedGroupGrid(cv) cv.interfaze = newInterfaceView(cv.name) + toggleActiveContainer, _ := walk.NewComposite(cv.name) + tacl := walk.NewHBoxLayout() + tacl.SetMargins(walk.Margins{}) + toggleActiveContainer.SetLayout(tacl) + ivVal := reflect.ValueOf(cv.interfaze).Elem() + cv.name.Layout().(*walk.GridLayout).SetRange(toggleActiveContainer, walk.Rectangle{0, ivVal.NumField(), 3, 1}) + cv.toggleActive, _ = walk.NewPushButton(toggleActiveContainer) + cv.toggleActive.SetText("Activate") + cv.toggleActive.Clicked().Attach(cv.onToggleActiveClicked) + walk.NewHSpacer(toggleActiveContainer) cv.peers = make(map[conf.Key]*peerView) cv.creatingThread = windows.GetCurrentThreadId() win.SetWindowLongPtr(cv.Handle(), win.GWLP_USERDATA, uintptr(unsafe.Pointer(cv))) cv.originalWndProc = win.SetWindowLongPtr(cv.Handle(), win.GWL_WNDPROC, crossThreadMessageHijack) + service.IPCClientRegisterTunnelChange(cv.onTunnelChanged) + cv.setTunnel(nil) return cv, nil } @@ -241,21 +257,101 @@ const crossThreadUpdate = win.WM_APP + 17 var crossThreadMessageHijack = windows.NewCallback(func(hwnd win.HWND, msg uint32, wParam, lParam uintptr) uintptr { cv := (*ConfView)(unsafe.Pointer(win.GetWindowLongPtr(hwnd, win.GWLP_USERDATA))) if msg == crossThreadUpdate { - cv.setConfiguration((*conf.Config)(unsafe.Pointer(wParam))) + cv.setTunnel((*service.Tunnel)(unsafe.Pointer(wParam))) return 0 } return win.CallWindowProc(cv.originalWndProc, hwnd, msg, wParam, lParam) }) -func (cv *ConfView) SetConfiguration(c *conf.Config) { +func (cv *ConfView) onToggleActiveClicked() { + state, err := cv.tunnel.State() + if err != nil { + walk.MsgBox(cv.Form(), "Failed to retrieve tunnel state", fmt.Sprintf("Error: %s", err.Error()), walk.MsgBoxIconError) + return + } + + cv.toggleActive.SetEnabled(false) + + switch state { + case service.TunnelStarted: + if err := cv.tunnel.Stop(); err != nil { + walk.MsgBox(cv.Form(), "Failed to stop tunnel", fmt.Sprintf("Error: %s", err.Error()), walk.MsgBoxIconError) + } + + case service.TunnelStopped: + if err := cv.tunnel.Start(); err != nil { + walk.MsgBox(cv.Form(), "Failed to start tunnel", fmt.Sprintf("Error: %s", err.Error()), walk.MsgBoxIconError) + } + + default: + panic("unexpected state") + } + + cv.setTunnel(cv.tunnel) +} + +func (cv *ConfView) onTunnelChanged(tunnel *service.Tunnel, state service.TunnelState, err error) { + if cv.tunnel == nil || cv.tunnel.Name != tunnel.Name { + return + } + + cv.updateTunnelStatus(state) +} + +func (cv *ConfView) updateTunnelStatus(state service.TunnelState) { + cv.toggleActive.SetVisible(cv.tunnel != nil) + + if cv.tunnel == nil { + return + } + + var enabled bool + var text string + + switch state { + case service.TunnelStarted: + enabled, text = true, "Deactivate" + + case service.TunnelStarting: + enabled, text = false, "Activating..." + + case service.TunnelStopped: + enabled, text = true, "Activate" + + case service.TunnelStopping: + enabled, text = false, "Deactivating..." + + default: + enabled, text = false, "Unknown state" + } + + cv.toggleActive.SetEnabled(enabled) + cv.toggleActive.SetText(text) +} + +func (cv *ConfView) SetTunnel(tunnel *service.Tunnel) { if cv.creatingThread == windows.GetCurrentThreadId() { - cv.setConfiguration(c) + cv.setTunnel(tunnel) } else { - cv.SendMessage(crossThreadUpdate, uintptr(unsafe.Pointer(c)), 0) + cv.SendMessage(crossThreadUpdate, uintptr(unsafe.Pointer(tunnel)), 0) } } -func (cv *ConfView) setConfiguration(c *conf.Config) { +func (cv *ConfView) setTunnel(tunnel *service.Tunnel) { + cv.tunnel = tunnel + + var state service.TunnelState + var config conf.Config + if tunnel != nil { + if state, _ = tunnel.State(); state == service.TunnelStarted { + config, _ = tunnel.RuntimeConfig() + } else { + config, _ = tunnel.StoredConfig() + } + } + + cv.name.SetVisible(tunnel != nil) + hasSuspended := false suspend := func() { if !hasSuspended { @@ -268,16 +364,17 @@ func (cv *ConfView) setConfiguration(c *conf.Config) { cv.SetSuspended(false) } }() - title := "Interface: " + c.Name + title := "Interface: " + config.Name if cv.name.Title() != title { cv.name.SetTitle(title) } - cv.interfaze.apply(&c.Interface) + cv.interfaze.apply(&config.Interface) + cv.updateTunnelStatus(state) inverse := make(map[*peerView]bool, len(cv.peers)) for _, pv := range cv.peers { inverse[pv] = true } - for _, peer := range c.Peers { + for _, peer := range config.Peers { if pv := cv.peers[peer.PublicKey]; pv != nil { pv.apply(&peer) inverse[pv] = false diff --git a/ui/manage_tunnels.go b/ui/manage_tunnels.go index 4568d394..6ab20873 100644 --- a/ui/manage_tunnels.go +++ b/ui/manage_tunnels.go @@ -61,10 +61,16 @@ func (mtw *ManageTunnelsWindow) setup() error { tunnelsContainer, _ := walk.NewComposite(splitter) tunnelsContainer.SetLayout(walk.NewVBoxLayout()) + splitter.SetFixed(tunnelsContainer, true) + mtw.tunnelsView, _ = NewTunnelsView(tunnelsContainer) mtw.tunnelsView.ItemActivated().Attach(mtw.onEditTunnel) mtw.tunnelsView.CurrentIndexChanged().Attach(mtw.updateConfView) + service.IPCClientRegisterTunnelChange(func(tunnel *service.Tunnel, state service.TunnelState, err error) { + mtw.tunnelsView.Invalidate() + }) + // ToolBar actions { // HACK: Because of https://github.com/lxn/walk/issues/481 @@ -168,19 +174,7 @@ func (mtw *ManageTunnelsWindow) updateConfView() { return } - currentTunnel := mtw.tunnelsView.CurrentTunnel() - if currentTunnel == nil { - // TODO: config must be non-nil right now - // mtw.confView.SetConfiguration(nil) - return - } - - config, err := currentTunnel.RuntimeConfig() - if err != nil { - return - } - - mtw.confView.SetConfiguration(&config) + mtw.confView.SetTunnel(mtw.tunnelsView.CurrentTunnel()) } func (mtw *ManageTunnelsWindow) runTunnelEdit(tunnel *service.Tunnel) *conf.Config { @@ -199,7 +193,7 @@ func (mtw *ManageTunnelsWindow) runTunnelEdit(tunnel *service.Tunnel) *conf.Conf } else { title = "Edit tunnel" name = tunnel.Name - config, _ = tunnel.RuntimeConfig() + config, _ = tunnel.StoredConfig() } dlg, _ := walk.NewDialog(mtw) @@ -416,7 +410,7 @@ func (mtw *ManageTunnelsWindow) addTunnel(config *conf.Config) { } } - mtw.confView.SetConfiguration(config) + mtw.confView.SetTunnel(&tunnel) } func (mtw *ManageTunnelsWindow) deleteTunnel(tunnel *service.Tunnel) { diff --git a/ui/tunnelsview.go b/ui/tunnelsview.go index c1bba252..6f1deed0 100644 --- a/ui/tunnelsview.go +++ b/ui/tunnelsview.go @@ -17,7 +17,6 @@ type TunnelModel struct { walk.TableModelBase walk.SorterBase - // TODO: also store the state to display a small icon as the first column tunnels []service.Tunnel } @@ -58,7 +57,11 @@ func (t *TunnelModel) Sort(col int, order walk.SortOrder) error { type TunnelsView struct { *walk.TableView - model *TunnelModel + model *TunnelModel + stoppedBrush *walk.SolidColorBrush + startingBrush *walk.SolidColorBrush + startedBrush *walk.SolidColorBrush + statusPen *walk.CosmeticPen } func NewTunnelsView(parent walk.Container) (*TunnelsView, error) { @@ -70,15 +73,66 @@ func NewTunnelsView(parent walk.Container) (*TunnelsView, error) { model := &TunnelModel{} tv.SetModel(model) - tv.SetAlternatingRowBGColor(walk.RGB(239, 239, 239)) tv.SetLastColumnStretched(true) tv.SetHeaderHidden(true) tv.Columns().Add(walk.NewTableViewColumn()) - return &TunnelsView{ + tunnelsView := &TunnelsView{ TableView: tv, model: model, - }, nil + } + + tunnelsView.stoppedBrush, _ = walk.NewSolidColorBrush(walk.RGB(239, 239, 239)) + tunnelsView.AddDisposable(tunnelsView.stoppedBrush) + + tunnelsView.startingBrush, _ = walk.NewSolidColorBrush(walk.RGB(255, 211, 31)) + tunnelsView.AddDisposable(tunnelsView.startingBrush) + + tunnelsView.startedBrush, _ = walk.NewSolidColorBrush(walk.RGB(0, 255, 0)) + tunnelsView.AddDisposable(tunnelsView.startedBrush) + + tunnelsView.statusPen, _ = walk.NewCosmeticPen(walk.PenSolid, walk.RGB(191, 191, 191)) + tunnelsView.AddDisposable(tunnelsView.statusPen) + + tv.SetCellStyler(tunnelsView) + + return tunnelsView, nil +} + +func (tv *TunnelsView) StyleCell(style *walk.CellStyle) { + canvas := style.Canvas() + if canvas == nil { + return + } + + tunnel := tv.model.tunnels[style.Row()] + + var brush *walk.SolidColorBrush + state, _ := tunnel.State() + switch state { + case service.TunnelStarted: + brush = tv.startedBrush + + case service.TunnelStarting: + brush = tv.startingBrush + + default: + brush = tv.stoppedBrush + } + + b := style.Bounds() + + b.X = b.Height + b.Width -= b.Height + canvas.DrawText(tunnel.Name, tv.Font(), 0, b, walk.TextVCenter) + + b.X = 4 + b.Y += 4 + b.Height -= 8 + b.Width = b.Height + + canvas.FillEllipse(brush, b) + canvas.DrawEllipse(tv.statusPen, b) } func (tv *TunnelsView) CurrentTunnel() *service.Tunnel { -- cgit v1.2.3-59-g8ed1b