1026 lines
35 KiB
Go
1026 lines
35 KiB
Go
//go:build windows
|
||
|
||
package main
|
||
|
||
import (
|
||
_ "embed"
|
||
"encoding/binary"
|
||
"fmt"
|
||
"net/url"
|
||
"os"
|
||
"os/exec"
|
||
"runtime"
|
||
"strings"
|
||
"sync"
|
||
"syscall"
|
||
"unsafe"
|
||
)
|
||
|
||
const (
|
||
WS_OVERLAPPED = 0x00000000
|
||
WS_CAPTION = 0x00C00000
|
||
WS_SYSMENU = 0x00080000
|
||
WS_MINIMIZEBOX = 0x00020000
|
||
WS_VISIBLE = 0x10000000
|
||
WS_CHILD = 0x40000000
|
||
WS_TABSTOP = 0x00010000
|
||
ES_AUTOHSCROLL = 0x0080
|
||
ES_MULTILINE = 0x0004
|
||
ES_AUTOVSCROLL = 0x0040
|
||
ES_READONLY = 0x0800
|
||
WS_VSCROLL = 0x00200000
|
||
BS_PUSHBUTTON = 0x00000000
|
||
BS_AUTOCHECKBOX = 0x00000003
|
||
SS_LEFT = 0
|
||
SS_CENTER = 1
|
||
SS_RIGHT = 2
|
||
SW_SHOW = 5
|
||
SW_HIDE = 0
|
||
SW_RESTORE = 9
|
||
WM_DESTROY = 0x0002
|
||
WM_CLOSE = 0x0010
|
||
WM_SETICON = 0x0080
|
||
WM_COMMAND = 0x0111
|
||
WM_TIMER = 0x0113
|
||
WM_PAINT = 0x000F
|
||
WM_CTLCOLORSTATIC = 0x0138
|
||
WM_CTLCOLOREDIT = 0x0133
|
||
WM_SETFONT = 0x0030
|
||
WM_APP = 0x8000
|
||
BN_CLICKED = 0
|
||
BM_GETCHECK = 0x00F0
|
||
BM_SETCHECK = 0x00F1
|
||
BST_CHECKED = 1
|
||
COLOR_WINDOW = 5
|
||
TRANSPARENT = 1
|
||
NIM_ADD = 0
|
||
NIM_MODIFY = 1
|
||
NIM_DELETE = 2
|
||
NIF_MESSAGE = 1
|
||
NIF_ICON = 2
|
||
NIF_TIP = 4
|
||
NIF_INFO = 0x10
|
||
NIIF_INFO = 1
|
||
NIIF_WARNING = 2
|
||
NIIF_ERROR = 3
|
||
WM_LBUTTONDBLCLK = 0x0203
|
||
WM_RBUTTONUP = 0x0205
|
||
MF_STRING = 0
|
||
TPM_RIGHTBUTTON = 0x0002
|
||
IDI_APPLICATION = 32512
|
||
IDC_ARROW = 32512
|
||
ICON_SMALL = 0
|
||
ICON_BIG = 1
|
||
LR_DEFAULTCOLOR = 0
|
||
FW_NORMAL = 400
|
||
FW_SEMIBOLD = 600
|
||
FW_BOLD = 700
|
||
DEFAULT_CHARSET = 1
|
||
OUT_DEFAULT_PRECIS = 0
|
||
CLIP_DEFAULT_PRECIS = 0
|
||
CLEARTYPE_QUALITY = 5
|
||
DEFAULT_PITCH = 0
|
||
NULL_BRUSH = 5
|
||
MB_OK = 0
|
||
MB_ICONINFORMATION = 0x40
|
||
MB_ICONERROR = 0x10
|
||
CF_UNICODETEXT = 13
|
||
GMEM_MOVEABLE = 0x0002
|
||
REG_SZ = 1
|
||
)
|
||
|
||
const (
|
||
idDiag = 1001
|
||
idSettings = 1002
|
||
idExit = 1003
|
||
idSaveSettings = 1101
|
||
idCancelSettings = 1102
|
||
idCopyLog = 1201
|
||
idServerValue = 2001
|
||
idVmixValue = 2002
|
||
idDeviceValue = 2003
|
||
idAccountValue = 2004
|
||
idMatchValue = 2005
|
||
idMappingValue = 2006
|
||
idMainStatus = 2007
|
||
idLastCommand = 2008
|
||
idDeviceID = 2009
|
||
trayMessage = WM_APP + 21
|
||
settingsDoneMsg = WM_APP + 22
|
||
)
|
||
|
||
type POINT struct{ X, Y int32 }
|
||
type RECT struct{ Left, Top, Right, Bottom int32 }
|
||
type MSG struct {
|
||
HWnd uintptr
|
||
Message uint32
|
||
WParam, LParam uintptr
|
||
Time uint32
|
||
Pt POINT
|
||
LPrivate uint32
|
||
}
|
||
type PAINTSTRUCT struct {
|
||
Hdc uintptr
|
||
FErase int32
|
||
RcPaint RECT
|
||
FRestore, FIncUpdate int32
|
||
RgbReserved [32]byte
|
||
}
|
||
type WNDCLASSEX struct {
|
||
CbSize uint32
|
||
Style uint32
|
||
LpfnWndProc uintptr
|
||
CbClsExtra, CbWndExtra int32
|
||
HInstance, HIcon, HCursor, HbrBackground uintptr
|
||
LpszMenuName, LpszClassName *uint16
|
||
HIconSm uintptr
|
||
}
|
||
type NOTIFYICONDATA struct {
|
||
CbSize uint32
|
||
HWnd uintptr
|
||
UID, UFlags, UCallbackMessage uint32
|
||
HIcon uintptr
|
||
SzTip [128]uint16
|
||
DwState, DwStateMask uint32
|
||
SzInfo [256]uint16
|
||
UTimeoutOrVersion uint32
|
||
SzInfoTitle [64]uint16
|
||
DwInfoFlags uint32
|
||
GuidItem [16]byte
|
||
HBalloonIcon uintptr
|
||
}
|
||
|
||
type WinApp struct {
|
||
agent *Agent
|
||
hwnd uintptr
|
||
settingsHwnd uintptr
|
||
diagHwnd uintptr
|
||
controls map[int]uintptr
|
||
settingsControls map[int]uintptr
|
||
diagEdit uintptr
|
||
font, fontSmall, fontBold, fontTitle, fontDiag uintptr
|
||
bgBrush, cardBrush, editBrush uintptr
|
||
tray NOTIFYICONDATA
|
||
iconBig, iconSmall uintptr
|
||
lastTrayStatus string
|
||
lastDiagLog string
|
||
lastTexts map[int]string
|
||
configCache Config
|
||
stateCache AppState
|
||
uiMu sync.Mutex
|
||
settingsResult string
|
||
settingsResultError bool
|
||
reallyExit bool
|
||
}
|
||
|
||
var app *WinApp
|
||
|
||
// smith.ico is embedded directly into agent.exe.
|
||
//
|
||
//go:embed smith.ico
|
||
var smithIcon []byte
|
||
|
||
func iconFromICO(data []byte, desired int) uintptr {
|
||
if len(data) < 6 || binary.LittleEndian.Uint16(data[0:2]) != 0 || binary.LittleEndian.Uint16(data[2:4]) != 1 {
|
||
return 0
|
||
}
|
||
count := int(binary.LittleEndian.Uint16(data[4:6]))
|
||
if count <= 0 || len(data) < 6+count*16 {
|
||
return 0
|
||
}
|
||
bestOff, bestSize, bestScore := 0, 0, int(^uint(0)>>1)
|
||
for i := 0; i < count; i++ {
|
||
e := 6 + i*16
|
||
w := int(data[e])
|
||
h := int(data[e+1])
|
||
if w == 0 {
|
||
w = 256
|
||
}
|
||
if h == 0 {
|
||
h = 256
|
||
}
|
||
sz := int(binary.LittleEndian.Uint32(data[e+8 : e+12]))
|
||
off := int(binary.LittleEndian.Uint32(data[e+12 : e+16]))
|
||
if sz <= 0 || off < 0 || off+sz > len(data) {
|
||
continue
|
||
}
|
||
score := absInt(w-desired) + absInt(h-desired)
|
||
if score < bestScore {
|
||
bestScore = score
|
||
bestOff = off
|
||
bestSize = sz
|
||
}
|
||
}
|
||
if bestSize == 0 {
|
||
return 0
|
||
}
|
||
h, _, _ := pCreateIconFromResourceEx.Call(uintptr(unsafe.Pointer(&data[bestOff])), uintptr(bestSize), 1, 0x00030000, uintptr(desired), uintptr(desired), LR_DEFAULTCOLOR)
|
||
return h
|
||
}
|
||
func absInt(v int) int {
|
||
if v < 0 {
|
||
return -v
|
||
}
|
||
return v
|
||
}
|
||
|
||
var (
|
||
user32 = syscall.NewLazyDLL("user32.dll")
|
||
gdi32 = syscall.NewLazyDLL("gdi32.dll")
|
||
shell32 = syscall.NewLazyDLL("shell32.dll")
|
||
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||
pRegisterClassEx = user32.NewProc("RegisterClassExW")
|
||
pCreateWindowEx = user32.NewProc("CreateWindowExW")
|
||
pDefWindowProc = user32.NewProc("DefWindowProcW")
|
||
pShowWindow = user32.NewProc("ShowWindow")
|
||
pUpdateWindow = user32.NewProc("UpdateWindow")
|
||
pGetMessage = user32.NewProc("GetMessageW")
|
||
pTranslateMessage = user32.NewProc("TranslateMessage")
|
||
pDispatchMessage = user32.NewProc("DispatchMessageW")
|
||
pPostQuitMessage = user32.NewProc("PostQuitMessage")
|
||
pDestroyWindow = user32.NewProc("DestroyWindow")
|
||
pSetTimer = user32.NewProc("SetTimer")
|
||
pSetWindowText = user32.NewProc("SetWindowTextW")
|
||
pSendMessage = user32.NewProc("SendMessageW")
|
||
pLoadIcon = user32.NewProc("LoadIconW")
|
||
pLoadCursor = user32.NewProc("LoadCursorW")
|
||
pCreateIconFromResourceEx = user32.NewProc("CreateIconFromResourceEx")
|
||
pDestroyIcon = user32.NewProc("DestroyIcon")
|
||
pAdjustWindowRectEx = user32.NewProc("AdjustWindowRectEx")
|
||
pBeginPaint = user32.NewProc("BeginPaint")
|
||
pEndPaint = user32.NewProc("EndPaint")
|
||
pInvalidateRect = user32.NewProc("InvalidateRect")
|
||
pMessageBox = user32.NewProc("MessageBoxW")
|
||
pGetWindowTextLength = user32.NewProc("GetWindowTextLengthW")
|
||
pGetWindowText = user32.NewProc("GetWindowTextW")
|
||
pSetForegroundWindow = user32.NewProc("SetForegroundWindow")
|
||
pGetCursorPos = user32.NewProc("GetCursorPos")
|
||
pIsWindowVisible = user32.NewProc("IsWindowVisible")
|
||
pPostMessage = user32.NewProc("PostMessageW")
|
||
pCreatePopupMenu = user32.NewProc("CreatePopupMenu")
|
||
pAppendMenu = user32.NewProc("AppendMenuW")
|
||
pTrackPopupMenu = user32.NewProc("TrackPopupMenu")
|
||
pDestroyMenu = user32.NewProc("DestroyMenu")
|
||
pOpenClipboard = user32.NewProc("OpenClipboard")
|
||
pEmptyClipboard = user32.NewProc("EmptyClipboard")
|
||
pSetClipboardData = user32.NewProc("SetClipboardData")
|
||
pCloseClipboard = user32.NewProc("CloseClipboard")
|
||
pSetBkMode = gdi32.NewProc("SetBkMode")
|
||
pSetBkColor = gdi32.NewProc("SetBkColor")
|
||
pSetTextColor = gdi32.NewProc("SetTextColor")
|
||
pCreateSolidBrush = gdi32.NewProc("CreateSolidBrush")
|
||
pCreateFont = gdi32.NewProc("CreateFontW")
|
||
pGetStockObject = gdi32.NewProc("GetStockObject")
|
||
pSelectObject = gdi32.NewProc("SelectObject")
|
||
pCreatePen = gdi32.NewProc("CreatePen")
|
||
pRoundRect = gdi32.NewProc("RoundRect")
|
||
pDeleteObject = gdi32.NewProc("DeleteObject")
|
||
pShellNotifyIcon = shell32.NewProc("Shell_NotifyIconW")
|
||
pGetModuleHandle = kernel32.NewProc("GetModuleHandleW")
|
||
pGlobalAlloc = kernel32.NewProc("GlobalAlloc")
|
||
pGlobalLock = kernel32.NewProc("GlobalLock")
|
||
pGlobalUnlock = kernel32.NewProc("GlobalUnlock")
|
||
pRtlMoveMemory = kernel32.NewProc("RtlMoveMemory")
|
||
pSetProcessDPIAware = user32.NewProc("SetProcessDPIAware")
|
||
)
|
||
|
||
func utf16(s string) *uint16 { p, _ := syscall.UTF16PtrFromString(s); return p }
|
||
func rgb(r, g, b byte) uintptr { return uintptr(uint32(r) | uint32(g)<<8 | uint32(b)<<16) }
|
||
func loword(v uintptr) int { return int(v & 0xffff) }
|
||
func hiword(v uintptr) int { return int((v >> 16) & 0xffff) }
|
||
|
||
func main() {
|
||
// Win32 windows and their message queue belong to the OS thread that created
|
||
// them. A Go goroutine may otherwise migrate between OS threads, which can
|
||
// manifest as an intermittent "Not responding" GUI. Keep the complete GUI
|
||
// lifetime pinned to one Windows thread.
|
||
runtime.LockOSThread()
|
||
defer runtime.UnlockOSThread()
|
||
|
||
pSetProcessDPIAware.Call()
|
||
path, err := configPath()
|
||
if err != nil {
|
||
return
|
||
}
|
||
cfg, err := loadConfig(path)
|
||
if err != nil {
|
||
return
|
||
}
|
||
agent := NewAgent(path, cfg)
|
||
app = &WinApp{
|
||
agent: agent,
|
||
controls: map[int]uintptr{},
|
||
settingsControls: map[int]uintptr{},
|
||
lastTexts: map[int]string{},
|
||
configCache: cfg,
|
||
}
|
||
if err := app.init(); err != nil {
|
||
return
|
||
}
|
||
// Make the GUI visible before starting any network work. This keeps first
|
||
// launch deterministic even when DNS, the server, or vMix are unavailable.
|
||
if !cfg.StartMinimized {
|
||
pShowWindow.Call(app.hwnd, SW_SHOW)
|
||
pUpdateWindow.Call(app.hwnd)
|
||
}
|
||
app.addTray()
|
||
agent.Start()
|
||
app.refresh()
|
||
if cfg.RunWithWindows {
|
||
// Registry maintenance is not part of startup-critical GUI work.
|
||
go func() { _ = setRunAtStartup(true) }()
|
||
}
|
||
var msg MSG
|
||
for {
|
||
r, _, _ := pGetMessage.Call(uintptr(unsafe.Pointer(&msg)), 0, 0, 0)
|
||
if int32(r) <= 0 {
|
||
break
|
||
}
|
||
pTranslateMessage.Call(uintptr(unsafe.Pointer(&msg)))
|
||
pDispatchMessage.Call(uintptr(unsafe.Pointer(&msg)))
|
||
}
|
||
agent.Stop()
|
||
app.removeTray()
|
||
}
|
||
|
||
func (w *WinApp) init() error {
|
||
inst, _, _ := pGetModuleHandle.Call(0)
|
||
fallback, _, _ := pLoadIcon.Call(0, IDI_APPLICATION)
|
||
cursor, _, _ := pLoadCursor.Call(0, IDC_ARROW)
|
||
w.iconBig = iconFromICO(smithIcon, 32)
|
||
w.iconSmall = iconFromICO(smithIcon, 16)
|
||
if w.iconBig == 0 {
|
||
w.iconBig = fallback
|
||
}
|
||
if w.iconSmall == 0 {
|
||
w.iconSmall = w.iconBig
|
||
}
|
||
w.bgBrush = createBrush(15, 20, 28)
|
||
w.cardBrush = createBrush(23, 30, 41)
|
||
w.editBrush = createBrush(30, 38, 51)
|
||
w.font = createFont(21, FW_NORMAL)
|
||
w.fontSmall = createFont(18, FW_NORMAL)
|
||
w.fontBold = createFont(22, FW_SEMIBOLD)
|
||
w.fontTitle = createFont(32, FW_BOLD)
|
||
w.fontDiag = createFontFace(18, FW_NORMAL, "Consolas")
|
||
class := utf16("HockeyVmixAgent123")
|
||
wc := WNDCLASSEX{CbSize: uint32(unsafe.Sizeof(WNDCLASSEX{})), LpfnWndProc: syscall.NewCallback(mainWndProc), HInstance: inst, HIcon: w.iconBig, HCursor: cursor, HbrBackground: w.bgBrush, LpszClassName: class, HIconSm: w.iconSmall}
|
||
if r, _, e := pRegisterClassEx.Call(uintptr(unsafe.Pointer(&wc))); r == 0 {
|
||
return fmt.Errorf("RegisterClassEx: %v", e)
|
||
}
|
||
// Keep the main window deliberately roomy. Dimensions below are CLIENT area
|
||
// dimensions; AdjustWindowRectEx adds the actual Windows frame/title bar.
|
||
style := uintptr(WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX)
|
||
rect := RECT{Right: 850, Bottom: 760}
|
||
pAdjustWindowRectEx.Call(uintptr(unsafe.Pointer(&rect)), style, 0, 0)
|
||
outerW := rect.Right - rect.Left
|
||
outerH := rect.Bottom - rect.Top
|
||
hwnd, _, e := pCreateWindowEx.Call(0, uintptr(unsafe.Pointer(class)), uintptr(unsafe.Pointer(utf16("HOCKEY vMix AGENT"))), style, 0x80000000, 0x80000000, uintptr(outerW), uintptr(outerH), 0, 0, inst, 0)
|
||
if hwnd == 0 {
|
||
return fmt.Errorf("CreateWindow: %v", e)
|
||
}
|
||
w.hwnd = hwnd
|
||
pSendMessage.Call(hwnd, WM_SETICON, ICON_BIG, w.iconBig)
|
||
pSendMessage.Call(hwnd, WM_SETICON, ICON_SMALL, w.iconSmall)
|
||
w.buildMain()
|
||
// Auxiliary windows are created lazily on the locked GUI thread. Keeping
|
||
// startup to one main window avoids unnecessary Win32 work on first launch.
|
||
pSetTimer.Call(hwnd, 1, 1000, 0)
|
||
return nil
|
||
}
|
||
|
||
func createBrush(r, g, b byte) uintptr { h, _, _ := pCreateSolidBrush.Call(rgb(r, g, b)); return h }
|
||
func createFont(size int, weight int) uintptr {
|
||
return createFontFace(size, weight, "Segoe UI")
|
||
}
|
||
func createFontFace(size int, weight int, face string) uintptr {
|
||
h, _, _ := pCreateFont.Call(uintptr(int32(-size)), 0, 0, 0, uintptr(weight), 0, 0, 0, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, DEFAULT_PITCH, uintptr(unsafe.Pointer(utf16(face))))
|
||
return h
|
||
}
|
||
|
||
func (w *WinApp) label(id int, text string, x, y, width, height int, font uintptr) uintptr {
|
||
return w.control(id, "STATIC", text, WS_CHILD|WS_VISIBLE|SS_LEFT, x, y, width, height, font)
|
||
}
|
||
func (w *WinApp) labelRight(id int, text string, x, y, width, height int, font uintptr) uintptr {
|
||
return w.control(id, "STATIC", text, WS_CHILD|WS_VISIBLE|SS_RIGHT, x, y, width, height, font)
|
||
}
|
||
func (w *WinApp) button(id int, text string, x, y, width, height int) uintptr {
|
||
return w.control(id, "BUTTON", text, WS_CHILD|WS_VISIBLE|WS_TABSTOP|BS_PUSHBUTTON, x, y, width, height, w.fontSmall)
|
||
}
|
||
func (w *WinApp) control(id int, class, text string, style uintptr, x, y, width, height int, font uintptr) uintptr {
|
||
h, _, _ := pCreateWindowEx.Call(0, uintptr(unsafe.Pointer(utf16(class))), uintptr(unsafe.Pointer(utf16(text))), style, uintptr(x), uintptr(y), uintptr(width), uintptr(height), w.hwnd, uintptr(id), 0, 0)
|
||
if h != 0 {
|
||
pSendMessage.Call(h, WM_SETFONT, font, 1)
|
||
w.controls[id] = h
|
||
}
|
||
return h
|
||
}
|
||
|
||
func (w *WinApp) buildMain() {
|
||
w.label(0, "HOCKEY vMix AGENT", 36, 24, 470, 46, w.fontTitle)
|
||
w.label(0, "универсальный мост Web ↔ vMix", 38, 68, 470, 26, w.fontSmall)
|
||
w.labelRight(idMainStatus, "● STARTING", 540, 30, 280, 38, w.fontBold)
|
||
|
||
w.label(0, "СЕРВЕР", 48, 120, 130, 24, w.fontSmall)
|
||
w.label(idServerValue, "Подключение…", 48, 149, 330, 36, w.fontBold)
|
||
w.label(0, "vMix", 468, 120, 130, 24, w.fontSmall)
|
||
w.label(idVmixValue, "Проверка…", 468, 149, 340, 36, w.fontBold)
|
||
|
||
w.label(0, "УСТРОЙСТВО", 48, 232, 160, 24, w.fontSmall)
|
||
w.label(idDeviceValue, "—", 48, 263, 760, 38, w.fontBold)
|
||
w.label(idDeviceID, "Device ID: —", 48, 304, 760, 30, w.fontSmall)
|
||
|
||
w.label(0, "АККАУНТ", 48, 376, 130, 24, w.fontSmall)
|
||
w.label(idAccountValue, "Не прикреплён", 48, 405, 330, 36, w.fontBold)
|
||
w.label(0, "ТЕКУЩИЙ МАТЧ", 468, 376, 190, 24, w.fontSmall)
|
||
w.label(idMatchValue, "—", 468, 405, 340, 36, w.fontBold)
|
||
|
||
w.label(0, "MAPPING", 48, 488, 130, 24, w.fontSmall)
|
||
w.label(idMappingValue, "Будет назначаться сервером", 48, 517, 760, 36, w.fontBold)
|
||
|
||
w.label(0, "ПОСЛЕДНЯЯ КОМАНДА", 48, 590, 220, 24, w.fontSmall)
|
||
w.label(idLastCommand, "Пока команд не было", 48, 619, 760, 32, w.font)
|
||
|
||
w.label(0, "v"+agentVersion+" • Protocol "+fmt.Sprint(protocolVersion), 38, 716, 320, 26, w.fontSmall)
|
||
w.button(idDiag, "Диагностика", 548, 700, 132, 44)
|
||
w.button(idSettings, "Настройки", 694, 700, 126, 44)
|
||
}
|
||
|
||
func mainWndProc(hwnd uintptr, msg uint32, wparam, lparam uintptr) uintptr {
|
||
if app == nil {
|
||
r, _, _ := pDefWindowProc.Call(hwnd, uintptr(msg), wparam, lparam)
|
||
return r
|
||
}
|
||
switch msg {
|
||
case WM_TIMER:
|
||
app.refresh()
|
||
// Do not rewrite a hidden diagnostics edit control. The previous build
|
||
// replaced the entire log every 500 ms forever after Diagnostics had
|
||
// been opened once, which could make the Win32 UI appear frozen.
|
||
if app.diagEdit != 0 && app.diagHwnd != 0 {
|
||
visible, _, _ := pIsWindowVisible.Call(app.diagHwnd)
|
||
if visible != 0 {
|
||
app.refreshDiagnostics()
|
||
}
|
||
}
|
||
return 0
|
||
case settingsDoneMsg:
|
||
app.uiMu.Lock()
|
||
text := app.settingsResult
|
||
isErr := app.settingsResultError
|
||
app.settingsResult = ""
|
||
app.settingsResultError = false
|
||
app.uiMu.Unlock()
|
||
// Successful saves are intentionally silent: the Settings window closes
|
||
// immediately and the Agent reconnects in the background. Only errors use
|
||
// a modal dialog, avoiding another source of perceived UI "freezes".
|
||
if text != "" && isErr {
|
||
message(app.hwnd, "Ошибка настроек", text, MB_ICONERROR)
|
||
}
|
||
return 0
|
||
case WM_COMMAND:
|
||
if hiword(wparam) == BN_CLICKED {
|
||
switch loword(wparam) {
|
||
case idDiag:
|
||
app.openDiagnostics()
|
||
case idSettings:
|
||
app.openSettings()
|
||
}
|
||
}
|
||
return 0
|
||
case WM_PAINT:
|
||
app.paint(hwnd)
|
||
return 0
|
||
case WM_CTLCOLORSTATIC:
|
||
pSetBkMode.Call(wparam, TRANSPARENT)
|
||
id := app.controlID(lparam)
|
||
color := rgb(203, 213, 225)
|
||
if id == idMainStatus {
|
||
status, _ := app.statusTextFor(app.stateCache)
|
||
color = status
|
||
pSetBkMode.Call(wparam, 0)
|
||
pSetBkColor.Call(wparam, rgb(15, 20, 28))
|
||
pSetTextColor.Call(wparam, color)
|
||
return app.bgBrush
|
||
}
|
||
if id == idServerValue {
|
||
if app.stateCache.ServerConnected {
|
||
color = rgb(74, 222, 128)
|
||
} else {
|
||
color = rgb(248, 113, 113)
|
||
}
|
||
}
|
||
if id == idVmixValue {
|
||
if app.stateCache.Vmix.Connected {
|
||
color = rgb(74, 222, 128)
|
||
} else {
|
||
color = rgb(248, 113, 113)
|
||
}
|
||
}
|
||
pSetTextColor.Call(wparam, color)
|
||
b, _, _ := pGetStockObject.Call(NULL_BRUSH)
|
||
return b
|
||
case WM_CLOSE:
|
||
if app.reallyExit {
|
||
pDestroyWindow.Call(hwnd)
|
||
} else {
|
||
pShowWindow.Call(hwnd, SW_HIDE)
|
||
}
|
||
return 0
|
||
case WM_DESTROY:
|
||
app.removeTray()
|
||
pPostQuitMessage.Call(0)
|
||
return 0
|
||
case trayMessage:
|
||
switch uint32(lparam) {
|
||
case WM_LBUTTONDBLCLK:
|
||
app.showMain()
|
||
case WM_RBUTTONUP:
|
||
app.trayMenu()
|
||
}
|
||
return 0
|
||
}
|
||
r, _, _ := pDefWindowProc.Call(hwnd, uintptr(msg), wparam, lparam)
|
||
return r
|
||
}
|
||
|
||
func (w *WinApp) controlID(hwnd uintptr) int {
|
||
for id, h := range w.controls {
|
||
if h == hwnd {
|
||
return id
|
||
}
|
||
}
|
||
return 0
|
||
}
|
||
func (w *WinApp) paint(hwnd uintptr) {
|
||
var ps PAINTSTRUCT
|
||
hdc, _, _ := pBeginPaint.Call(hwnd, uintptr(unsafe.Pointer(&ps)))
|
||
pen, _, _ := pCreatePen.Call(0, 1, rgb(48, 59, 75))
|
||
oldPen, _, _ := pSelectObject.Call(hdc, pen)
|
||
oldBrush, _, _ := pSelectObject.Call(hdc, w.cardBrush)
|
||
for _, r := range []RECT{{30, 104, 415, 202}, {445, 104, 830, 202}, {30, 216, 830, 350}, {30, 364, 415, 460}, {445, 364, 830, 460}, {30, 474, 830, 566}, {30, 578, 830, 676}} {
|
||
pRoundRect.Call(hdc, uintptr(r.Left), uintptr(r.Top), uintptr(r.Right), uintptr(r.Bottom), 14, 14)
|
||
}
|
||
pSelectObject.Call(hdc, oldPen)
|
||
pSelectObject.Call(hdc, oldBrush)
|
||
pDeleteObject.Call(pen)
|
||
pEndPaint.Call(hwnd, uintptr(unsafe.Pointer(&ps)))
|
||
}
|
||
|
||
func (w *WinApp) statusTextFor(s AppState) (uintptr, string) {
|
||
if !s.ServerConnected {
|
||
if s.ServerConnecting {
|
||
return rgb(251, 191, 36), "● CONNECTING"
|
||
}
|
||
return rgb(248, 113, 113), "● SERVER OFFLINE"
|
||
}
|
||
if !s.Vmix.Connected {
|
||
return rgb(248, 113, 113), "● VMIX NOT FOUND"
|
||
}
|
||
if !s.Paired {
|
||
return rgb(251, 191, 36), "● NOT PAIRED"
|
||
}
|
||
if !s.Active {
|
||
return rgb(148, 163, 184), "● STANDBY"
|
||
}
|
||
if s.MatchID == "" {
|
||
return rgb(251, 191, 36), "● WAITING FOR MATCH"
|
||
}
|
||
return rgb(74, 222, 128), "● READY"
|
||
}
|
||
|
||
func (w *WinApp) refresh() {
|
||
// Snapshot shared state once per timer tick. Painting and colour callbacks
|
||
// use this GUI-owned cache and never wait on networking locks.
|
||
s := w.agent.Snapshot()
|
||
cfg := w.agent.Config()
|
||
w.stateCache = s
|
||
w.configCache = cfg
|
||
_, status := w.statusTextFor(s)
|
||
w.setControlText(idMainStatus, status)
|
||
server := "Не подключён"
|
||
if s.ServerConnected {
|
||
server = "Подключён"
|
||
if host := serverHost(s.ConnectedServer); host != "" {
|
||
server += " · " + host
|
||
}
|
||
} else if s.ServerConnecting {
|
||
server = "Подключение…"
|
||
}
|
||
w.setControlText(idServerValue, server)
|
||
vm := "Не найден"
|
||
if s.Vmix.Connected {
|
||
vm = "Подключён"
|
||
if s.Vmix.Version != "" {
|
||
vm += " · " + s.Vmix.Version
|
||
}
|
||
}
|
||
w.setControlText(idVmixValue, vm)
|
||
w.setControlText(idDeviceValue, cfg.DeviceName)
|
||
w.setControlText(idDeviceID, "Device ID: "+cfg.DeviceID)
|
||
account := "Не прикреплён"
|
||
if s.Paired {
|
||
account = s.PairedLogin
|
||
if account == "" {
|
||
account = "Прикреплён"
|
||
}
|
||
if !s.Active {
|
||
account += " · standby"
|
||
}
|
||
}
|
||
w.setControlText(idAccountValue, account)
|
||
match := "—"
|
||
if s.MatchID != "" {
|
||
match = s.MatchID
|
||
}
|
||
w.setControlText(idMatchValue, match)
|
||
mapping := "Не настроен (следующий этап)"
|
||
if s.MappingName != "" {
|
||
mapping = s.MappingName
|
||
if s.MappingVersion != "" {
|
||
mapping += " · v" + s.MappingVersion
|
||
}
|
||
}
|
||
w.setControlText(idMappingValue, mapping)
|
||
cmd := "Пока команд не было"
|
||
if s.LastCommand != "" {
|
||
icon := "✓"
|
||
if !s.LastCommandOK {
|
||
icon = "✕"
|
||
}
|
||
cmd = icon + " " + s.LastCommand + " · " + s.LastCommandAt.Format("15:04:05")
|
||
}
|
||
w.setControlText(idLastCommand, cmd)
|
||
w.updateTray(status, s)
|
||
}
|
||
|
||
func (w *WinApp) setControlText(id int, text string) {
|
||
if w.lastTexts[id] == text {
|
||
return
|
||
}
|
||
w.lastTexts[id] = text
|
||
setText(w.controls[id], text)
|
||
// Repaint only when the visible value actually changed.
|
||
if h := w.controls[id]; h != 0 {
|
||
pInvalidateRect.Call(h, 0, 1)
|
||
}
|
||
pInvalidateRect.Call(w.hwnd, 0, 0)
|
||
}
|
||
|
||
func serverHost(raw string) string {
|
||
u, err := url.Parse(strings.TrimSpace(raw))
|
||
if err == nil && u.Host != "" {
|
||
return u.Host
|
||
}
|
||
return ""
|
||
}
|
||
func shortDevice(v string) string {
|
||
if len(v) <= 22 {
|
||
return v
|
||
}
|
||
return v[:10] + " … " + v[len(v)-8:]
|
||
}
|
||
func setText(hwnd uintptr, text string) {
|
||
if hwnd != 0 {
|
||
pSetWindowText.Call(hwnd, uintptr(unsafe.Pointer(utf16(text))))
|
||
}
|
||
}
|
||
|
||
func (w *WinApp) addTray() {
|
||
icon := w.iconSmall
|
||
if icon == 0 {
|
||
icon, _, _ = pLoadIcon.Call(0, IDI_APPLICATION)
|
||
}
|
||
w.tray = NOTIFYICONDATA{CbSize: uint32(unsafe.Sizeof(NOTIFYICONDATA{})), HWnd: w.hwnd, UID: 1, UFlags: NIF_MESSAGE | NIF_ICON | NIF_TIP, UCallbackMessage: trayMessage, HIcon: icon}
|
||
copyUTF16(w.tray.SzTip[:], "Hockey vMix Agent")
|
||
pShellNotifyIcon.Call(NIM_ADD, uintptr(unsafe.Pointer(&w.tray)))
|
||
}
|
||
func (w *WinApp) removeTray() {
|
||
if w.tray.CbSize != 0 {
|
||
pShellNotifyIcon.Call(NIM_DELETE, uintptr(unsafe.Pointer(&w.tray)))
|
||
w.tray.CbSize = 0
|
||
}
|
||
}
|
||
func (w *WinApp) updateTray(status string, s AppState) {
|
||
if status == w.lastTrayStatus {
|
||
return
|
||
}
|
||
w.lastTrayStatus = status
|
||
w.tray.UFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP
|
||
tip := "Hockey Agent · " + strings.TrimPrefix(status, "● ")
|
||
if s.MatchID != "" {
|
||
tip += " · Match " + s.MatchID
|
||
}
|
||
copyUTF16(w.tray.SzTip[:], tip)
|
||
pShellNotifyIcon.Call(NIM_MODIFY, uintptr(unsafe.Pointer(&w.tray)))
|
||
}
|
||
func copyUTF16(dst []uint16, s string) {
|
||
u, _ := syscall.UTF16FromString(s)
|
||
for i := range dst {
|
||
dst[i] = 0
|
||
}
|
||
copy(dst, u)
|
||
}
|
||
func (w *WinApp) showMain() { pShowWindow.Call(w.hwnd, SW_RESTORE); pSetForegroundWindow.Call(w.hwnd) }
|
||
func (w *WinApp) trayMenu() {
|
||
menu, _, _ := pCreatePopupMenu.Call()
|
||
pAppendMenu.Call(menu, MF_STRING, 1, uintptr(unsafe.Pointer(utf16("Открыть Hockey Agent"))))
|
||
pAppendMenu.Call(menu, MF_STRING, 2, uintptr(unsafe.Pointer(utf16("Диагностика"))))
|
||
pAppendMenu.Call(menu, MF_STRING, 3, uintptr(unsafe.Pointer(utf16("Настройки"))))
|
||
pAppendMenu.Call(menu, MF_STRING, 4, uintptr(unsafe.Pointer(utf16("Выход"))))
|
||
var pt POINT
|
||
pGetCursorPos.Call(uintptr(unsafe.Pointer(&pt)))
|
||
pSetForegroundWindow.Call(w.hwnd)
|
||
cmd, _, _ := pTrackPopupMenu.Call(menu, TPM_RIGHTBUTTON|0x0100, uintptr(pt.X), uintptr(pt.Y), 0, w.hwnd, 0)
|
||
pDestroyMenu.Call(menu)
|
||
switch cmd {
|
||
case 1:
|
||
w.showMain()
|
||
case 2:
|
||
w.openDiagnostics()
|
||
case 3:
|
||
w.openSettings()
|
||
case 4:
|
||
w.reallyExit = true
|
||
pDestroyWindow.Call(w.hwnd)
|
||
}
|
||
}
|
||
|
||
func (w *WinApp) createSettingsWindow() {
|
||
if w.settingsHwnd != 0 {
|
||
return
|
||
}
|
||
w.settingsHwnd = w.simpleWindow("Настройки Hockey Agent", 840, 700, syscall.NewCallback(settingsWndProc))
|
||
y := 34
|
||
w.settingsLabel("Основной Hockey Server URL", 40, y)
|
||
w.settingsEdit(3001, "", 40, y+30, 760)
|
||
y += 108
|
||
w.settingsLabel("Резервный Server URL (необязательно)", 40, y)
|
||
w.settingsEdit(3006, "", 40, y+30, 760)
|
||
y += 108
|
||
w.settingsLabel("vMix API URL", 40, y)
|
||
w.settingsEdit(3002, "", 40, y+30, 760)
|
||
y += 108
|
||
w.settingsLabel("Device name", 40, y)
|
||
w.settingsEdit(3003, "", 40, y+30, 760)
|
||
|
||
w.settingsNote("Для удалённого сервера можно указать https://адрес — Agent сам подключится по WSS.\r\nПри переносе также можно положить server_url.txt рядом с agent.exe: этот адрес будет иметь приоритет.", 40, 470, 760, 58)
|
||
w.settingsCheck(3004, "Запускать вместе с Windows", false, 40, 548)
|
||
w.settingsCheck(3005, "Запускать свёрнутым в tray", false, 40, 590)
|
||
w.settingsButton(idSaveSettings, "Сохранить", 526, 632, 130)
|
||
w.settingsButton(idCancelSettings, "Отмена", 670, 632, 130)
|
||
w.populateSettings()
|
||
}
|
||
|
||
func (w *WinApp) populateSettings() {
|
||
cfg := w.configCache
|
||
setText(w.settingsControls[3001], cfg.ServerURL)
|
||
setText(w.settingsControls[3006], cfg.FallbackServerURL)
|
||
setText(w.settingsControls[3002], cfg.VmixURL)
|
||
setText(w.settingsControls[3003], cfg.DeviceName)
|
||
check := func(id int, value bool) {
|
||
state := uintptr(0)
|
||
if value {
|
||
state = BST_CHECKED
|
||
}
|
||
pSendMessage.Call(w.settingsControls[id], BM_SETCHECK, state, 0)
|
||
}
|
||
check(3004, cfg.RunWithWindows)
|
||
check(3005, cfg.StartMinimized)
|
||
}
|
||
|
||
func (w *WinApp) openSettings() {
|
||
if w.settingsHwnd == 0 {
|
||
w.createSettingsWindow()
|
||
}
|
||
w.populateSettings()
|
||
pShowWindow.Call(w.settingsHwnd, SW_SHOW)
|
||
pSetForegroundWindow.Call(w.settingsHwnd)
|
||
}
|
||
|
||
func (w *WinApp) simpleWindow(title string, clientWidth, clientHeight int, proc uintptr) uintptr {
|
||
inst, _, _ := pGetModuleHandle.Call(0)
|
||
className := utf16(title + "Class123")
|
||
cursor, _, _ := pLoadCursor.Call(0, IDC_ARROW)
|
||
wc := WNDCLASSEX{CbSize: uint32(unsafe.Sizeof(WNDCLASSEX{})), LpfnWndProc: proc, HInstance: inst, HIcon: w.iconBig, HCursor: cursor, HbrBackground: w.bgBrush, LpszClassName: className, HIconSm: w.iconSmall}
|
||
pRegisterClassEx.Call(uintptr(unsafe.Pointer(&wc)))
|
||
style := uintptr(WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU)
|
||
rect := RECT{Right: int32(clientWidth), Bottom: int32(clientHeight)}
|
||
pAdjustWindowRectEx.Call(uintptr(unsafe.Pointer(&rect)), style, 0, 0)
|
||
outerW := rect.Right - rect.Left
|
||
outerH := rect.Bottom - rect.Top
|
||
h, _, _ := pCreateWindowEx.Call(0, uintptr(unsafe.Pointer(className)), uintptr(unsafe.Pointer(utf16(title))), style, 0x80000000, 0x80000000, uintptr(outerW), uintptr(outerH), w.hwnd, 0, inst, 0)
|
||
if h != 0 {
|
||
pSendMessage.Call(h, WM_SETICON, ICON_BIG, w.iconBig)
|
||
pSendMessage.Call(h, WM_SETICON, ICON_SMALL, w.iconSmall)
|
||
}
|
||
return h
|
||
}
|
||
func (w *WinApp) settingsLabel(text string, x, y int) {
|
||
h, _, _ := pCreateWindowEx.Call(0, uintptr(unsafe.Pointer(utf16("STATIC"))), uintptr(unsafe.Pointer(utf16(text))), WS_CHILD|WS_VISIBLE, uintptr(x), uintptr(y), 760, 28, w.settingsHwnd, 0, 0, 0)
|
||
pSendMessage.Call(h, WM_SETFONT, w.fontSmall, 1)
|
||
}
|
||
func (w *WinApp) settingsNote(text string, x, y, width, height int) {
|
||
h, _, _ := pCreateWindowEx.Call(0, uintptr(unsafe.Pointer(utf16("STATIC"))), uintptr(unsafe.Pointer(utf16(text))), WS_CHILD|WS_VISIBLE, uintptr(x), uintptr(y), uintptr(width), uintptr(height), w.settingsHwnd, 0, 0, 0)
|
||
pSendMessage.Call(h, WM_SETFONT, w.fontSmall, 1)
|
||
}
|
||
func (w *WinApp) settingsEdit(id int, text string, x, y, width int) {
|
||
h, _, _ := pCreateWindowEx.Call(0, uintptr(unsafe.Pointer(utf16("EDIT"))), uintptr(unsafe.Pointer(utf16(text))), WS_CHILD|WS_VISIBLE|WS_TABSTOP|ES_AUTOHSCROLL, uintptr(x), uintptr(y), uintptr(width), 46, w.settingsHwnd, uintptr(id), 0, 0)
|
||
pSendMessage.Call(h, WM_SETFONT, w.font, 1)
|
||
w.settingsControls[id] = h
|
||
}
|
||
func (w *WinApp) settingsCheck(id int, text string, checked bool, x, y int) {
|
||
h, _, _ := pCreateWindowEx.Call(0, uintptr(unsafe.Pointer(utf16("BUTTON"))), uintptr(unsafe.Pointer(utf16(text))), WS_CHILD|WS_VISIBLE|WS_TABSTOP|BS_AUTOCHECKBOX, uintptr(x), uintptr(y), 600, 34, w.settingsHwnd, uintptr(id), 0, 0)
|
||
pSendMessage.Call(h, WM_SETFONT, w.font, 1)
|
||
if checked {
|
||
pSendMessage.Call(h, BM_SETCHECK, BST_CHECKED, 0)
|
||
}
|
||
w.settingsControls[id] = h
|
||
}
|
||
func (w *WinApp) settingsButton(id int, text string, x, y, width int) {
|
||
h, _, _ := pCreateWindowEx.Call(0, uintptr(unsafe.Pointer(utf16("BUTTON"))), uintptr(unsafe.Pointer(utf16(text))), WS_CHILD|WS_VISIBLE|WS_TABSTOP|BS_PUSHBUTTON, uintptr(x), uintptr(y), uintptr(width), 46, w.settingsHwnd, uintptr(id), 0, 0)
|
||
pSendMessage.Call(h, WM_SETFONT, w.font, 1)
|
||
}
|
||
func settingsWndProc(hwnd uintptr, msg uint32, wparam, lparam uintptr) uintptr {
|
||
if app == nil {
|
||
r, _, _ := pDefWindowProc.Call(hwnd, uintptr(msg), wparam, lparam)
|
||
return r
|
||
}
|
||
switch msg {
|
||
case WM_COMMAND:
|
||
if hiword(wparam) == BN_CLICKED {
|
||
switch loword(wparam) {
|
||
case idSaveSettings:
|
||
app.saveSettings()
|
||
case idCancelSettings:
|
||
pShowWindow.Call(hwnd, SW_HIDE)
|
||
}
|
||
}
|
||
return 0
|
||
case WM_CLOSE:
|
||
pShowWindow.Call(hwnd, SW_HIDE)
|
||
return 0
|
||
case WM_CTLCOLORSTATIC:
|
||
pSetBkMode.Call(wparam, TRANSPARENT)
|
||
pSetTextColor.Call(wparam, rgb(203, 213, 225))
|
||
b, _, _ := pGetStockObject.Call(NULL_BRUSH)
|
||
return b
|
||
case WM_CTLCOLOREDIT:
|
||
pSetTextColor.Call(wparam, rgb(226, 232, 240))
|
||
pSetBkMode.Call(wparam, TRANSPARENT)
|
||
return app.editBrush
|
||
}
|
||
r, _, _ := pDefWindowProc.Call(hwnd, uintptr(msg), wparam, lparam)
|
||
return r
|
||
}
|
||
func (w *WinApp) saveSettings() {
|
||
cfg := w.configCache
|
||
cfg.ServerURL = getText(w.settingsControls[3001])
|
||
cfg.FallbackServerURL = getText(w.settingsControls[3006])
|
||
cfg.VmixURL = getText(w.settingsControls[3002])
|
||
cfg.DeviceName = getText(w.settingsControls[3003])
|
||
cfg.RunWithWindows = checked(w.settingsControls[3004])
|
||
cfg.StartMinimized = checked(w.settingsControls[3005])
|
||
w.configCache = cfg
|
||
pShowWindow.Call(w.settingsHwnd, SW_HIDE)
|
||
|
||
// Saving may close a stale WebSocket and update Windows startup settings.
|
||
// Those operations are intentionally kept off the GUI thread.
|
||
go func(cfg Config) {
|
||
var result string
|
||
var isErr bool
|
||
if err := w.agent.UpdateConfig(cfg); err != nil {
|
||
result = err.Error()
|
||
isErr = true
|
||
} else if err := setRunAtStartup(cfg.RunWithWindows); err != nil {
|
||
result = "Настройки сохранены, но не удалось обновить автозапуск: " + err.Error()
|
||
isErr = true
|
||
} else {
|
||
result = "Настройки сохранены. Соединение будет обновлено автоматически."
|
||
}
|
||
w.uiMu.Lock()
|
||
w.settingsResult = result
|
||
w.settingsResultError = isErr
|
||
w.uiMu.Unlock()
|
||
pPostMessage.Call(w.hwnd, settingsDoneMsg, 0, 0)
|
||
}(cfg)
|
||
}
|
||
|
||
func checked(hwnd uintptr) bool {
|
||
r, _, _ := pSendMessage.Call(hwnd, BM_GETCHECK, 0, 0)
|
||
return r == BST_CHECKED
|
||
}
|
||
func getText(hwnd uintptr) string {
|
||
n, _, _ := pGetWindowTextLength.Call(hwnd)
|
||
buf := make([]uint16, n+1)
|
||
pGetWindowText.Call(hwnd, uintptr(unsafe.Pointer(&buf[0])), n+1)
|
||
return syscall.UTF16ToString(buf)
|
||
}
|
||
func message(hwnd uintptr, title, text string, flags uintptr) {
|
||
pMessageBox.Call(hwnd, uintptr(unsafe.Pointer(utf16(text))), uintptr(unsafe.Pointer(utf16(title))), MB_OK|flags)
|
||
}
|
||
func setRunAtStartup(enabled bool) error {
|
||
exe, err := os.Executable()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
key := `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`
|
||
if enabled {
|
||
return exec.Command("reg", "add", key, "/v", "HockeyVmixAgent", "/t", "REG_SZ", "/d", `"`+exe+`"`, "/f").Run()
|
||
}
|
||
_ = exec.Command("reg", "delete", key, "/v", "HockeyVmixAgent", "/f").Run()
|
||
return nil
|
||
}
|
||
|
||
func (w *WinApp) createDiagnosticsWindow() {
|
||
if w.diagHwnd != 0 {
|
||
return
|
||
}
|
||
w.diagHwnd = w.simpleWindow("Диагностика Hockey Agent", 1080, 760, syscall.NewCallback(diagWndProc))
|
||
w.diagEdit = w.diagControl("EDIT", "", WS_CHILD|WS_VISIBLE|ES_MULTILINE|ES_AUTOVSCROLL|ES_READONLY|WS_VSCROLL, 28, 28, 1024, 640, 0)
|
||
w.diagControl("BUTTON", "Копировать лог", WS_CHILD|WS_VISIBLE|WS_TABSTOP|BS_PUSHBUTTON, 842, 690, 210, 46, idCopyLog)
|
||
}
|
||
|
||
func (w *WinApp) refreshDiagnostics() {
|
||
logs := w.agent.Logs()
|
||
if logs == w.lastDiagLog {
|
||
return
|
||
}
|
||
w.lastDiagLog = logs
|
||
setText(w.diagEdit, logs)
|
||
}
|
||
|
||
func (w *WinApp) openDiagnostics() {
|
||
if w.diagHwnd == 0 {
|
||
w.createDiagnosticsWindow()
|
||
}
|
||
w.refreshDiagnostics()
|
||
pShowWindow.Call(w.diagHwnd, SW_SHOW)
|
||
pSetForegroundWindow.Call(w.diagHwnd)
|
||
}
|
||
|
||
func (w *WinApp) diagControl(class, text string, style uintptr, x, y, width, height, id int) uintptr {
|
||
h, _, _ := pCreateWindowEx.Call(0, uintptr(unsafe.Pointer(utf16(class))), uintptr(unsafe.Pointer(utf16(text))), style, uintptr(x), uintptr(y), uintptr(width), uintptr(height), w.diagHwnd, uintptr(id), 0, 0)
|
||
font := w.font
|
||
if class == "EDIT" {
|
||
font = w.fontDiag
|
||
}
|
||
pSendMessage.Call(h, WM_SETFONT, font, 1)
|
||
return h
|
||
}
|
||
func diagWndProc(hwnd uintptr, msg uint32, wparam, lparam uintptr) uintptr {
|
||
if app == nil {
|
||
r, _, _ := pDefWindowProc.Call(hwnd, uintptr(msg), wparam, lparam)
|
||
return r
|
||
}
|
||
switch msg {
|
||
case WM_COMMAND:
|
||
if hiword(wparam) == BN_CLICKED && loword(wparam) == idCopyLog {
|
||
copyClipboard(app.agent.Logs())
|
||
message(hwnd, "Диагностика", "Лог скопирован в буфер обмена.", MB_ICONINFORMATION)
|
||
}
|
||
return 0
|
||
case WM_CLOSE:
|
||
pShowWindow.Call(hwnd, SW_HIDE)
|
||
return 0
|
||
case WM_CTLCOLORSTATIC:
|
||
if lparam == app.diagEdit {
|
||
pSetTextColor.Call(wparam, rgb(226, 232, 240))
|
||
pSetBkMode.Call(wparam, TRANSPARENT)
|
||
return app.editBrush
|
||
}
|
||
pSetBkMode.Call(wparam, TRANSPARENT)
|
||
pSetTextColor.Call(wparam, rgb(203, 213, 225))
|
||
b, _, _ := pGetStockObject.Call(NULL_BRUSH)
|
||
return b
|
||
case WM_CTLCOLOREDIT:
|
||
pSetTextColor.Call(wparam, rgb(226, 232, 240))
|
||
pSetBkMode.Call(wparam, TRANSPARENT)
|
||
return app.editBrush
|
||
}
|
||
r, _, _ := pDefWindowProc.Call(hwnd, uintptr(msg), wparam, lparam)
|
||
return r
|
||
}
|
||
func copyClipboard(text string) {
|
||
u, _ := syscall.UTF16FromString(text)
|
||
size := uintptr(len(u) * 2)
|
||
h, _, _ := pGlobalAlloc.Call(GMEM_MOVEABLE, size)
|
||
if h == 0 {
|
||
return
|
||
}
|
||
ptr, _, _ := pGlobalLock.Call(h)
|
||
if ptr == 0 {
|
||
return
|
||
}
|
||
if len(u) > 0 {
|
||
pRtlMoveMemory.Call(ptr, uintptr(unsafe.Pointer(&u[0])), size)
|
||
}
|
||
pGlobalUnlock.Call(h)
|
||
if r, _, _ := pOpenClipboard.Call(app.hwnd); r == 0 {
|
||
return
|
||
}
|
||
pEmptyClipboard.Call()
|
||
pSetClipboardData.Call(CF_UNICODETEXT, h)
|
||
pCloseClipboard.Call()
|
||
}
|