feat: mudar o foco da janela quando for pro client
This commit is contained in:
parent
5d016d7087
commit
1de5a60843
@ -1,7 +1,8 @@
|
||||
{
|
||||
"permissions": {
|
||||
"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`).
|
||||
|
||||
@ -30,16 +34,21 @@ Hardware suportado: ESP32-S3 (BLE NUS) e Arduino Leonardo + HC-06 (Serial SPP).
|
||||
|
||||
```
|
||||
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/
|
||||
transport/
|
||||
transport.go — interface Transport (Detect/Connect/Send/SendLossy...)
|
||||
ble_windows.go — BLE NUS via tinygo/bluetooth
|
||||
kvm/
|
||||
engine.go — lógica KVM: mouse, teclado, clipboard, modo cliente
|
||||
WindowManager interface (SetMiniMode/RestoreNormalMode)
|
||||
LogDebug → kvmote_debug.log (arquivo na raiz)
|
||||
input/
|
||||
input.go — interface InputHandler + tipos (Point, MouseEvent, KeyboardEvent)
|
||||
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/
|
||||
dist/index.html — UI
|
||||
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
|
||||
|
||||
- **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
|
||||
- **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
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
engine *kvm.Engine
|
||||
|
||||
// Estado da janela (pixels físicos Win32)
|
||||
origX, origY int32
|
||||
origW, origH int32
|
||||
isMini bool
|
||||
ctx context.Context
|
||||
engine *kvm.Engine
|
||||
overlay *input.OverlayWindow
|
||||
isMini bool
|
||||
}
|
||||
|
||||
// NewApp creates a new App application struct
|
||||
@ -29,36 +26,23 @@ func NewApp() *App {
|
||||
h := input.NewInputHandler()
|
||||
e := kvm.NewEngine(t, h)
|
||||
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
|
||||
}
|
||||
|
||||
const miniW, miniH = int32(300), int32(100)
|
||||
|
||||
func (a *App) SetMiniMode() {
|
||||
if a.isMini {
|
||||
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
|
||||
runtime.EventsEmit(a.ctx, "window-mode", "mini")
|
||||
|
||||
// Posiciona usando Win32 SetWindowPos (evita mistura de coordenadas lógicas/físicas)
|
||||
// MonitorFromWindow garante monitor correto em setups multi-monitor
|
||||
// Overlay nativo Win32 no canto — não toca na janela principal
|
||||
mx, my, mw, mh := a.engine.GetMonitorWorkArea()
|
||||
tx := mx + mw - miniW - 20
|
||||
ty := my + mh - miniH - 50
|
||||
a.engine.MoveWindow(tx, ty, miniW, miniH, true) // topmost=true
|
||||
a.overlay.Show(mx, my, mw, mh)
|
||||
}
|
||||
|
||||
func (a *App) RestoreNormalMode() {
|
||||
@ -67,9 +51,7 @@ func (a *App) RestoreNormalMode() {
|
||||
}
|
||||
a.isMini = false
|
||||
runtime.EventsEmit(a.ctx, "window-mode", "normal")
|
||||
|
||||
// Restaura via Win32 (mesma base de coordenadas do SetMiniMode)
|
||||
a.engine.MoveWindow(a.origX, a.origY, a.origW, a.origH, false)
|
||||
a.overlay.Hide()
|
||||
}
|
||||
|
||||
// startup is called when the app starts. The context is saved
|
||||
|
||||
@ -31,4 +31,12 @@ type InputHandler interface {
|
||||
GetWindowSize() (int32, int32)
|
||||
GetMonitorWorkArea() (x, y, w, h int32)
|
||||
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")
|
||||
procGetWindowRect = user32.NewProc("GetWindowRect")
|
||||
procClipCursor = user32.NewProc("ClipCursor")
|
||||
procSetWindowPos = user32.NewProc("SetWindowPos")
|
||||
procMonitorFromWindow = user32.NewProc("MonitorFromWindow")
|
||||
procGetMonitorInfoW = user32.NewProc("GetMonitorInfoW")
|
||||
procSetWindowPos = user32.NewProc("SetWindowPos")
|
||||
procMonitorFromWindow = user32.NewProc("MonitorFromWindow")
|
||||
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 {
|
||||
@ -45,6 +55,46 @@ type MONITORINFO struct {
|
||||
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 (
|
||||
WH_KEYBOARD_LL = 13
|
||||
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 {
|
||||
// Captura o HWND da janela ativa (que deve ser o nosso App Wails chamando Install)
|
||||
hwnd, _, _ := procGetActiveWindow.Call()
|
||||
// GetForegroundWindow é system-wide (GetActiveWindow é thread-local e pode retornar 0)
|
||||
hwnd, _, _ := procGetForegroundWindow.Call()
|
||||
h.hwnd = hwnd
|
||||
|
||||
ready := make(chan error, 1)
|
||||
@ -194,8 +244,21 @@ func (h *windowsInputHandler) GetScreenResolution() (int32, int32) {
|
||||
}
|
||||
|
||||
func (h *windowsInputHandler) RequestFocus() {
|
||||
if h.hwnd != 0 {
|
||||
procSetForegroundWindow.Call(h.hwnd)
|
||||
if h.hwnd == 0 {
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
||||
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,10 +46,11 @@ type WindowManager interface {
|
||||
}
|
||||
|
||||
type Engine struct {
|
||||
mu sync.Mutex
|
||||
transport transport.Transport
|
||||
inputHandler input.InputHandler
|
||||
winManager WindowManager
|
||||
mu sync.Mutex
|
||||
rawScrollOnce sync.Once
|
||||
transport transport.Transport
|
||||
inputHandler input.InputHandler
|
||||
winManager WindowManager
|
||||
|
||||
clientMode bool
|
||||
clientPos ClientPos
|
||||
@ -72,6 +73,9 @@ type Engine struct {
|
||||
wheelAccum int32
|
||||
|
||||
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 {
|
||||
@ -110,12 +114,52 @@ func (e *Engine) MoveWindow(x, y, w, h int32, topmost bool) {
|
||||
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 {
|
||||
w, h := e.inputHandler.GetScreenResolution()
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
e.mu.Lock()
|
||||
@ -151,10 +195,7 @@ func (e *Engine) onMouse(ev input.MouseEvent) bool {
|
||||
case 0x0200: // Move
|
||||
if e.isWarping { e.isWarping = false; return true }
|
||||
|
||||
wx, wy := e.inputHandler.GetWindowPos()
|
||||
|
||||
if e.scrollActive {
|
||||
// Durante scroll: decai após 250ms, mas sempre parka cursor na janela
|
||||
if time.Since(e.scrollTimer) > 250*time.Millisecond {
|
||||
e.scrollActive = false
|
||||
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.inputHandler.SetCursorPos(wx+150, wy+50)
|
||||
e.lastRawPos = input.Point{X: wx + 150, Y: wy + 50}
|
||||
e.inputHandler.SetCursorPos(cx, cy)
|
||||
e.lastRawPos = input.Point{X: cx, Y: cy}
|
||||
return true
|
||||
|
||||
case 0x0201: e.transport.Send([]byte{'D', 'L'}); return true
|
||||
@ -227,33 +270,34 @@ func (e *Engine) enterClientMode(p input.Point) {
|
||||
e.wheelAccum = 0
|
||||
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 {
|
||||
e.winManager.SetMiniMode()
|
||||
}
|
||||
|
||||
// Wails move janela de forma assíncrona; aguardar antes de parkear
|
||||
// ShowCursor(true) DEVE ser após resize/move: Wails/webview pode chamar ShowCursor(false) internamente
|
||||
go func() {
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
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))
|
||||
}()
|
||||
// Parka cursor no centro — scroll via Raw Input, cursor pode ficar visível
|
||||
w, h := e.inputHandler.GetScreenResolution()
|
||||
cx, cy := w/2, h/2
|
||||
e.isWarping = true
|
||||
e.inputHandler.SetCursorPos(cx, cy)
|
||||
e.lastRawPos = input.Point{X: cx, Y: cy}
|
||||
|
||||
// 'A' (ReleaseAll) limpa estados presos no firmware, 'O' sinaliza LED Magenta
|
||||
e.transport.Send([]byte{'A'})
|
||||
e.transport.Send([]byte{'O'})
|
||||
}
|
||||
@ -263,12 +307,19 @@ func (e *Engine) exitClientMode() {
|
||||
e.clientMode = false
|
||||
e.lastModeChange = time.Now()
|
||||
|
||||
// Restaura a janela (Normal Mode)
|
||||
if e.winManager != nil {
|
||||
e.winManager.RestoreNormalMode()
|
||||
}
|
||||
|
||||
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()
|
||||
var ret input.Point
|
||||
const Offset = 120
|
||||
|
||||
Loading…
Reference in New Issue
Block a user