diff options
-rw-r--r-- | ui/syntax/confview.c | 175 | ||||
-rw-r--r-- | ui/syntax/confview.go | 158 | ||||
-rw-r--r-- | ui/syntax/confview.h | 20 |
3 files changed, 353 insertions, 0 deletions
diff --git a/ui/syntax/confview.c b/ui/syntax/confview.c new file mode 100644 index 00000000..9083c252 --- /dev/null +++ b/ui/syntax/confview.c @@ -0,0 +1,175 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2019 WireGuard LLC. All Rights Reserved. + */ + +#include <stdlib.h> +#include <stdbool.h> +#include <stdint.h> +#include <string.h> +#include <assert.h> +#include <windows.h> +#include <richedit.h> +#include <richole.h> +#include <tom.h> + +#include "confview.h" +#include "highlighter.h" + +static WNDPROC parent_proc; + +static void set_rtf(HWND hWnd, const char *str) +{ + SETTEXTEX settextex = { + .flags = ST_DEFAULT, + .codepage = CP_ACP + }; + CHARRANGE orig_selection; + POINT original_scroll; + + SendMessage(hWnd, WM_SETREDRAW, FALSE, 0); + SendMessage(hWnd, EM_EXGETSEL, 0, (LPARAM)&orig_selection); + SendMessage(hWnd, EM_GETSCROLLPOS, 0, (LPARAM)&original_scroll); + SendMessage(hWnd, EM_HIDESELECTION, TRUE, 0); + SendMessage(hWnd, EM_SETTEXTEX, (WPARAM)&settextex, (LPARAM)str); + SendMessage(hWnd, EM_SETSCROLLPOS, 0, (LPARAM)&original_scroll); + SendMessage(hWnd, EM_EXSETSEL, 0, (LPARAM)&orig_selection); + SendMessage(hWnd, EM_HIDESELECTION, FALSE, 0); + SendMessage(hWnd, WM_SETREDRAW, TRUE, 0); + HideCaret(hWnd); + RedrawWindow(hWnd, NULL, NULL, RDW_ERASE | RDW_FRAME | RDW_INVALIDATE | RDW_ALLCHILDREN); +} + +static void context_menu(HWND hWnd, INT x, INT y) +{ + GETTEXTLENGTHEX gettextlengthex = { + .flags = GTL_DEFAULT, + .codepage = CP_ACP + }; + /* This disturbing hack grabs the system edit menu normally used for the EDIT control. */ + HMENU popup, menu = LoadMenuW(GetModuleHandleW(L"comctl32.dll"), MAKEINTRESOURCEW(1)); + CHARRANGE selection = { 0 }; + bool has_selection, can_selectall; + UINT cmd; + + if (!menu) + return; + + SendMessage(hWnd, EM_EXGETSEL, 0, (LPARAM)&selection); + has_selection = selection.cpMax - selection.cpMin; + can_selectall = selection.cpMin || (selection.cpMax < SendMessage(hWnd, EM_GETTEXTLENGTHEX, (WPARAM)&gettextlengthex, 0)); + + popup = GetSubMenu(menu, 0); + EnableMenuItem(popup, WM_COPY, MF_BYCOMMAND | (has_selection ? MF_ENABLED : MF_GRAYED)); + EnableMenuItem(popup, EM_SETSEL, MF_BYCOMMAND | (can_selectall ? MF_ENABLED : MF_GRAYED)); + + /* Delete items that we don't handle. */ + for (int ctl = GetMenuItemCount(popup) - 1; ctl >= 0; --ctl) { + MENUITEMINFOW menu_item = { + .cbSize = sizeof(MENUITEMINFOW), + .fMask = MIIM_FTYPE | MIIM_ID + }; + if (!GetMenuItemInfoW(popup, ctl, MF_BYPOSITION, &menu_item)) + continue; + if (menu_item.fType & MFT_SEPARATOR) + continue; + switch (menu_item.wID) { + case WM_COPY: + case EM_SETSEL: + continue; + } + DeleteMenu(popup, ctl, MF_BYPOSITION); + } + /* Delete trailing and adjacent separators. */ + for (int ctl = GetMenuItemCount(popup) - 1, end = true; ctl >= 0; --ctl) { + MENUITEMINFOW menu_item = { + .cbSize = sizeof(MENUITEMINFOW), + .fMask = MIIM_FTYPE + }; + if (!GetMenuItemInfoW(popup, ctl, MF_BYPOSITION, &menu_item)) + continue; + if (!(menu_item.fType & MFT_SEPARATOR)) { + end = false; + continue; + } + if (!end && ctl) { + if (!GetMenuItemInfoW(popup, ctl - 1, MF_BYPOSITION, &menu_item)) + continue; + if (!(menu_item.fType & MFT_SEPARATOR)) + continue; + } + DeleteMenu(popup, ctl, MF_BYPOSITION); + } + + if (x == -1 && y == -1) { + RECT rect; + GetWindowRect(hWnd, &rect); + x = rect.left + (rect.right - rect.left) / 2; + y = rect.top + (rect.bottom - rect.top) / 2; + } + + if (GetFocus() != hWnd) + SetFocus(hWnd); + + cmd = TrackPopupMenu(popup, TPM_LEFTALIGN | TPM_RIGHTBUTTON | TPM_RETURNCMD | TPM_NONOTIFY, x, y, 0, hWnd, NULL); + if (cmd) + SendMessage(hWnd, cmd, 0, cmd == EM_SETSEL ? -1 : 0); + + DestroyMenu(menu); +} + +static LRESULT CALLBACK child_proc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam) +{ + switch (Msg) { + case WM_CREATE: + HideCaret(hWnd); + break; + case WM_LBUTTONDOWN: + case WM_SETFOCUS: { + LRESULT ret = parent_proc(hWnd, Msg, wParam, lParam); + HideCaret(hWnd); + return ret; + } + case WM_SETCURSOR: + return 0; + case PV_NEWRTF: + set_rtf(hWnd, (const char *)wParam); + return 0; + case WM_CONTEXTMENU: + context_menu(hWnd, LOWORD(lParam), HIWORD(lParam)); + return 0; + } + return parent_proc(hWnd, Msg, wParam, lParam); +} + +bool register_conf_view(void) +{ + WNDCLASSEXW class = { .cbSize = sizeof(WNDCLASSEXW) }; + WNDPROC pp; + HANDLE lib; + + if (parent_proc) + return true; + + lib = LoadLibraryW(L"msftedit.dll"); + if (!lib) + return false; + + if (!GetClassInfoExW(NULL, L"RICHEDIT50W", &class)) + goto err; + pp = class.lpfnWndProc; + if (!pp) + goto err; + class.cbSize = sizeof(WNDCLASSEXW); + class.hInstance = GetModuleHandleW(NULL); + class.lpszClassName = L"WgConfView"; + class.lpfnWndProc = child_proc; + if (!RegisterClassExW(&class)) + goto err; + parent_proc = pp; + return true; + +err: + FreeLibrary(lib); + return false; +} diff --git a/ui/syntax/confview.go b/ui/syntax/confview.go new file mode 100644 index 00000000..b88a7702 --- /dev/null +++ b/ui/syntax/confview.go @@ -0,0 +1,158 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2019 WireGuard LLC. All Rights Reserved. + */ + +package syntax + +import ( + "fmt" + "strconv" + "strings" + "unsafe" + + "github.com/lxn/walk" + "golang.zx2c4.com/wireguard/windows/conf" +) + +// #include "confview.h" +import "C" + +type ConfView struct { + walk.WidgetBase + lastRtf string +} + +func init() { + C.register_conf_view() +} + +func (cv *ConfView) LayoutFlags() walk.LayoutFlags { + return walk.GrowableHorz | walk.GrowableVert | walk.GreedyHorz | walk.GreedyVert +} + +func (cv *ConfView) MinSizeHint() walk.Size { + return walk.Size{20, 12} +} + +func (cv *ConfView) SizeHint() walk.Size { + return walk.Size{200, 100} +} + +func (cv *ConfView) SetConfiguration(conf *conf.Config) { + var output strings.Builder + + if conf == nil { + t := byte(0) + cv.SendMessage(C.PV_NEWRTF, uintptr(unsafe.Pointer(&t)), 0) + return + } + + escape := func(s string) string { + var o strings.Builder + for i := 0; i < len(s); i++ { + if s[i] > 127 || s[i] == '}' || s[i] == '{' || s[i] == '\\' { + o.WriteString(fmt.Sprintf("\\'%d", s[i])) + continue + } + o.WriteByte(s[i]) + } + return o.String() + } + field := func(key, value string) { + output.WriteString(fmt.Sprintf("{\\b %s:} %s\\par", escape(key), escape(value))) + } + + output.WriteString("{\\rtf1\\ansi\\fs20") + + field("Interface", conf.Name) + field("Public Key", conf.Interface.PrivateKey.Public().String()) + if conf.Interface.ListenPort > 0 { + field("Listen Port", strconv.Itoa(int(conf.Interface.ListenPort))) + } + + if conf.Interface.Mtu > 0 { + field("MTU", strconv.Itoa(int(conf.Interface.Mtu))) + } + + if len(conf.Interface.Addresses) > 0 { + addrStrings := make([]string, len(conf.Interface.Addresses)) + for i, address := range conf.Interface.Addresses { + addrStrings[i] = address.String() + } + field("Address", strings.Join(addrStrings[:], ", ")) + } + + if len(conf.Interface.Dns) > 0 { + addrStrings := make([]string, len(conf.Interface.Dns)) + for i, address := range conf.Interface.Dns { + addrStrings[i] = address.String() + } + field("DNS", strings.Join(addrStrings[:], ", ")) + } + + for _, peer := range conf.Peers { + output.WriteString("\\par") + field("Peer", peer.PublicKey.String()) + + if !peer.PresharedKey.IsZero() { + output.WriteString("{\\b Preshared Key:} {\\i enabled}\\par") + } + + if len(peer.AllowedIPs) > 0 { + addrStrings := make([]string, len(peer.AllowedIPs)) + for i, address := range peer.AllowedIPs { + addrStrings[i] = address.String() + } + field("Allowed IPs", strings.Join(addrStrings[:], ", ")) + } + + if !peer.Endpoint.IsEmpty() { + field("Endpoint", peer.Endpoint.String()) + } + + if peer.PersistentKeepalive > 0 { + field("Persistent Keepalive", strconv.Itoa(int(peer.PersistentKeepalive))) + } + + if !peer.LastHandshakeTime.IsEmpty() { + field("Latest Handshake", peer.LastHandshakeTime.String()) + } + + if peer.RxBytes > 0 || peer.TxBytes > 0 { + field("Transfer", fmt.Sprintf("%s received, %s sent", peer.RxBytes.String(), peer.TxBytes.String())) + } + } + + output.WriteString("}") + + text := output.String() + if text == cv.lastRtf { + return + } + cv.lastRtf = text + + t := C.CString(text) + cv.SendMessage(C.PV_NEWRTF, uintptr(unsafe.Pointer(t)), 0) + C.free(unsafe.Pointer(t)) +} + +func NewConfView(parent walk.Container) (*ConfView, error) { + cv := &ConfView{ + lastRtf: "", + } + err := walk.InitWidget( + cv, + parent, + "WgConfView", + C.CONFVIEW_STYLE, + C.CONFVIEW_EXTSTYLE, + ) + if err != nil { + return nil, err + } + + cv.GraphicsEffects().Add(walk.InteractionEffect) + cv.GraphicsEffects().Add(walk.FocusEffect) + return cv, nil +} diff --git a/ui/syntax/confview.h b/ui/syntax/confview.h new file mode 100644 index 00000000..d415dfe8 --- /dev/null +++ b/ui/syntax/confview.h @@ -0,0 +1,20 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2019 WireGuard LLC. All Rights Reserved. + */ + +#ifndef CONFVIEW_H +#define CONFVIEW_H + +#include <stdbool.h> +#include <windows.h> +#include <richedit.h> + +#define CONFVIEW_STYLE (WS_CHILD | WS_CLIPSIBLINGS | ES_MULTILINE | WS_VISIBLE | WS_VSCROLL | ES_READONLY | WS_TABSTOP | ES_WANTRETURN | ES_NOOLEDRAGDROP) +#define CONFVIEW_EXTSTYLE (WS_EX_TRANSPARENT) + +#define PV_NEWRTF (WM_USER + 0x3200) + +extern bool register_conf_view(void); + +#endif |