feat: mudar o foco da janela quando for pro client
This commit is contained in:
parent
5d016d7087
commit
1de5a60843
@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"permissions": {
|
"permissions": {
|
||||||
"allow": [
|
"allow": [
|
||||||
"Bash(go build:*)"
|
"Bash(go build:*)",
|
||||||
|
"Bash(git:*)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
23
CLAUDE.md
23
CLAUDE.md
@ -1,4 +1,8 @@
|
|||||||
# CLAUDE.md — KVMote (Go/Wails)
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
# KVMote (Go/Wails)
|
||||||
|
|
||||||
Reescrita em Go do KVMote (original em C#/WinForms em `C:\vscode\KVMote`).
|
Reescrita em Go do KVMote (original em C#/WinForms em `C:\vscode\KVMote`).
|
||||||
|
|
||||||
@ -30,16 +34,21 @@ Hardware suportado: ESP32-S3 (BLE NUS) e Arduino Leonardo + HC-06 (Serial SPP).
|
|||||||
|
|
||||||
```
|
```
|
||||||
main.go — entry point Wails, bind App
|
main.go — entry point Wails, bind App
|
||||||
app.go — App struct, métodos expostos ao frontend
|
app.go — App struct, métodos expostos ao frontend; implementa WindowManager
|
||||||
internal/
|
internal/
|
||||||
transport/
|
transport/
|
||||||
transport.go — interface Transport (Detect/Connect/Send/SendLossy...)
|
transport.go — interface Transport (Detect/Connect/Send/SendLossy...)
|
||||||
ble_windows.go — BLE NUS via tinygo/bluetooth
|
ble_windows.go — BLE NUS via tinygo/bluetooth
|
||||||
kvm/
|
kvm/
|
||||||
engine.go — lógica KVM: mouse, teclado, clipboard, modo cliente
|
engine.go — lógica KVM: mouse, teclado, clipboard, modo cliente
|
||||||
|
WindowManager interface (SetMiniMode/RestoreNormalMode)
|
||||||
|
LogDebug → kvmote_debug.log (arquivo na raiz)
|
||||||
input/
|
input/
|
||||||
input.go — interface InputHandler + tipos (Point, MouseEvent, KeyboardEvent)
|
input.go — interface InputHandler + tipos (Point, MouseEvent, KeyboardEvent)
|
||||||
input_windows.go — hooks Win32, SetCursorPos, ShowCursor, GetSystemMetrics
|
input_windows.go — hooks Win32, SetCursorPos, ShowCursor, GetSystemMetrics
|
||||||
|
overlay_windows.go — janela Win32 nativa independente do Wails (neon "KVMote")
|
||||||
|
aparece no canto durante modo cliente; usa sua própria goroutine
|
||||||
|
com LockOSThread + message pump próprio
|
||||||
frontend/
|
frontend/
|
||||||
dist/index.html — UI
|
dist/index.html — UI
|
||||||
wails.json — config Wails
|
wails.json — config Wails
|
||||||
@ -120,9 +129,17 @@ Saída: `build/bin/kvmote.exe`
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Mini-mode / Overlay
|
||||||
|
|
||||||
|
Ao entrar em modo cliente, `engine` chama `WindowManager.SetMiniMode()` → `App` emite evento `window-mode:mini` ao frontend e exibe `OverlayWindow` nativo Win32. `OverlayWindow` tem goroutine própria com `LockOSThread` e message pump independente do Wails — não interfere com WebView2, Raw Input, nem coordenadas Wails.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## ⚠️ Cuidados
|
## ⚠️ Cuidados
|
||||||
|
|
||||||
- **Scroll touchpad:** `scrollActive` e `scrollTimer` não devem ser resetados em enter/exitClientMode
|
- **Scroll touchpad:** `scrollActive` e `scrollTimer` não devem ser resetados em enter/exitClientMode
|
||||||
- **runtime.LockOSThread:** nunca remover da goroutine de hooks
|
- **runtime.LockOSThread:** nunca remover da goroutine de hooks nem da goroutine do overlay
|
||||||
- **isWarping flag:** previne loop infinito SetCursorPos → WM_MOUSEMOVE → SetCursorPos
|
- **isWarping flag:** previne loop infinito SetCursorPos → WM_MOUSEMOVE → SetCursorPos
|
||||||
- **Mutex ordering:** engine.mu protege todo estado KVM; transport.mu protege conexão BLE
|
- **Mutex ordering:** engine.mu protege todo estado KVM; transport.mu protege conexão BLE
|
||||||
|
- **overlayOnce:** `RegisterClassExW` só pode ser chamado uma vez por processo; overlay usa `sync.Once`
|
||||||
|
- **LogDebug:** escreve em `kvmote_debug.log` na raiz do projeto (ignorado pelo git)
|
||||||
|
|||||||
38
app.go
38
app.go
@ -14,13 +14,10 @@ import (
|
|||||||
|
|
||||||
// App struct
|
// App struct
|
||||||
type App struct {
|
type App struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
engine *kvm.Engine
|
engine *kvm.Engine
|
||||||
|
overlay *input.OverlayWindow
|
||||||
// Estado da janela (pixels físicos Win32)
|
isMini bool
|
||||||
origX, origY int32
|
|
||||||
origW, origH int32
|
|
||||||
isMini bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewApp creates a new App application struct
|
// NewApp creates a new App application struct
|
||||||
@ -29,36 +26,23 @@ func NewApp() *App {
|
|||||||
h := input.NewInputHandler()
|
h := input.NewInputHandler()
|
||||||
e := kvm.NewEngine(t, h)
|
e := kvm.NewEngine(t, h)
|
||||||
app := &App{
|
app := &App{
|
||||||
engine: e,
|
engine: e,
|
||||||
|
overlay: input.NewOverlayWindow(),
|
||||||
}
|
}
|
||||||
e.SetWindowManager(app) // Vincula o app como gerenciador de janela da engine
|
e.SetWindowManager(app)
|
||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
|
|
||||||
const miniW, miniH = int32(300), int32(100)
|
|
||||||
|
|
||||||
func (a *App) SetMiniMode() {
|
func (a *App) SetMiniMode() {
|
||||||
if a.isMini {
|
if a.isMini {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Salva posição/tamanho via Win32 (pixels físicos — mesma base do MoveWindow)
|
|
||||||
wx, wy := a.engine.GetWindowPos()
|
|
||||||
ww, wh := a.engine.GetWindowSize()
|
|
||||||
if ww > 200 {
|
|
||||||
a.origX, a.origY = wx, wy
|
|
||||||
a.origW, a.origH = ww, wh
|
|
||||||
}
|
|
||||||
|
|
||||||
a.isMini = true
|
a.isMini = true
|
||||||
runtime.EventsEmit(a.ctx, "window-mode", "mini")
|
runtime.EventsEmit(a.ctx, "window-mode", "mini")
|
||||||
|
|
||||||
// Posiciona usando Win32 SetWindowPos (evita mistura de coordenadas lógicas/físicas)
|
// Overlay nativo Win32 no canto — não toca na janela principal
|
||||||
// MonitorFromWindow garante monitor correto em setups multi-monitor
|
|
||||||
mx, my, mw, mh := a.engine.GetMonitorWorkArea()
|
mx, my, mw, mh := a.engine.GetMonitorWorkArea()
|
||||||
tx := mx + mw - miniW - 20
|
a.overlay.Show(mx, my, mw, mh)
|
||||||
ty := my + mh - miniH - 50
|
|
||||||
a.engine.MoveWindow(tx, ty, miniW, miniH, true) // topmost=true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) RestoreNormalMode() {
|
func (a *App) RestoreNormalMode() {
|
||||||
@ -67,9 +51,7 @@ func (a *App) RestoreNormalMode() {
|
|||||||
}
|
}
|
||||||
a.isMini = false
|
a.isMini = false
|
||||||
runtime.EventsEmit(a.ctx, "window-mode", "normal")
|
runtime.EventsEmit(a.ctx, "window-mode", "normal")
|
||||||
|
a.overlay.Hide()
|
||||||
// Restaura via Win32 (mesma base de coordenadas do SetMiniMode)
|
|
||||||
a.engine.MoveWindow(a.origX, a.origY, a.origW, a.origH, false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// startup is called when the app starts. The context is saved
|
// startup is called when the app starts. The context is saved
|
||||||
|
|||||||
@ -31,4 +31,12 @@ type InputHandler interface {
|
|||||||
GetWindowSize() (int32, int32)
|
GetWindowSize() (int32, int32)
|
||||||
GetMonitorWorkArea() (x, y, w, h int32)
|
GetMonitorWorkArea() (x, y, w, h int32)
|
||||||
MoveWindow(x, y, w, h int32, topmost bool)
|
MoveWindow(x, y, w, h int32, topmost bool)
|
||||||
|
GetDpiScale() float64
|
||||||
|
GetForegroundWindow() uintptr
|
||||||
|
SetForegroundTo(hwnd uintptr)
|
||||||
|
GetAppHwnd() uintptr
|
||||||
|
IsMinimized(hwnd uintptr) bool
|
||||||
|
RestoreWindow(hwnd uintptr)
|
||||||
|
MinimizeWindow(hwnd uintptr)
|
||||||
|
RegisterRawScrollSink(callback func(delta int16)) error
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,9 +29,19 @@ var (
|
|||||||
procSetForegroundWindow = user32.NewProc("SetForegroundWindow")
|
procSetForegroundWindow = user32.NewProc("SetForegroundWindow")
|
||||||
procGetWindowRect = user32.NewProc("GetWindowRect")
|
procGetWindowRect = user32.NewProc("GetWindowRect")
|
||||||
procClipCursor = user32.NewProc("ClipCursor")
|
procClipCursor = user32.NewProc("ClipCursor")
|
||||||
procSetWindowPos = user32.NewProc("SetWindowPos")
|
procSetWindowPos = user32.NewProc("SetWindowPos")
|
||||||
procMonitorFromWindow = user32.NewProc("MonitorFromWindow")
|
procMonitorFromWindow = user32.NewProc("MonitorFromWindow")
|
||||||
procGetMonitorInfoW = user32.NewProc("GetMonitorInfoW")
|
procGetMonitorInfoW = user32.NewProc("GetMonitorInfoW")
|
||||||
|
procCreateWindowExW = user32.NewProc("CreateWindowExW")
|
||||||
|
procRegisterRawInputDevices = user32.NewProc("RegisterRawInputDevices")
|
||||||
|
procGetRawInputData = user32.NewProc("GetRawInputData")
|
||||||
|
procGetDpiForWindow = user32.NewProc("GetDpiForWindow")
|
||||||
|
procGetForegroundWindow = user32.NewProc("GetForegroundWindow")
|
||||||
|
procIsIconic = user32.NewProc("IsIconic")
|
||||||
|
procShowWindowAsync = user32.NewProc("ShowWindowAsync")
|
||||||
|
procGetWindowThreadProcessId = user32.NewProc("GetWindowThreadProcessId")
|
||||||
|
procAttachThreadInput = user32.NewProc("AttachThreadInput")
|
||||||
|
procBringWindowToTop = user32.NewProc("BringWindowToTop")
|
||||||
)
|
)
|
||||||
|
|
||||||
type RECT struct {
|
type RECT struct {
|
||||||
@ -45,6 +55,46 @@ type MONITORINFO struct {
|
|||||||
DwFlags uint32
|
DwFlags uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Raw Input structs (layout deve bater exatamente com winuser.h)
|
||||||
|
type RAWINPUTHEADER struct {
|
||||||
|
DwType uint32
|
||||||
|
DwSize uint32
|
||||||
|
HDevice uintptr
|
||||||
|
WParam uintptr
|
||||||
|
}
|
||||||
|
|
||||||
|
type RAWMOUSE struct {
|
||||||
|
UsFlags uint16
|
||||||
|
_ [2]byte // padding para alinhar union de 4 bytes
|
||||||
|
UsButtonFlags uint16
|
||||||
|
UsButtonData uint16
|
||||||
|
UlRawButtons uint32
|
||||||
|
LLastX int32
|
||||||
|
LLastY int32
|
||||||
|
UlExtraInfo uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
type RAWINPUT struct {
|
||||||
|
Header RAWINPUTHEADER
|
||||||
|
Mouse RAWMOUSE
|
||||||
|
}
|
||||||
|
|
||||||
|
type RAWINPUTDEVICE struct {
|
||||||
|
UsUsagePage uint16
|
||||||
|
UsUsage uint16
|
||||||
|
DwFlags uint32
|
||||||
|
HwndTarget uintptr
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
WM_INPUT = 0x00FF
|
||||||
|
RID_INPUT = 0x10000003
|
||||||
|
RIM_TYPEMOUSE = 0
|
||||||
|
RI_MOUSE_WHEEL = 0x0400
|
||||||
|
RIDEV_INPUTSINK = 0x00000100
|
||||||
|
HWND_MESSAGE = ^uintptr(2) // -3
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
WH_KEYBOARD_LL = 13
|
WH_KEYBOARD_LL = 13
|
||||||
WH_MOUSE_LL = 14
|
WH_MOUSE_LL = 14
|
||||||
@ -85,8 +135,8 @@ func NewInputHandler() InputHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *windowsInputHandler) Install(ctx context.Context, onMouse func(MouseEvent) bool, onKey func(KeyboardEvent) bool) error {
|
func (h *windowsInputHandler) Install(ctx context.Context, onMouse func(MouseEvent) bool, onKey func(KeyboardEvent) bool) error {
|
||||||
// Captura o HWND da janela ativa (que deve ser o nosso App Wails chamando Install)
|
// GetForegroundWindow é system-wide (GetActiveWindow é thread-local e pode retornar 0)
|
||||||
hwnd, _, _ := procGetActiveWindow.Call()
|
hwnd, _, _ := procGetForegroundWindow.Call()
|
||||||
h.hwnd = hwnd
|
h.hwnd = hwnd
|
||||||
|
|
||||||
ready := make(chan error, 1)
|
ready := make(chan error, 1)
|
||||||
@ -194,8 +244,21 @@ func (h *windowsInputHandler) GetScreenResolution() (int32, int32) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *windowsInputHandler) RequestFocus() {
|
func (h *windowsInputHandler) RequestFocus() {
|
||||||
if h.hwnd != 0 {
|
if h.hwnd == 0 {
|
||||||
procSetForegroundWindow.Call(h.hwnd)
|
return
|
||||||
|
}
|
||||||
|
// AttachThreadInput trick: único jeito confiável de roubar foco de outra janela no Windows.
|
||||||
|
// SetForegroundWindow sozinho falha se o processo chamante não é o foreground.
|
||||||
|
fgHwnd, _, _ := procGetForegroundWindow.Call()
|
||||||
|
fgTid, _, _ := procGetWindowThreadProcessId.Call(fgHwnd, 0)
|
||||||
|
myTid := uintptr(windows.GetCurrentThreadId())
|
||||||
|
if fgTid != 0 && fgTid != myTid {
|
||||||
|
procAttachThreadInput.Call(myTid, fgTid, 1) // attach
|
||||||
|
}
|
||||||
|
procBringWindowToTop.Call(h.hwnd)
|
||||||
|
procSetForegroundWindow.Call(h.hwnd)
|
||||||
|
if fgTid != 0 && fgTid != myTid {
|
||||||
|
procAttachThreadInput.Call(myTid, fgTid, 0) // detach
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -256,3 +319,128 @@ func (h *windowsInputHandler) MoveWindow(x, y, w, ht int32, topmost bool) {
|
|||||||
SWP_NOACTIVATE,
|
SWP_NOACTIVATE,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *windowsInputHandler) GetDpiScale() float64 {
|
||||||
|
if h.hwnd == 0 {
|
||||||
|
return 1.0
|
||||||
|
}
|
||||||
|
dpi, _, _ := procGetDpiForWindow.Call(h.hwnd)
|
||||||
|
if dpi == 0 {
|
||||||
|
return 1.0
|
||||||
|
}
|
||||||
|
return float64(dpi) / 96.0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsInputHandler) GetForegroundWindow() uintptr {
|
||||||
|
hwnd, _, _ := procGetForegroundWindow.Call()
|
||||||
|
return hwnd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsInputHandler) SetForegroundTo(hwnd uintptr) {
|
||||||
|
if hwnd == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fgHwnd, _, _ := procGetForegroundWindow.Call()
|
||||||
|
fgTid, _, _ := procGetWindowThreadProcessId.Call(fgHwnd, 0)
|
||||||
|
myTid := uintptr(windows.GetCurrentThreadId())
|
||||||
|
if fgTid != 0 && fgTid != myTid {
|
||||||
|
procAttachThreadInput.Call(myTid, fgTid, 1)
|
||||||
|
}
|
||||||
|
procBringWindowToTop.Call(hwnd)
|
||||||
|
procSetForegroundWindow.Call(hwnd)
|
||||||
|
if fgTid != 0 && fgTid != myTid {
|
||||||
|
procAttachThreadInput.Call(myTid, fgTid, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsInputHandler) GetAppHwnd() uintptr {
|
||||||
|
return h.hwnd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsInputHandler) IsMinimized(hwnd uintptr) bool {
|
||||||
|
ret, _, _ := procIsIconic.Call(hwnd)
|
||||||
|
return ret != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsInputHandler) RestoreWindow(hwnd uintptr) {
|
||||||
|
procShowWindowAsync.Call(hwnd, 9) // SW_RESTORE
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsInputHandler) MinimizeWindow(hwnd uintptr) {
|
||||||
|
procShowWindowAsync.Call(hwnd, 6) // SW_MINIMIZE
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsInputHandler) RegisterRawScrollSink(callback func(delta int16)) error {
|
||||||
|
ready := make(chan error, 1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
runtime.LockOSThread()
|
||||||
|
|
||||||
|
// Janela message-only como alvo do Raw Input (sem UI, sem z-order)
|
||||||
|
className, _ := windows.UTF16PtrFromString("STATIC")
|
||||||
|
hwndMsg, _, _ := procCreateWindowExW.Call(
|
||||||
|
0,
|
||||||
|
uintptr(unsafe.Pointer(className)),
|
||||||
|
0, 0,
|
||||||
|
0, 0, 0, 0,
|
||||||
|
HWND_MESSAGE, // janela oculta sem pai na tela
|
||||||
|
0, 0, 0,
|
||||||
|
)
|
||||||
|
if hwndMsg == 0 {
|
||||||
|
ready <- fmt.Errorf("CreateWindowExW failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registra mouse raw com RIDEV_INPUTSINK: recebe eventos mesmo sem foco/cursor
|
||||||
|
rid := RAWINPUTDEVICE{
|
||||||
|
UsUsagePage: 0x01, // HID_USAGE_PAGE_GENERIC
|
||||||
|
UsUsage: 0x02, // HID_USAGE_GENERIC_MOUSE
|
||||||
|
DwFlags: RIDEV_INPUTSINK,
|
||||||
|
HwndTarget: hwndMsg,
|
||||||
|
}
|
||||||
|
ret, _, _ := procRegisterRawInputDevices.Call(
|
||||||
|
uintptr(unsafe.Pointer(&rid)),
|
||||||
|
1,
|
||||||
|
uintptr(unsafe.Sizeof(rid)),
|
||||||
|
)
|
||||||
|
if ret == 0 {
|
||||||
|
ready <- fmt.Errorf("RegisterRawInputDevices failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ready <- nil
|
||||||
|
|
||||||
|
// Loop de mensagens dedicado para WM_INPUT
|
||||||
|
var msg struct {
|
||||||
|
Hwnd uintptr
|
||||||
|
Message uint32
|
||||||
|
WParam uintptr
|
||||||
|
LParam uintptr
|
||||||
|
Time uint32
|
||||||
|
Pt Point
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
r, _, _ := procGetMessageW.Call(uintptr(unsafe.Pointer(&msg)), hwndMsg, 0, 0)
|
||||||
|
if r == 0 || msg.Message == WM_QUIT {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if msg.Message == WM_INPUT {
|
||||||
|
var raw RAWINPUT
|
||||||
|
size := uint32(unsafe.Sizeof(raw))
|
||||||
|
procGetRawInputData.Call(
|
||||||
|
msg.LParam, // HRAWINPUT
|
||||||
|
RID_INPUT,
|
||||||
|
uintptr(unsafe.Pointer(&raw)),
|
||||||
|
uintptr(unsafe.Pointer(&size)),
|
||||||
|
uintptr(unsafe.Sizeof(RAWINPUTHEADER{})),
|
||||||
|
)
|
||||||
|
if raw.Header.DwType == RIM_TYPEMOUSE &&
|
||||||
|
raw.Mouse.UsButtonFlags&RI_MOUSE_WHEEL != 0 {
|
||||||
|
callback(int16(raw.Mouse.UsButtonData))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return <-ready
|
||||||
|
}
|
||||||
|
|||||||
203
internal/input/overlay_windows.go
Normal file
203
internal/input/overlay_windows.go
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package input
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
gdi32 = windows.NewLazySystemDLL("gdi32.dll")
|
||||||
|
|
||||||
|
procCreateSolidBrush = gdi32.NewProc("CreateSolidBrush")
|
||||||
|
procGdiDeleteObject = gdi32.NewProc("DeleteObject")
|
||||||
|
procGetStockObject = gdi32.NewProc("GetStockObject")
|
||||||
|
procGdiSelectObject = gdi32.NewProc("SelectObject")
|
||||||
|
procSetBkMode = gdi32.NewProc("SetBkMode")
|
||||||
|
procSetTextColor = gdi32.NewProc("SetTextColor")
|
||||||
|
|
||||||
|
procBeginPaint = user32.NewProc("BeginPaint")
|
||||||
|
procEndPaint = user32.NewProc("EndPaint")
|
||||||
|
procGetClientRect = user32.NewProc("GetClientRect")
|
||||||
|
procFillRect = user32.NewProc("FillRect")
|
||||||
|
procDrawTextW = user32.NewProc("DrawTextW")
|
||||||
|
procRegisterClassExW = user32.NewProc("RegisterClassExW")
|
||||||
|
procDefWindowProcW = user32.NewProc("DefWindowProcW")
|
||||||
|
procUpdateWindow = user32.NewProc("UpdateWindow")
|
||||||
|
procShowWindowFn = user32.NewProc("ShowWindow")
|
||||||
|
procTranslateMessage = user32.NewProc("TranslateMessage")
|
||||||
|
procDispatchMessageW = user32.NewProc("DispatchMessageW")
|
||||||
|
)
|
||||||
|
|
||||||
|
// WNDCLASSEXW layout: 80 bytes on 64-bit
|
||||||
|
type WNDCLASSEXW struct {
|
||||||
|
CbSize uint32
|
||||||
|
Style uint32
|
||||||
|
LpfnWndProc uintptr
|
||||||
|
CbClsExtra int32
|
||||||
|
CbWndExtra int32
|
||||||
|
HInstance uintptr
|
||||||
|
HIcon uintptr
|
||||||
|
HCursor uintptr
|
||||||
|
HbrBackground uintptr
|
||||||
|
LpszMenuName *uint16
|
||||||
|
LpszClassName *uint16
|
||||||
|
HIconSm uintptr
|
||||||
|
}
|
||||||
|
|
||||||
|
// PAINTSTRUCT layout: 72 bytes on 64-bit (includes trailing padding)
|
||||||
|
type PAINTSTRUCT struct {
|
||||||
|
Hdc uintptr
|
||||||
|
FErase int32
|
||||||
|
RcPaint RECT
|
||||||
|
FRestore int32
|
||||||
|
FIncUpdate int32
|
||||||
|
RgbReserved [32]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
overlayOnce sync.Once
|
||||||
|
overlayCallback uintptr // prevent GC of callback
|
||||||
|
)
|
||||||
|
|
||||||
|
// OverlayWindow é uma janela Win32 nativa independente do Wails.
|
||||||
|
// Exibe indicador neon "KVMote" no canto da tela durante modo cliente.
|
||||||
|
// Não interfere com WebView2, Raw Input, nem coordenadas do Wails.
|
||||||
|
type OverlayWindow struct {
|
||||||
|
hwnd uintptr
|
||||||
|
ready chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOverlayWindow() *OverlayWindow {
|
||||||
|
ov := &OverlayWindow{ready: make(chan struct{})}
|
||||||
|
go ov.run()
|
||||||
|
<-ov.ready
|
||||||
|
return ov
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ov *OverlayWindow) run() {
|
||||||
|
runtime.LockOSThread()
|
||||||
|
|
||||||
|
className, _ := windows.UTF16PtrFromString("KVMoteOverlay")
|
||||||
|
|
||||||
|
overlayOnce.Do(func() {
|
||||||
|
cb := windows.NewCallback(func(hwnd, msg, wParam, lParam uintptr) uintptr {
|
||||||
|
const (
|
||||||
|
wmPaint = 0x000F
|
||||||
|
wmEraseBkgnd = 0x0014
|
||||||
|
dtCenter = 0x00000001
|
||||||
|
dtVCenter = 0x00000004
|
||||||
|
dtSingleLine = 0x00000020
|
||||||
|
transparent = 1
|
||||||
|
neonGreen = 0x0014FF39 // #39FF14 em COLORREF (0x00BBGGRR)
|
||||||
|
defaultFont = 17 // DEFAULT_GUI_FONT
|
||||||
|
)
|
||||||
|
switch msg {
|
||||||
|
case wmEraseBkgnd:
|
||||||
|
return 1
|
||||||
|
case wmPaint:
|
||||||
|
var ps PAINTSTRUCT
|
||||||
|
hdc, _, _ := procBeginPaint.Call(hwnd, uintptr(unsafe.Pointer(&ps)))
|
||||||
|
var rc RECT
|
||||||
|
procGetClientRect.Call(hwnd, uintptr(unsafe.Pointer(&rc)))
|
||||||
|
|
||||||
|
brush, _, _ := procCreateSolidBrush.Call(0) // preto
|
||||||
|
procFillRect.Call(hdc, uintptr(unsafe.Pointer(&rc)), brush)
|
||||||
|
procGdiDeleteObject.Call(brush)
|
||||||
|
|
||||||
|
font, _, _ := procGetStockObject.Call(defaultFont)
|
||||||
|
old, _, _ := procGdiSelectObject.Call(hdc, font)
|
||||||
|
procSetBkMode.Call(hdc, transparent)
|
||||||
|
procSetTextColor.Call(hdc, neonGreen)
|
||||||
|
text, _ := windows.UTF16PtrFromString("KVMote")
|
||||||
|
procDrawTextW.Call(hdc, uintptr(unsafe.Pointer(text)), ^uintptr(0),
|
||||||
|
uintptr(unsafe.Pointer(&rc)), dtCenter|dtVCenter|dtSingleLine)
|
||||||
|
procGdiSelectObject.Call(hdc, old)
|
||||||
|
|
||||||
|
procEndPaint.Call(hwnd, uintptr(unsafe.Pointer(&ps)))
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
r, _, _ := procDefWindowProcW.Call(hwnd, msg, wParam, lParam)
|
||||||
|
return r
|
||||||
|
})
|
||||||
|
overlayCallback = cb
|
||||||
|
|
||||||
|
hMod, _, _ := procGetModuleHandleW.Call(0)
|
||||||
|
wc := WNDCLASSEXW{
|
||||||
|
LpfnWndProc: cb,
|
||||||
|
HInstance: hMod,
|
||||||
|
LpszClassName: className,
|
||||||
|
}
|
||||||
|
wc.CbSize = uint32(unsafe.Sizeof(wc))
|
||||||
|
procRegisterClassExW.Call(uintptr(unsafe.Pointer(&wc)))
|
||||||
|
})
|
||||||
|
|
||||||
|
const (
|
||||||
|
wsPopup = 0x80000000
|
||||||
|
wsExTopmost = 0x00000008
|
||||||
|
wsExNoActivate = 0x08000000
|
||||||
|
wsExToolWindow = 0x00000080
|
||||||
|
)
|
||||||
|
hMod, _, _ := procGetModuleHandleW.Call(0)
|
||||||
|
hwnd, _, _ := procCreateWindowExW.Call(
|
||||||
|
wsExTopmost|wsExNoActivate|wsExToolWindow,
|
||||||
|
uintptr(unsafe.Pointer(className)),
|
||||||
|
0, wsPopup,
|
||||||
|
0, 0, 200, 60,
|
||||||
|
0, 0, hMod, 0,
|
||||||
|
)
|
||||||
|
ov.hwnd = hwnd
|
||||||
|
close(ov.ready)
|
||||||
|
|
||||||
|
if hwnd == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var msg struct {
|
||||||
|
Hwnd uintptr
|
||||||
|
Message uint32
|
||||||
|
WParam uintptr
|
||||||
|
LParam uintptr
|
||||||
|
Time uint32
|
||||||
|
Pt Point
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
ret, _, _ := procGetMessageW.Call(uintptr(unsafe.Pointer(&msg)), hwnd, 0, 0)
|
||||||
|
if ret == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
procTranslateMessage.Call(uintptr(unsafe.Pointer(&msg)))
|
||||||
|
procDispatchMessageW.Call(uintptr(unsafe.Pointer(&msg)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show exibe o overlay no canto superior esquerdo do monitor especificado.
|
||||||
|
// mx, my, mw, mh: work area do monitor em pixels físicos.
|
||||||
|
func (ov *OverlayWindow) Show(mx, my, mw, mh int32) {
|
||||||
|
if ov.hwnd == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = mw
|
||||||
|
_ = mh
|
||||||
|
const overlayW, overlayH = int32(150), int32(40)
|
||||||
|
const swpNoActivate = 0x0010
|
||||||
|
// Canto superior esquerdo do monitor — sem depender de taskbar
|
||||||
|
x := mx + 10
|
||||||
|
y := my + 10
|
||||||
|
procSetWindowPos.Call(ov.hwnd, ^uintptr(0), // HWND_TOPMOST
|
||||||
|
uintptr(x), uintptr(y), uintptr(overlayW), uintptr(overlayH), swpNoActivate)
|
||||||
|
procShowWindowFn.Call(ov.hwnd, 5) // SW_SHOW
|
||||||
|
procUpdateWindow.Call(ov.hwnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide esconde o overlay.
|
||||||
|
func (ov *OverlayWindow) Hide() {
|
||||||
|
if ov.hwnd == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
procShowWindowFn.Call(ov.hwnd, 0) // SW_HIDE
|
||||||
|
}
|
||||||
@ -46,11 +46,12 @@ type WindowManager interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Engine struct {
|
type Engine struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
transport transport.Transport
|
rawScrollOnce sync.Once
|
||||||
inputHandler input.InputHandler
|
transport transport.Transport
|
||||||
winManager WindowManager
|
inputHandler input.InputHandler
|
||||||
|
winManager WindowManager
|
||||||
|
|
||||||
clientMode bool
|
clientMode bool
|
||||||
clientPos ClientPos
|
clientPos ClientPos
|
||||||
clientLayout ClientLayout
|
clientLayout ClientLayout
|
||||||
@ -72,6 +73,9 @@ type Engine struct {
|
|||||||
wheelAccum int32
|
wheelAccum int32
|
||||||
|
|
||||||
mouseThrottle time.Time
|
mouseThrottle time.Time
|
||||||
|
|
||||||
|
prevForegroundHwnd uintptr // janela que estava em foco antes de entrar em client mode
|
||||||
|
prevWasActive bool // true se salvamos uma janela diferente do KVMote
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewEngine(t transport.Transport, h input.InputHandler) *Engine {
|
func NewEngine(t transport.Transport, h input.InputHandler) *Engine {
|
||||||
@ -110,12 +114,52 @@ func (e *Engine) MoveWindow(x, y, w, h int32, topmost bool) {
|
|||||||
e.inputHandler.MoveWindow(x, y, w, h, topmost)
|
e.inputHandler.MoveWindow(x, y, w, h, topmost)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *Engine) GetDpiScale() float64 {
|
||||||
|
return e.inputHandler.GetDpiScale()
|
||||||
|
}
|
||||||
|
|
||||||
func (e *Engine) Start(ctx context.Context) error {
|
func (e *Engine) Start(ctx context.Context) error {
|
||||||
w, h := e.inputHandler.GetScreenResolution()
|
w, h := e.inputHandler.GetScreenResolution()
|
||||||
LogDebug(fmt.Sprintf("Engine Iniciada. Tela: %dx%d. Pos: %v", w, h, e.clientPos))
|
LogDebug(fmt.Sprintf("Engine Iniciada. Tela: %dx%d. Pos: %v", w, h, e.clientPos))
|
||||||
|
|
||||||
|
// Registra apenas uma vez — Connect() pode chamar Start() múltiplas vezes
|
||||||
|
e.rawScrollOnce.Do(func() {
|
||||||
|
if err := e.inputHandler.RegisterRawScrollSink(e.onRawScroll); err != nil {
|
||||||
|
LogDebug(fmt.Sprintf("Raw scroll sink falhou: %v", err))
|
||||||
|
} else {
|
||||||
|
LogDebug("Raw scroll sink registrado OK")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return e.inputHandler.Install(ctx, e.onMouse, e.onKey)
|
return e.inputHandler.Install(ctx, e.onMouse, e.onKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *Engine) onRawScroll(delta int16) {
|
||||||
|
e.mu.Lock()
|
||||||
|
defer e.mu.Unlock()
|
||||||
|
|
||||||
|
if !e.clientMode || !e.transport.IsConnected() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.scrollActive = true
|
||||||
|
e.scrollTimer = time.Now()
|
||||||
|
|
||||||
|
// Mesmo acumulador e divisor do HandleManualScroll
|
||||||
|
// delta vem em unidades WHEEL_DELTA (±120 por notch)
|
||||||
|
e.wheelAccum += int32(delta)
|
||||||
|
const Divisor = 30
|
||||||
|
toSend := e.wheelAccum / Divisor
|
||||||
|
if toSend != 0 {
|
||||||
|
e.wheelAccum -= toSend * Divisor
|
||||||
|
val := int8(clamp(int(toSend), -127, 127))
|
||||||
|
go func(v int8) {
|
||||||
|
e.transport.Send([]byte{'W', byte(v)})
|
||||||
|
LogDebug(fmt.Sprintf("RAW SCROLL: delta=%d → W=%d", delta, v))
|
||||||
|
}(val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
func (e *Engine) onMouse(ev input.MouseEvent) bool {
|
func (e *Engine) onMouse(ev input.MouseEvent) bool {
|
||||||
e.mu.Lock()
|
e.mu.Lock()
|
||||||
@ -151,10 +195,7 @@ func (e *Engine) onMouse(ev input.MouseEvent) bool {
|
|||||||
case 0x0200: // Move
|
case 0x0200: // Move
|
||||||
if e.isWarping { e.isWarping = false; return true }
|
if e.isWarping { e.isWarping = false; return true }
|
||||||
|
|
||||||
wx, wy := e.inputHandler.GetWindowPos()
|
|
||||||
|
|
||||||
if e.scrollActive {
|
if e.scrollActive {
|
||||||
// Durante scroll: decai após 250ms, mas sempre parka cursor na janela
|
|
||||||
if time.Since(e.scrollTimer) > 250*time.Millisecond {
|
if time.Since(e.scrollTimer) > 250*time.Millisecond {
|
||||||
e.scrollActive = false
|
e.scrollActive = false
|
||||||
e.virtualX, e.virtualY = 0, 0
|
e.virtualX, e.virtualY = 0, 0
|
||||||
@ -180,10 +221,12 @@ func (e *Engine) onMouse(ev input.MouseEvent) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sempre parka cursor no centro da mini janela (garante alvo para scroll)
|
// Park no centro da tela (cursor oculto, só para manter delta consistente)
|
||||||
|
w, h := e.inputHandler.GetScreenResolution()
|
||||||
|
cx, cy := w/2, h/2
|
||||||
e.isWarping = true
|
e.isWarping = true
|
||||||
e.inputHandler.SetCursorPos(wx+150, wy+50)
|
e.inputHandler.SetCursorPos(cx, cy)
|
||||||
e.lastRawPos = input.Point{X: wx + 150, Y: wy + 50}
|
e.lastRawPos = input.Point{X: cx, Y: cy}
|
||||||
return true
|
return true
|
||||||
|
|
||||||
case 0x0201: e.transport.Send([]byte{'D', 'L'}); return true
|
case 0x0201: e.transport.Send([]byte{'D', 'L'}); return true
|
||||||
@ -226,34 +269,35 @@ func (e *Engine) enterClientMode(p input.Point) {
|
|||||||
e.pendingDX, e.pendingDY = 0, 0
|
e.pendingDX, e.pendingDY = 0, 0
|
||||||
e.wheelAccum = 0
|
e.wheelAccum = 0
|
||||||
e.mouseThrottle = time.Now()
|
e.mouseThrottle = time.Now()
|
||||||
|
|
||||||
e.inputHandler.RequestFocus()
|
|
||||||
|
|
||||||
|
// Salva janela em foco e traz KVMote pra frente, se necessário
|
||||||
|
kvHwnd := e.inputHandler.GetAppHwnd()
|
||||||
|
fgHwnd := e.inputHandler.GetForegroundWindow()
|
||||||
|
if fgHwnd != kvHwnd {
|
||||||
|
e.prevForegroundHwnd = fgHwnd
|
||||||
|
e.prevWasActive = true
|
||||||
|
if e.inputHandler.IsMinimized(kvHwnd) {
|
||||||
|
e.inputHandler.RestoreWindow(kvHwnd)
|
||||||
|
}
|
||||||
|
e.inputHandler.RequestFocus()
|
||||||
|
LogDebug(fmt.Sprintf("Janela anterior salva: 0x%X, KVMote trazido ao foco", fgHwnd))
|
||||||
|
} else {
|
||||||
|
e.prevWasActive = false
|
||||||
|
e.prevForegroundHwnd = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mantém processo ativo + mostra overlay
|
||||||
if e.winManager != nil {
|
if e.winManager != nil {
|
||||||
e.winManager.SetMiniMode()
|
e.winManager.SetMiniMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wails move janela de forma assíncrona; aguardar antes de parkear
|
// Parka cursor no centro — scroll via Raw Input, cursor pode ficar visível
|
||||||
// ShowCursor(true) DEVE ser após resize/move: Wails/webview pode chamar ShowCursor(false) internamente
|
w, h := e.inputHandler.GetScreenResolution()
|
||||||
go func() {
|
cx, cy := w/2, h/2
|
||||||
time.Sleep(250 * time.Millisecond)
|
e.isWarping = true
|
||||||
e.mu.Lock()
|
e.inputHandler.SetCursorPos(cx, cy)
|
||||||
defer e.mu.Unlock()
|
e.lastRawPos = input.Point{X: cx, Y: cy}
|
||||||
if !e.clientMode {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Força cursor visível (combate ShowCursor(false) interno do Wails/webview)
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
e.inputHandler.ShowCursor(true)
|
|
||||||
}
|
|
||||||
wx, wy := e.inputHandler.GetWindowPos()
|
|
||||||
e.isWarping = true
|
|
||||||
e.inputHandler.SetCursorPos(wx+150, wy+50)
|
|
||||||
e.lastRawPos = input.Point{X: wx + 150, Y: wy + 50}
|
|
||||||
LogDebug(fmt.Sprintf("Cursor parkado em mini: (%d,%d)", wx+150, wy+50))
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 'A' (ReleaseAll) limpa estados presos no firmware, 'O' sinaliza LED Magenta
|
|
||||||
e.transport.Send([]byte{'A'})
|
e.transport.Send([]byte{'A'})
|
||||||
e.transport.Send([]byte{'O'})
|
e.transport.Send([]byte{'O'})
|
||||||
}
|
}
|
||||||
@ -263,12 +307,19 @@ func (e *Engine) exitClientMode() {
|
|||||||
e.clientMode = false
|
e.clientMode = false
|
||||||
e.lastModeChange = time.Now()
|
e.lastModeChange = time.Now()
|
||||||
|
|
||||||
// Restaura a janela (Normal Mode)
|
|
||||||
if e.winManager != nil {
|
if e.winManager != nil {
|
||||||
e.winManager.RestoreNormalMode()
|
e.winManager.RestoreNormalMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
e.inputHandler.ShowCursor(true)
|
e.inputHandler.ShowCursor(true)
|
||||||
|
|
||||||
|
// Restaura janela anterior ao foco
|
||||||
|
if e.prevWasActive && e.prevForegroundHwnd != 0 {
|
||||||
|
e.inputHandler.SetForegroundTo(e.prevForegroundHwnd)
|
||||||
|
LogDebug(fmt.Sprintf("Janela anterior restaurada: 0x%X", e.prevForegroundHwnd))
|
||||||
|
e.prevForegroundHwnd = 0
|
||||||
|
e.prevWasActive = false
|
||||||
|
}
|
||||||
w, h := e.inputHandler.GetScreenResolution()
|
w, h := e.inputHandler.GetScreenResolution()
|
||||||
var ret input.Point
|
var ret input.Point
|
||||||
const Offset = 120
|
const Offset = 120
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user