golang.zx2c4.com/wireguard/windows@v0.5.4-0.20230123132234-dcc0eb72a04b/ui/syntax/syntaxedit.go (about) 1 /* SPDX-License-Identifier: MIT 2 * 3 * Copyright (C) 2019-2022 WireGuard LLC. All Rights Reserved. 4 */ 5 6 package syntax 7 8 import ( 9 "errors" 10 "fmt" 11 "strings" 12 "sync/atomic" 13 "syscall" 14 "unsafe" 15 16 "github.com/lxn/walk" 17 "github.com/lxn/win" 18 "golang.org/x/sys/windows" 19 ) 20 21 type SyntaxEdit struct { 22 walk.WidgetBase 23 irich *win.IRichEditOle 24 idoc *win.ITextDocument 25 lastBlockState BlockState 26 yheight int 27 highlightGuard uint32 28 textChangedPublisher walk.EventPublisher 29 privateKeyPublisher walk.StringEventPublisher 30 blockUntunneledTrafficPublisher walk.IntEventPublisher 31 } 32 33 type BlockState int 34 35 const ( 36 InevaluableBlockingUntunneledTraffic BlockState = iota 37 BlockingUntunneledTraffic 38 NotBlockingUntunneledTraffic 39 ) 40 41 func (se *SyntaxEdit) LayoutFlags() walk.LayoutFlags { 42 return walk.GrowableHorz | walk.GrowableVert | walk.GreedyHorz | walk.GreedyVert 43 } 44 45 func (se *SyntaxEdit) MinSizeHint() walk.Size { 46 return walk.Size{20, 12} 47 } 48 49 func (se *SyntaxEdit) SizeHint() walk.Size { 50 return walk.Size{200, 100} 51 } 52 53 func (*SyntaxEdit) CreateLayoutItem(ctx *walk.LayoutContext) walk.LayoutItem { 54 return walk.NewGreedyLayoutItem() 55 } 56 57 func (se *SyntaxEdit) Text() string { 58 textLength := se.SendMessage(win.WM_GETTEXTLENGTH, 0, 0) 59 buf := make([]uint16, textLength+1) 60 se.SendMessage(win.WM_GETTEXT, uintptr(textLength+1), uintptr(unsafe.Pointer(&buf[0]))) 61 return strings.Replace(syscall.UTF16ToString(buf), "\r\n", "\n", -1) 62 } 63 64 func (se *SyntaxEdit) SetText(text string) (err error) { 65 if text == se.Text() { 66 return nil 67 } 68 text = strings.Replace(text, "\n", "\r\n", -1) 69 if win.TRUE != se.SendMessage(win.WM_SETTEXT, 0, uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(text)))) { 70 err = errors.New("WM_SETTEXT failed") 71 } 72 return 73 } 74 75 func (se *SyntaxEdit) TextChanged() *walk.Event { 76 return se.textChangedPublisher.Event() 77 } 78 79 func (se *SyntaxEdit) PrivateKeyChanged() *walk.StringEvent { 80 return se.privateKeyPublisher.Event() 81 } 82 83 func (se *SyntaxEdit) BlockUntunneledTrafficStateChanged() *walk.IntEvent { 84 return se.blockUntunneledTrafficPublisher.Event() 85 } 86 87 type spanStyle struct { 88 color win.COLORREF 89 effects uint32 90 } 91 92 var stylemap = map[highlight]spanStyle{ 93 highlightSection: {color: win.RGB(0x32, 0x6D, 0x74), effects: win.CFE_BOLD}, 94 highlightField: {color: win.RGB(0x9B, 0x23, 0x93), effects: win.CFE_BOLD}, 95 highlightPrivateKey: {color: win.RGB(0x64, 0x38, 0x20)}, 96 highlightPublicKey: {color: win.RGB(0x64, 0x38, 0x20)}, 97 highlightPresharedKey: {color: win.RGB(0x64, 0x38, 0x20)}, 98 highlightIP: {color: win.RGB(0x0E, 0x0E, 0xFF)}, 99 highlightCidr: {color: win.RGB(0x81, 0x5F, 0x03)}, 100 highlightHost: {color: win.RGB(0x0E, 0x0E, 0xFF)}, 101 highlightPort: {color: win.RGB(0x81, 0x5F, 0x03)}, 102 highlightMTU: {color: win.RGB(0x1C, 0x00, 0xCF)}, 103 highlightTable: {color: win.RGB(0x1C, 0x00, 0xCF)}, 104 highlightKeepalive: {color: win.RGB(0x1C, 0x00, 0xCF)}, 105 highlightComment: {color: win.RGB(0x53, 0x65, 0x79), effects: win.CFE_ITALIC}, 106 highlightDelimiter: {color: win.RGB(0x00, 0x00, 0x00)}, 107 highlightCmd: {color: win.RGB(0x63, 0x75, 0x89)}, 108 highlightError: {color: win.RGB(0xC4, 0x1A, 0x16), effects: win.CFE_UNDERLINE}, 109 } 110 111 func (se *SyntaxEdit) evaluateUntunneledBlocking(cfg string, spans []highlightSpan) { 112 state := InevaluableBlockingUntunneledTraffic 113 var onAllowedIPs, 114 onTable, 115 tableOff, 116 seenPeer, 117 seen00v6, 118 seen00v4, 119 seen01v6, 120 seen80001v6, 121 seen01v4, 122 seen1281v4 bool 123 124 for i := range spans { 125 span := &spans[i] 126 switch span.t { 127 case highlightError: 128 goto done 129 case highlightSection: 130 if !strings.EqualFold(cfg[span.s:span.s+span.len], "[Peer]") { 131 break 132 } 133 if !seenPeer { 134 seenPeer = true 135 } else { 136 goto done 137 } 138 case highlightField: 139 onAllowedIPs = strings.EqualFold(cfg[span.s:span.s+span.len], "AllowedIPs") 140 onTable = strings.EqualFold(cfg[span.s:span.s+span.len], "Table") 141 case highlightTable: 142 if onTable { 143 tableOff = cfg[span.s:span.s+span.len] == "off" 144 } 145 case highlightIP: 146 if !onAllowedIPs || !seenPeer { 147 break 148 } 149 if i+2 >= len(spans) || spans[i+1].t != highlightDelimiter || spans[i+2].t != highlightCidr { 150 break 151 } 152 if spans[i+2].len != 1 { 153 break 154 } 155 switch cfg[spans[i+2].s] { 156 case '0': 157 switch cfg[span.s : span.s+span.len] { 158 case "0.0.0.0": 159 seen00v4 = true 160 case "::": 161 seen00v6 = true 162 } 163 case '1': 164 switch cfg[span.s : span.s+span.len] { 165 case "0.0.0.0": 166 seen01v4 = true 167 case "128.0.0.0": 168 seen1281v4 = true 169 case "::": 170 seen01v6 = true 171 case "8000::": 172 seen80001v6 = true 173 } 174 } 175 } 176 } 177 if tableOff { 178 return 179 } 180 181 if seen00v4 || seen00v6 { 182 state = BlockingUntunneledTraffic 183 } else if (seen01v4 && seen1281v4) || (seen01v6 && seen80001v6) { 184 state = NotBlockingUntunneledTraffic 185 } 186 187 done: 188 if state != se.lastBlockState { 189 se.blockUntunneledTrafficPublisher.Publish(int(state)) 190 se.lastBlockState = state 191 } 192 } 193 194 func (se *SyntaxEdit) highlightText() error { 195 if !atomic.CompareAndSwapUint32(&se.highlightGuard, 0, 1) { 196 return nil 197 } 198 defer atomic.StoreUint32(&se.highlightGuard, 0) 199 200 hWnd := se.Handle() 201 gettextlengthex := win.GETTEXTLENGTHEX{ 202 Flags: win.GTL_NUMBYTES, 203 Codepage: win.CP_ACP, // Probably CP_UTF8 would be better, but (wine at least) returns utf32 sizes. 204 } 205 msgSize := uint32(win.SendMessage(hWnd, win.EM_GETTEXTLENGTHEX, uintptr(unsafe.Pointer(&gettextlengthex)), 0)) 206 if msgSize == win.E_INVALIDARG { 207 return errors.New("Failed to get text length") 208 } 209 210 gettextex := win.GETTEXTEX{ 211 Flags: win.GT_NOHIDDENTEXT, 212 Codepage: gettextlengthex.Codepage, 213 Cb: msgSize + 1, 214 } 215 msg := make([]byte, msgSize+1) 216 msgCount := win.SendMessage(hWnd, win.EM_GETTEXTEX, uintptr(unsafe.Pointer(&gettextex)), uintptr(unsafe.Pointer(&msg[0]))) 217 if msgCount < 0 { 218 return errors.New("Failed to get text") 219 } 220 cfg := strings.Replace(string(msg[:msgCount]), "\r", "\n", -1) 221 222 spans := highlightConfig(cfg) 223 se.evaluateUntunneledBlocking(cfg, spans) 224 225 se.idoc.Undo(win.TomSuspend, nil) 226 win.SendMessage(hWnd, win.EM_SETEVENTMASK, 0, 0) 227 win.SendMessage(hWnd, win.WM_SETREDRAW, win.FALSE, 0) 228 var origSelection win.CHARRANGE 229 win.SendMessage(hWnd, win.EM_EXGETSEL, 0, uintptr(unsafe.Pointer(&origSelection))) 230 var origScroll win.POINT 231 win.SendMessage(hWnd, win.EM_GETSCROLLPOS, 0, uintptr(unsafe.Pointer(&origScroll))) 232 win.SendMessage(hWnd, win.EM_HIDESELECTION, win.TRUE, 0) 233 format := win.CHARFORMAT2{ 234 CHARFORMAT: win.CHARFORMAT{ 235 CbSize: uint32(unsafe.Sizeof(win.CHARFORMAT2{})), 236 DwMask: win.CFM_COLOR | win.CFM_CHARSET | win.CFM_SIZE | win.CFM_BOLD | win.CFM_ITALIC | win.CFM_UNDERLINE, 237 DwEffects: win.CFE_AUTOCOLOR, 238 BCharSet: win.ANSI_CHARSET, 239 }, 240 } 241 if se.yheight != 0 { 242 format.YHeight = 20 * 10 243 } 244 win.SendMessage(hWnd, win.EM_SETCHARFORMAT, win.SCF_ALL, uintptr(unsafe.Pointer(&format))) 245 bgColor := win.COLORREF(win.GetSysColor(win.COLOR_WINDOW)) 246 bgInversion := (bgColor & win.RGB(0xFF, 0xFF, 0xFF)) ^ win.RGB(0xFF, 0xFF, 0xFF) 247 win.SendMessage(hWnd, win.EM_SETBKGNDCOLOR, 0, uintptr(bgColor)) 248 numSpans := len(spans) 249 foundPrivateKey := false 250 for i := range spans { 251 span := &spans[i] 252 if numSpans <= 2048 { 253 selection := win.CHARRANGE{int32(span.s), int32(span.s + span.len)} 254 win.SendMessage(hWnd, win.EM_EXSETSEL, 0, uintptr(unsafe.Pointer(&selection))) 255 format.CrTextColor = stylemap[span.t].color ^ bgInversion 256 format.DwEffects = stylemap[span.t].effects 257 win.SendMessage(hWnd, win.EM_SETCHARFORMAT, win.SCF_SELECTION, uintptr(unsafe.Pointer(&format))) 258 } 259 if span.t == highlightPrivateKey && !foundPrivateKey { 260 privateKey := cfg[span.s : span.s+span.len] 261 se.privateKeyPublisher.Publish(privateKey) 262 foundPrivateKey = true 263 } 264 } 265 win.SendMessage(hWnd, win.EM_SETSCROLLPOS, 0, uintptr(unsafe.Pointer(&origScroll))) 266 win.SendMessage(hWnd, win.EM_EXSETSEL, 0, uintptr(unsafe.Pointer(&origSelection))) 267 win.SendMessage(hWnd, win.EM_HIDESELECTION, win.FALSE, 0) 268 win.SendMessage(hWnd, win.WM_SETREDRAW, win.TRUE, 0) 269 win.RedrawWindow(hWnd, nil, 0, win.RDW_ERASE|win.RDW_FRAME|win.RDW_INVALIDATE|win.RDW_ALLCHILDREN) 270 win.SendMessage(hWnd, win.EM_SETEVENTMASK, 0, win.ENM_CHANGE) 271 se.idoc.Undo(win.TomResume, nil) 272 if !foundPrivateKey { 273 se.privateKeyPublisher.Publish("") 274 } 275 return nil 276 } 277 278 func (se *SyntaxEdit) contextMenu(x, y int32) error { 279 /* This disturbing hack grabs the system edit menu normally used for the EDIT control. */ 280 comctl32UTF16, err := windows.UTF16PtrFromString("comctl32.dll") 281 if err != nil { 282 return err 283 } 284 comctl32Handle := win.GetModuleHandle(comctl32UTF16) 285 if comctl32Handle == 0 { 286 return errors.New("Failed to get comctl32.dll handle") 287 } 288 menu := win.LoadMenu(comctl32Handle, win.MAKEINTRESOURCE(1)) 289 if menu == 0 { 290 return errors.New("Failed to load menu") 291 } 292 defer win.DestroyMenu(menu) 293 294 hWnd := se.Handle() 295 enableWhenSelected := uint32(win.MF_GRAYED) 296 var selection win.CHARRANGE 297 win.SendMessage(hWnd, win.EM_EXGETSEL, 0, uintptr(unsafe.Pointer(&selection))) 298 if selection.CpMin < selection.CpMax { 299 enableWhenSelected = win.MF_ENABLED 300 } 301 enableSelectAll := uint32(win.MF_GRAYED) 302 gettextlengthex := win.GETTEXTLENGTHEX{ 303 Flags: win.GTL_DEFAULT, 304 Codepage: win.CP_ACP, 305 } 306 if selection.CpMin != 0 || (selection.CpMax < int32(win.SendMessage(hWnd, win.EM_GETTEXTLENGTHEX, uintptr(unsafe.Pointer(&gettextlengthex)), 0))) { 307 enableSelectAll = win.MF_ENABLED 308 } 309 enableUndo := uint32(win.MF_GRAYED) 310 if win.SendMessage(hWnd, win.EM_CANUNDO, 0, 0) != 0 { 311 enableUndo = win.MF_ENABLED 312 } 313 enablePaste := uint32(win.MF_GRAYED) 314 if win.SendMessage(hWnd, win.EM_CANPASTE, win.CF_TEXT, 0) != 0 { 315 enablePaste = win.MF_ENABLED 316 } 317 318 popup := win.GetSubMenu(menu, 0) 319 win.EnableMenuItem(popup, win.WM_UNDO, win.MF_BYCOMMAND|enableUndo) 320 win.EnableMenuItem(popup, win.WM_CUT, win.MF_BYCOMMAND|enableWhenSelected) 321 win.EnableMenuItem(popup, win.WM_COPY, win.MF_BYCOMMAND|enableWhenSelected) 322 win.EnableMenuItem(popup, win.WM_PASTE, win.MF_BYCOMMAND|enablePaste) 323 win.EnableMenuItem(popup, win.WM_CLEAR, win.MF_BYCOMMAND|enableWhenSelected) 324 win.EnableMenuItem(popup, win.EM_SETSEL, win.MF_BYCOMMAND|enableSelectAll) 325 326 // Delete items that we don't handle. 327 for ctl := win.GetMenuItemCount(popup) - 1; ctl >= 0; ctl-- { 328 menuItem := win.MENUITEMINFO{ 329 CbSize: uint32(unsafe.Sizeof(win.MENUITEMINFO{})), 330 FMask: win.MIIM_FTYPE | win.MIIM_ID, 331 } 332 if !win.GetMenuItemInfo(popup, uint32(ctl), win.MF_BYPOSITION, &menuItem) { 333 continue 334 } 335 if (menuItem.FType & win.MFT_SEPARATOR) != 0 { 336 continue 337 } 338 switch menuItem.WID { 339 case win.WM_UNDO, win.WM_CUT, win.WM_COPY, win.WM_PASTE, win.WM_CLEAR, win.EM_SETSEL: 340 continue 341 } 342 win.DeleteMenu(popup, uint32(ctl), win.MF_BYPOSITION) 343 } 344 // Delete trailing and adjacent separators. 345 end := true 346 for ctl := win.GetMenuItemCount(popup) - 1; ctl >= 0; ctl-- { 347 menuItem := win.MENUITEMINFO{ 348 CbSize: uint32(unsafe.Sizeof(win.MENUITEMINFO{})), 349 FMask: win.MIIM_FTYPE, 350 } 351 if !win.GetMenuItemInfo(popup, uint32(ctl), win.MF_BYPOSITION, &menuItem) { 352 continue 353 } 354 if (menuItem.FType & win.MFT_SEPARATOR) == 0 { 355 end = false 356 continue 357 } 358 if !end && ctl > 0 { 359 if !win.GetMenuItemInfo(popup, uint32(ctl-1), win.MF_BYPOSITION, &menuItem) { 360 continue 361 } 362 if (menuItem.FType & win.MFT_SEPARATOR) == 0 { 363 continue 364 } 365 } 366 win.DeleteMenu(popup, uint32(ctl), win.MF_BYPOSITION) 367 } 368 369 if x == -1 && y == -1 { 370 var rect win.RECT 371 win.GetWindowRect(hWnd, &rect) 372 x = (rect.Left + rect.Right) / 2 373 y = (rect.Top + rect.Bottom) / 2 374 } 375 376 if win.GetFocus() != hWnd { 377 win.SetFocus(hWnd) 378 } 379 380 cmd := win.TrackPopupMenu(popup, win.TPM_LEFTALIGN|win.TPM_RIGHTBUTTON|win.TPM_RETURNCMD|win.TPM_NONOTIFY, x, y, 0, hWnd, nil) 381 if cmd != 0 { 382 lParam := uintptr(0) 383 if cmd == win.EM_SETSEL { 384 lParam = ^uintptr(0) 385 } 386 win.SendMessage(hWnd, cmd, 0, lParam) 387 } 388 389 return nil 390 } 391 392 func (*SyntaxEdit) NeedsWmSize() bool { 393 return true 394 } 395 396 func (se *SyntaxEdit) WndProc(hWnd win.HWND, msg uint32, wParam, lParam uintptr) uintptr { 397 switch msg { 398 case win.WM_DESTROY: 399 if se.idoc != nil { 400 se.idoc.Release() 401 } 402 if se.irich != nil { 403 se.irich.Release() 404 } 405 406 case win.WM_SETTEXT: 407 ret := se.WidgetBase.WndProc(hWnd, msg, wParam, lParam) 408 se.highlightText() 409 win.SendMessage(hWnd, win.EM_EMPTYUNDOBUFFER, 0, 0) 410 se.textChangedPublisher.Publish() 411 return ret 412 413 case win.WM_COMMAND, win.WM_NOTIFY: 414 switch win.HIWORD(uint32(wParam)) { 415 case win.EN_CHANGE: 416 se.highlightText() 417 se.textChangedPublisher.Publish() 418 } 419 420 case win.WM_PASTE: 421 win.SendMessage(hWnd, win.EM_PASTESPECIAL, win.CF_TEXT, 0) 422 return 0 423 424 case win.WM_KEYDOWN: 425 key := win.LOWORD(uint32(wParam)) 426 if key == 'V' && win.GetKeyState(win.VK_CONTROL) < 0 || 427 key == win.VK_INSERT && win.GetKeyState(win.VK_SHIFT) < 0 { 428 win.SendMessage(hWnd, win.EM_PASTESPECIAL, win.CF_TEXT, 0) 429 return 0 430 } 431 432 case win.WM_CONTEXTMENU: 433 se.contextMenu(win.GET_X_LPARAM(lParam), win.GET_Y_LPARAM(lParam)) 434 return 0 435 436 case win.WM_THEMECHANGED: 437 se.highlightText() 438 439 case win.WM_GETDLGCODE: 440 m := (*win.MSG)(unsafe.Pointer(lParam)) 441 ret := se.WidgetBase.WndProc(hWnd, msg, wParam, lParam) 442 ret &^= win.DLGC_WANTTAB 443 if m != nil && m.Message == win.WM_KEYDOWN && m.WParam == win.VK_TAB && win.GetKeyState(win.VK_CONTROL) >= 0 { 444 ret &^= win.DLGC_WANTMESSAGE 445 } 446 return ret 447 } 448 449 return se.WidgetBase.WndProc(hWnd, msg, wParam, lParam) 450 } 451 452 func NewSyntaxEdit(parent walk.Container) (*SyntaxEdit, error) { 453 const LOAD_LIBRARY_SEARCH_SYSTEM32 = 0x00000800 454 _, err := windows.LoadLibraryEx("msftedit.dll", 0, LOAD_LIBRARY_SEARCH_SYSTEM32) 455 if err != nil { 456 return nil, fmt.Errorf("Failed to load msftedit.dll: %w", err) 457 } 458 459 se := &SyntaxEdit{} 460 if err := walk.InitWidget( 461 se, 462 parent, 463 win.MSFTEDIT_CLASS, 464 win.WS_CHILD|win.ES_MULTILINE|win.WS_VISIBLE|win.WS_VSCROLL|win.WS_BORDER|win.WS_HSCROLL|win.WS_TABSTOP|win.ES_WANTRETURN|win.ES_NOOLEDRAGDROP, 465 0); err != nil { 466 return nil, err 467 } 468 hWnd := se.Handle() 469 win.SetWindowLong(hWnd, win.GWL_EXSTYLE, win.GetWindowLong(hWnd, win.GWL_EXSTYLE)&^win.WS_EX_CLIENTEDGE) 470 win.SendMessage(hWnd, win.EM_GETOLEINTERFACE, 0, uintptr(unsafe.Pointer(&se.irich))) 471 var idoc unsafe.Pointer 472 se.irich.QueryInterface(&win.IID_ITextDocument, &idoc) 473 se.idoc = (*win.ITextDocument)(idoc) 474 win.SendMessage(hWnd, win.EM_SETEVENTMASK, 0, win.ENM_CHANGE) 475 win.SendMessage(hWnd, win.EM_SETTEXTMODE, win.TM_SINGLECODEPAGE, 0) 476 se.ApplyDPI(parent.DPI()) 477 se.GraphicsEffects().Add(walk.InteractionEffect) 478 se.GraphicsEffects().Add(walk.FocusEffect) 479 se.MustRegisterProperty("Text", walk.NewProperty( 480 func() any { 481 return se.Text() 482 }, 483 func(v any) error { 484 if s, ok := v.(string); ok { 485 return se.SetText(s) 486 } 487 return se.SetText("") 488 }, 489 se.textChangedPublisher.Event())) 490 return se, nil 491 } 492 493 func (se *SyntaxEdit) ApplyDPI(dpi int) { 494 hWnd := se.Handle() 495 hdc := win.GetDC(hWnd) 496 logPixels := win.GetDeviceCaps(hdc, win.LOGPIXELSY) 497 if se.yheight != 0 { 498 win.SendMessage(hWnd, win.EM_SETZOOM, uintptr(logPixels), uintptr(dpi)) 499 } 500 se.yheight = 20 * 10 * dpi / int(logPixels) 501 win.ReleaseDC(hWnd, hdc) 502 se.highlightText() 503 }