Compare commits
4 Commits
552b2ca62c
...
879ae24ca6
| Author | SHA1 | Date | |
|---|---|---|---|
| 879ae24ca6 | |||
| 1de5a60843 | |||
| 5d016d7087 | |||
| 6bdf5fcdbd |
8
.claude/settings.local.json
Normal file
8
.claude/settings.local.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"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)
|
||||||
|
|||||||
307
S3/KVMote_ESP32S3.ino
Normal file
307
S3/KVMote_ESP32S3.ino
Normal file
@ -0,0 +1,307 @@
|
|||||||
|
/*
|
||||||
|
KVMote — ESP32-S3 N16R8
|
||||||
|
BLE NUS (Nordic UART Service) → USB HID nativo
|
||||||
|
|
||||||
|
Substitui: Arduino Leonardo + HC-06 + LED RGB externo
|
||||||
|
LED: WS2812B embutido na placa (GPIO 48 na maioria das DevKit-C1).
|
||||||
|
Se a sua placa usar outro pino, altere LED_PIN abaixo.
|
||||||
|
|
||||||
|
Protocolo binário: idêntico ao KVMote.ino (Leonardo).
|
||||||
|
Mouse move → 'M' dx(int8) dy(int8) 3 bytes
|
||||||
|
Mouse wheel → 'W' delta(int8) 2 bytes
|
||||||
|
Tecla write → 'K' char 2 bytes
|
||||||
|
Clique → 'C' 'L'|'R' 2 bytes
|
||||||
|
Tecla press → 'P' keycode 2 bytes
|
||||||
|
Tecla release → 'U' keycode 2 bytes
|
||||||
|
ReleaseAll → 'A' 1 byte
|
||||||
|
Mouse press → 'D' 'L'|'R' 2 bytes
|
||||||
|
Mouse release → 'E' 'L'|'R' 2 bytes
|
||||||
|
LED cliente → 'O' (magenta)
|
||||||
|
LED host ok → 'H' (azul)
|
||||||
|
LED sem host → 'G' (verde)
|
||||||
|
Ping/Pong → '~' → responde [PONG] 1 byte
|
||||||
|
|
||||||
|
Dependências (instale pela Library Manager do Arduino IDE):
|
||||||
|
- Adafruit NeoPixel (by Adafruit)
|
||||||
|
Já incluídas no core ESP32:
|
||||||
|
- USB / USBHIDKeyboard / USBHIDMouse
|
||||||
|
- BLEDevice / BLEServer / BLE2902
|
||||||
|
|
||||||
|
Board: "ESP32S3 Dev Module"
|
||||||
|
USB Mode → Hardware CDC and JTAG ← mantém JTAG para upload via porta COM
|
||||||
|
USB CDC On Boot → Disabled ← CRÍTICO: libera USB nativo para HID
|
||||||
|
Upload Mode → Internal USB (ou USB-OTG CDC)
|
||||||
|
PSRAM → OPI PSRAM (para N16R8)
|
||||||
|
Flash Size → 16MB
|
||||||
|
|
||||||
|
Conexões:
|
||||||
|
Porta USB (nativa OTG) → PC cliente (aparece como teclado+mouse HID)
|
||||||
|
Porta COM (CH343) → PC de desenvolvimento (upload de firmware)
|
||||||
|
BLE → Host PC (sem fio, KVMote.exe)
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "USB.h"
|
||||||
|
#include "USBHIDKeyboard.h"
|
||||||
|
#include "USBHIDMouse.h"
|
||||||
|
#include <BLEDevice.h>
|
||||||
|
#include <BLEServer.h>
|
||||||
|
#include <BLEUtils.h>
|
||||||
|
#include <BLE2902.h>
|
||||||
|
#include <Adafruit_NeoPixel.h>
|
||||||
|
|
||||||
|
// ── NUS UUIDs ─────────────────────────────────────────────────────────────────
|
||||||
|
#define NUS_SERVICE_UUID "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
|
||||||
|
#define NUS_RX_UUID "6E400002-B5A3-F393-E0A9-E50E24DCCA9E" // PC escreve aqui
|
||||||
|
#define NUS_TX_UUID "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" // ESP32 notifica (PONG)
|
||||||
|
|
||||||
|
// ── LED WS2812B embutido ──────────────────────────────────────────────────────
|
||||||
|
#define LED_PIN 48 // GPIO 48 — ESP32-S3-DevKitC-1; altere se necessário
|
||||||
|
#define LED_COUNT 1
|
||||||
|
#define LED_BRIGHTNESS 80 // 0–255 (80 ≈ 30%, evita ofuscar)
|
||||||
|
|
||||||
|
// ── Objetos USB HID ───────────────────────────────────────────────────────────
|
||||||
|
USBHIDKeyboard Keyboard;
|
||||||
|
USBHIDMouse Mouse;
|
||||||
|
|
||||||
|
// ── NeoPixel ──────────────────────────────────────────────────────────────────
|
||||||
|
Adafruit_NeoPixel pixel(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800);
|
||||||
|
|
||||||
|
void ledCor(uint8_t r, uint8_t g, uint8_t b) {
|
||||||
|
pixel.setPixelColor(0, pixel.Color(r, g, b));
|
||||||
|
pixel.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cor base atual — restaurada após paste batch
|
||||||
|
uint8_t basR = 0, basG = 255, basB = 0; // verde = aguardando conexão
|
||||||
|
|
||||||
|
// ── BLE ───────────────────────────────────────────────────────────────────────
|
||||||
|
BLEServer* pServer = nullptr;
|
||||||
|
BLECharacteristic* pTxChar = nullptr;
|
||||||
|
bool bleConn = false;
|
||||||
|
|
||||||
|
// ── Fila BLE → HID (desacopla callback BLE do USB TinyUSB) ───────────────────
|
||||||
|
// O callback BLE roda numa task FreeRTOS separada. Chamar Mouse.move() /
|
||||||
|
// Keyboard.press() de lá bloqueia a task BLE esperando o USB. A fila resolve:
|
||||||
|
// callback só enfileira bytes, loop() drena e chama o HID.
|
||||||
|
static QueueHandle_t rxQueue;
|
||||||
|
|
||||||
|
// ── Máquina de estados (idêntica ao Leonardo) ─────────────────────────────────
|
||||||
|
enum Estado : uint8_t {
|
||||||
|
AGUARDA_CMD,
|
||||||
|
AGUARDA_MOUSE_DX,
|
||||||
|
AGUARDA_MOUSE_DY,
|
||||||
|
AGUARDA_MOUSE_WHEEL,
|
||||||
|
AGUARDA_TECLA,
|
||||||
|
AGUARDA_CLIQUE,
|
||||||
|
AGUARDA_PRESS_KEY,
|
||||||
|
AGUARDA_RELEASE_KEY,
|
||||||
|
AGUARDA_MOUSE_PRESS,
|
||||||
|
AGUARDA_MOUSE_RELEASE,
|
||||||
|
AGUARDA_BATCH_LEN1, // recebendo byte alto do tamanho do chunk
|
||||||
|
AGUARDA_BATCH_LEN2, // recebendo byte baixo do tamanho do chunk
|
||||||
|
AGUARDA_BATCH_DATA // recebendo bytes de texto → Keyboard.write()
|
||||||
|
};
|
||||||
|
|
||||||
|
Estado estado = AGUARDA_CMD;
|
||||||
|
int8_t pendingDX = 0;
|
||||||
|
uint16_t batchRemaining = 0;
|
||||||
|
uint32_t batchLedMs = 0;
|
||||||
|
bool batchLedOn = false;
|
||||||
|
|
||||||
|
// ── Processa um byte do protocolo ─────────────────────────────────────────────
|
||||||
|
// Chamada diretamente do callback BLE (task separada do FreeRTOS).
|
||||||
|
// As funções HID do ESP32 são thread-safe.
|
||||||
|
void processaByte(uint8_t b) {
|
||||||
|
switch (estado) {
|
||||||
|
|
||||||
|
case AGUARDA_CMD:
|
||||||
|
if (b == 'M') estado = AGUARDA_MOUSE_DX;
|
||||||
|
else if (b == 'W') estado = AGUARDA_MOUSE_WHEEL;
|
||||||
|
else if (b == 'K') estado = AGUARDA_TECLA;
|
||||||
|
else if (b == 'C') estado = AGUARDA_CLIQUE;
|
||||||
|
else if (b == 'P') estado = AGUARDA_PRESS_KEY;
|
||||||
|
else if (b == 'U') estado = AGUARDA_RELEASE_KEY;
|
||||||
|
else if (b == 'D') estado = AGUARDA_MOUSE_PRESS;
|
||||||
|
else if (b == 'E') estado = AGUARDA_MOUSE_RELEASE;
|
||||||
|
else if (b == 'A') { Keyboard.releaseAll(); }
|
||||||
|
else if (b == 'O') { basR = 255; basG = 0; basB = 255; ledCor(basR, basG, basB); }
|
||||||
|
else if (b == 'H') { basR = 0; basG = 0; basB = 255; ledCor(basR, basG, basB); }
|
||||||
|
else if (b == 'G') { basR = 0; basG = 255; basB = 0; ledCor(basR, basG, basB); }
|
||||||
|
else if (b == 'T') { estado = AGUARDA_BATCH_LEN1; }
|
||||||
|
else if (b == '~') {
|
||||||
|
if (pTxChar && bleConn) {
|
||||||
|
pTxChar->setValue((uint8_t*)"[PONG]", 6);
|
||||||
|
pTxChar->notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case AGUARDA_MOUSE_DX:
|
||||||
|
pendingDX = (int8_t)b;
|
||||||
|
estado = AGUARDA_MOUSE_DY;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case AGUARDA_MOUSE_DY:
|
||||||
|
Mouse.move(pendingDX, (int8_t)b, 0);
|
||||||
|
estado = AGUARDA_CMD;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case AGUARDA_MOUSE_WHEEL:
|
||||||
|
Mouse.move(0, 0, (int8_t)b);
|
||||||
|
estado = AGUARDA_CMD;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case AGUARDA_TECLA:
|
||||||
|
Keyboard.write(b); // keycodes >= 0x80 seguem a mesma convenção do Arduino HID
|
||||||
|
estado = AGUARDA_CMD;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case AGUARDA_CLIQUE:
|
||||||
|
if (b == 'L') Mouse.click(MOUSE_LEFT);
|
||||||
|
if (b == 'R') Mouse.click(MOUSE_RIGHT);
|
||||||
|
estado = AGUARDA_CMD;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case AGUARDA_PRESS_KEY:
|
||||||
|
Keyboard.press(b);
|
||||||
|
estado = AGUARDA_CMD;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case AGUARDA_RELEASE_KEY:
|
||||||
|
Keyboard.release(b);
|
||||||
|
estado = AGUARDA_CMD;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case AGUARDA_MOUSE_PRESS:
|
||||||
|
if (b == 'L') Mouse.press(MOUSE_LEFT);
|
||||||
|
if (b == 'R') Mouse.press(MOUSE_RIGHT);
|
||||||
|
estado = AGUARDA_CMD;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case AGUARDA_MOUSE_RELEASE:
|
||||||
|
if (b == 'L') Mouse.release(MOUSE_LEFT);
|
||||||
|
if (b == 'R') Mouse.release(MOUSE_RIGHT);
|
||||||
|
estado = AGUARDA_CMD;
|
||||||
|
break;
|
||||||
|
|
||||||
|
// ── Paste batch: T + uint16_be(N) + N bytes → Keyboard.write por byte ────
|
||||||
|
// Sem buffer: processa byte a byte enquanto chegam. LED pisca vermelho.
|
||||||
|
case AGUARDA_BATCH_LEN1:
|
||||||
|
batchRemaining = (uint16_t)b << 8;
|
||||||
|
estado = AGUARDA_BATCH_LEN2;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case AGUARDA_BATCH_LEN2:
|
||||||
|
batchRemaining |= b;
|
||||||
|
if (batchRemaining == 0) { estado = AGUARDA_CMD; break; }
|
||||||
|
batchLedMs = millis();
|
||||||
|
batchLedOn = true;
|
||||||
|
ledCor(220, 0, 0); // vermelho: início do chunk
|
||||||
|
estado = AGUARDA_BATCH_DATA;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case AGUARDA_BATCH_DATA: {
|
||||||
|
uint32_t now = millis();
|
||||||
|
if (now - batchLedMs >= 150) {
|
||||||
|
batchLedMs = now;
|
||||||
|
batchLedOn = !batchLedOn;
|
||||||
|
ledCor(batchLedOn ? 220 : 0, 0, 0);
|
||||||
|
}
|
||||||
|
Keyboard.write(b);
|
||||||
|
delay(5); // inter-char delay: evita drop do Shift no USB HID em rajada
|
||||||
|
if (--batchRemaining == 0) {
|
||||||
|
ledCor(basR, basG, basB); // restaura cor anterior
|
||||||
|
estado = AGUARDA_CMD;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Callback: chegada de dados pelo BLE (PC → ESP32) ─────────────────────────
|
||||||
|
// Apenas enfileira — não chama HID aqui para não bloquear a task BLE.
|
||||||
|
class RxCallback : public BLECharacteristicCallbacks {
|
||||||
|
void onWrite(BLECharacteristic* pChar) override {
|
||||||
|
String val = pChar->getValue();
|
||||||
|
for (int i = 0; i < val.length(); i++) {
|
||||||
|
uint8_t b = (uint8_t)val[i];
|
||||||
|
xQueueSend(rxQueue, &b, 0); // não bloqueia se a fila estiver cheia
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Callbacks de conexão / desconexão BLE ─────────────────────────────────────
|
||||||
|
class ServerCallbacks : public BLEServerCallbacks {
|
||||||
|
void onConnect(BLEServer*) override {
|
||||||
|
bleConn = true;
|
||||||
|
// LED permanece verde até o host enviar 'H'
|
||||||
|
}
|
||||||
|
void onDisconnect(BLEServer*) override {
|
||||||
|
bleConn = false;
|
||||||
|
estado = AGUARDA_CMD;
|
||||||
|
batchRemaining = 0;
|
||||||
|
Keyboard.releaseAll();
|
||||||
|
basR = 0; basG = 255; basB = 0;
|
||||||
|
ledCor(basR, basG, basB); // verde — aguardando host
|
||||||
|
BLEDevice::startAdvertising(); // permite reconexão imediata
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Setup ─────────────────────────────────────────────────────────────────────
|
||||||
|
void setup() {
|
||||||
|
// LED
|
||||||
|
pixel.begin();
|
||||||
|
pixel.setBrightness(LED_BRIGHTNESS);
|
||||||
|
ledCor(0, 255, 0); // verde — aguardando conexão
|
||||||
|
|
||||||
|
// USB HID (TinyUSB via USB OTG)
|
||||||
|
USB.productName("KVMote");
|
||||||
|
USB.manufacturerName("KVMote");
|
||||||
|
USB.begin();
|
||||||
|
Keyboard.begin();
|
||||||
|
Mouse.begin();
|
||||||
|
|
||||||
|
// BLE — NUS
|
||||||
|
BLEDevice::init("KVMote");
|
||||||
|
pServer = BLEDevice::createServer();
|
||||||
|
pServer->setCallbacks(new ServerCallbacks());
|
||||||
|
|
||||||
|
BLEService* pService = pServer->createService(NUS_SERVICE_UUID);
|
||||||
|
|
||||||
|
// RX: aceita Write e Write Without Response — sem criptografia (evita AccessDenied no Windows)
|
||||||
|
BLECharacteristic* pRxChar = pService->createCharacteristic(
|
||||||
|
NUS_RX_UUID,
|
||||||
|
BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_WRITE_NR
|
||||||
|
);
|
||||||
|
pRxChar->setAccessPermissions(ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE);
|
||||||
|
pRxChar->setCallbacks(new RxCallback());
|
||||||
|
|
||||||
|
// TX: apenas Notify (para [PONG]) — sem criptografia
|
||||||
|
pTxChar = pService->createCharacteristic(
|
||||||
|
NUS_TX_UUID,
|
||||||
|
BLECharacteristic::PROPERTY_NOTIFY
|
||||||
|
);
|
||||||
|
pTxChar->setAccessPermissions(ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE);
|
||||||
|
BLE2902* cccd = new BLE2902();
|
||||||
|
cccd->setAccessPermissions(ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE);
|
||||||
|
pTxChar->addDescriptor(cccd);
|
||||||
|
|
||||||
|
pService->start();
|
||||||
|
|
||||||
|
rxQueue = xQueueCreate(2048, sizeof(uint8_t));
|
||||||
|
|
||||||
|
BLEAdvertising* pAdv = BLEDevice::getAdvertising();
|
||||||
|
pAdv->addServiceUUID(NUS_SERVICE_UUID);
|
||||||
|
pAdv->setScanResponse(true);
|
||||||
|
pAdv->setMinPreferred(0x06); // melhora compatibilidade com iOS/Windows
|
||||||
|
BLEDevice::startAdvertising();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Loop ──────────────────────────────────────────────────────────────────────
|
||||||
|
void loop() {
|
||||||
|
// Drena a fila e processa no contexto do loop (seguro para TinyUSB HID)
|
||||||
|
uint8_t b;
|
||||||
|
while (xQueueReceive(rxQueue, &b, 0) == pdTRUE)
|
||||||
|
processaByte(b);
|
||||||
|
delay(1);
|
||||||
|
}
|
||||||
34
app.go
34
app.go
@ -8,12 +8,16 @@ import (
|
|||||||
"kvmote/internal/input"
|
"kvmote/internal/input"
|
||||||
"kvmote/internal/kvm"
|
"kvmote/internal/kvm"
|
||||||
"kvmote/internal/transport"
|
"kvmote/internal/transport"
|
||||||
|
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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
|
||||||
|
isMini bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewApp creates a new App application struct
|
// NewApp creates a new App application struct
|
||||||
@ -21,9 +25,33 @@ func NewApp() *App {
|
|||||||
t := transport.NewBleTransport()
|
t := transport.NewBleTransport()
|
||||||
h := input.NewInputHandler()
|
h := input.NewInputHandler()
|
||||||
e := kvm.NewEngine(t, h)
|
e := kvm.NewEngine(t, h)
|
||||||
return &App{
|
app := &App{
|
||||||
engine: e,
|
engine: e,
|
||||||
|
overlay: input.NewOverlayWindow(),
|
||||||
}
|
}
|
||||||
|
e.SetWindowManager(app)
|
||||||
|
return app
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) SetMiniMode() {
|
||||||
|
if a.isMini {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
a.isMini = true
|
||||||
|
runtime.EventsEmit(a.ctx, "window-mode", "mini")
|
||||||
|
|
||||||
|
// Overlay nativo Win32 no canto — não toca na janela principal
|
||||||
|
mx, my, mw, mh := a.engine.GetMonitorWorkArea()
|
||||||
|
a.overlay.Show(mx, my, mw, mh)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) RestoreNormalMode() {
|
||||||
|
if !a.isMini {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
a.isMini = false
|
||||||
|
runtime.EventsEmit(a.ctx, "window-mode", "normal")
|
||||||
|
a.overlay.Hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
// startup is called when the app starts. The context is saved
|
// startup is called when the app starts. The context is saved
|
||||||
@ -76,3 +104,7 @@ func (a *App) ChangeLayout(layout int) {
|
|||||||
func (a *App) SetPosition(pos int) {
|
func (a *App) SetPosition(pos int) {
|
||||||
a.engine.SetPosition(pos)
|
a.engine.SetPosition(pos)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) HandleScroll(delta int) {
|
||||||
|
a.engine.HandleManualScroll(delta)
|
||||||
|
}
|
||||||
|
|||||||
67
frontend/dist/index.html
vendored
67
frontend/dist/index.html
vendored
@ -115,9 +115,27 @@
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
.btn-cad { margin-top: 10px; background: #2a2a2a; color: #ccc; }
|
.btn-cad { margin-top: 10px; background: #2a2a2a; color: #ccc; }
|
||||||
|
|
||||||
|
/* Estilo Neon para Modo Mini */
|
||||||
|
.neon-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: black;
|
||||||
|
cursor: default !important; /* Força cursor visível */
|
||||||
|
}
|
||||||
|
.neon-text {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #39FF14;
|
||||||
|
text-shadow: 0 0 5px #39FF14, 0 0 10px #39FF14;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body x-data="kvmApp()">
|
<body x-data="kvmApp()" style="cursor: default !important;">
|
||||||
|
<template x-if="mode === 'normal'">
|
||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
<div class="section-title">Posição do PC Cliente:</div>
|
<div class="section-title">Posição do PC Cliente:</div>
|
||||||
|
|
||||||
@ -179,11 +197,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div class="status-bar">
|
<template x-if="mode === 'mini'">
|
||||||
|
<div class="neon-container" id="mini-bg">
|
||||||
|
<div class="neon-text" x-text="'KVMote [' + scrollCount + ']'"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="status-bar" x-show="mode === 'normal'">
|
||||||
<span x-text="statusText"></span>
|
<span x-text="statusText"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Área rolável fake para forçar captura de scroll -->
|
||||||
|
<div id="scroll-sink" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; overflow-y: scroll; z-index: -1; pointer-events: none;">
|
||||||
|
<div style="height: 5000px; width: 1px;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function kvmApp() {
|
function kvmApp() {
|
||||||
return {
|
return {
|
||||||
@ -192,6 +222,31 @@
|
|||||||
statusText: 'Desconectado',
|
statusText: 'Desconectado',
|
||||||
pos: 'right',
|
pos: 'right',
|
||||||
layout: 1,
|
layout: 1,
|
||||||
|
mode: 'normal',
|
||||||
|
scrollCount: 0,
|
||||||
|
init() {
|
||||||
|
if (window.runtime) {
|
||||||
|
window.runtime.EventsOn("window-mode", (m) => {
|
||||||
|
this.mode = m;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listener de Scroll interno ao kvmApp para acessar scrollCount
|
||||||
|
window.addEventListener('wheel', (event) => {
|
||||||
|
if (this.mode === 'mini') {
|
||||||
|
this.scrollCount++;
|
||||||
|
// Feedback visual rápido
|
||||||
|
const bg = document.getElementById('mini-bg');
|
||||||
|
if (bg) {
|
||||||
|
bg.style.backgroundColor = '#1a3300';
|
||||||
|
setTimeout(() => bg.style.backgroundColor = 'black', 50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (window.go && window.go.main && window.go.main.App) {
|
||||||
|
window.go.main.App.HandleScroll(Math.round(event.deltaY));
|
||||||
|
}
|
||||||
|
}, { passive: false });
|
||||||
|
},
|
||||||
toggleConnect() {
|
toggleConnect() {
|
||||||
if (this.connected) {
|
if (this.connected) {
|
||||||
window.go.main.App.Disconnect().then(res => {
|
window.go.main.App.Disconnect().then(res => {
|
||||||
@ -221,6 +276,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Captura o scroll globalmente na janela do KVMote
|
||||||
|
window.addEventListener('wheel', (event) => {
|
||||||
|
console.log("Wheel event:", event.deltaY);
|
||||||
|
if (window.go && window.go.main && window.go.main.App) {
|
||||||
|
window.go.main.App.HandleScroll(Math.round(event.deltaY));
|
||||||
|
}
|
||||||
|
}, { passive: false });
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -7,6 +7,12 @@ export function Connect():Promise<string>;
|
|||||||
|
|
||||||
export function Disconnect():Promise<string>;
|
export function Disconnect():Promise<string>;
|
||||||
|
|
||||||
|
export function HandleScroll(arg1:number):Promise<void>;
|
||||||
|
|
||||||
|
export function RestoreNormalMode():Promise<void>;
|
||||||
|
|
||||||
export function SendCtrlAltDel():Promise<void>;
|
export function SendCtrlAltDel():Promise<void>;
|
||||||
|
|
||||||
|
export function SetMiniMode():Promise<void>;
|
||||||
|
|
||||||
export function SetPosition(arg1:number):Promise<void>;
|
export function SetPosition(arg1:number):Promise<void>;
|
||||||
|
|||||||
@ -14,10 +14,22 @@ export function Disconnect() {
|
|||||||
return window['go']['main']['App']['Disconnect']();
|
return window['go']['main']['App']['Disconnect']();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function HandleScroll(arg1) {
|
||||||
|
return window['go']['main']['App']['HandleScroll'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RestoreNormalMode() {
|
||||||
|
return window['go']['main']['App']['RestoreNormalMode']();
|
||||||
|
}
|
||||||
|
|
||||||
export function SendCtrlAltDel() {
|
export function SendCtrlAltDel() {
|
||||||
return window['go']['main']['App']['SendCtrlAltDel']();
|
return window['go']['main']['App']['SendCtrlAltDel']();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function SetMiniMode() {
|
||||||
|
return window['go']['main']['App']['SetMiniMode']();
|
||||||
|
}
|
||||||
|
|
||||||
export function SetPosition(arg1) {
|
export function SetPosition(arg1) {
|
||||||
return window['go']['main']['App']['SetPosition'](arg1);
|
return window['go']['main']['App']['SetPosition'](arg1);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,4 +25,19 @@ type InputHandler interface {
|
|||||||
SetCursorPos(x, y int32) bool
|
SetCursorPos(x, y int32) bool
|
||||||
ShowCursor(show bool)
|
ShowCursor(show bool)
|
||||||
GetScreenResolution() (int32, int32)
|
GetScreenResolution() (int32, int32)
|
||||||
|
RequestFocus()
|
||||||
|
SetCursorClip(clip bool)
|
||||||
|
GetWindowPos() (int32, int32)
|
||||||
|
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)
|
||||||
|
GetRestoredRect() (x, y, w, h int32)
|
||||||
|
RegisterRawScrollSink(callback func(delta int16)) error
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
//go:build windows
|
//go:build windows
|
||||||
|
|
||||||
package input
|
package input
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -24,6 +25,85 @@ var (
|
|||||||
procGetSystemMetrics = user32.NewProc("GetSystemMetrics")
|
procGetSystemMetrics = user32.NewProc("GetSystemMetrics")
|
||||||
procSetProcessDPIAware = user32.NewProc("SetProcessDPIAware")
|
procSetProcessDPIAware = user32.NewProc("SetProcessDPIAware")
|
||||||
procPostThreadMessageW = user32.NewProc("PostThreadMessageW")
|
procPostThreadMessageW = user32.NewProc("PostThreadMessageW")
|
||||||
|
procGetActiveWindow = user32.NewProc("GetActiveWindow")
|
||||||
|
procSetForegroundWindow = user32.NewProc("SetForegroundWindow")
|
||||||
|
procGetWindowRect = user32.NewProc("GetWindowRect")
|
||||||
|
procClipCursor = user32.NewProc("ClipCursor")
|
||||||
|
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")
|
||||||
|
procGetWindowPlacement = user32.NewProc("GetWindowPlacement")
|
||||||
|
procGetWindowThreadProcessId = user32.NewProc("GetWindowThreadProcessId")
|
||||||
|
procAttachThreadInput = user32.NewProc("AttachThreadInput")
|
||||||
|
procBringWindowToTop = user32.NewProc("BringWindowToTop")
|
||||||
|
)
|
||||||
|
|
||||||
|
type RECT struct {
|
||||||
|
Left, Top, Right, Bottom int32
|
||||||
|
}
|
||||||
|
|
||||||
|
type WINDOWPLACEMENT struct {
|
||||||
|
Length uint32
|
||||||
|
Flags uint32
|
||||||
|
ShowCmd uint32
|
||||||
|
PtMinPosition Point
|
||||||
|
PtMaxPosition Point
|
||||||
|
RcNormalPosition RECT
|
||||||
|
RcDevice RECT
|
||||||
|
}
|
||||||
|
|
||||||
|
type MONITORINFO struct {
|
||||||
|
CbSize uint32
|
||||||
|
RcMonitor RECT
|
||||||
|
RcWork RECT
|
||||||
|
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 (
|
||||||
@ -40,7 +120,7 @@ func init() {
|
|||||||
|
|
||||||
type MSLLHOOKSTRUCT struct {
|
type MSLLHOOKSTRUCT struct {
|
||||||
Pt Point
|
Pt Point
|
||||||
MouseData uint32 // Mantemos uint32 mas vamos converter no callback
|
MouseData uint32
|
||||||
Flags uint32
|
Flags uint32
|
||||||
Time uint32
|
Time uint32
|
||||||
DwExtraInfo uintptr
|
DwExtraInfo uintptr
|
||||||
@ -58,6 +138,7 @@ type windowsInputHandler struct {
|
|||||||
mouseHook uintptr
|
mouseHook uintptr
|
||||||
keyHook uintptr
|
keyHook uintptr
|
||||||
tid uint32
|
tid uint32
|
||||||
|
hwnd uintptr
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewInputHandler() InputHandler {
|
func NewInputHandler() InputHandler {
|
||||||
@ -65,6 +146,10 @@ 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 {
|
||||||
|
// GetForegroundWindow é system-wide (GetActiveWindow é thread-local e pode retornar 0)
|
||||||
|
hwnd, _, _ := procGetForegroundWindow.Call()
|
||||||
|
h.hwnd = hwnd
|
||||||
|
|
||||||
ready := make(chan error, 1)
|
ready := make(chan error, 1)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
@ -81,7 +166,7 @@ func (h *windowsInputHandler) Install(ctx context.Context, onMouse func(MouseEve
|
|||||||
Data: info.MouseData,
|
Data: info.MouseData,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Se a engine tratar o evento, não passamos para o próximo hook
|
// Se a engine tratar o evento (retornar true), bloqueamos o Windows
|
||||||
if onMouse(ev) {
|
if onMouse(ev) {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
@ -160,7 +245,6 @@ func (h *windowsInputHandler) ShowCursor(show bool) {
|
|||||||
if show {
|
if show {
|
||||||
s = 1
|
s = 1
|
||||||
}
|
}
|
||||||
// Vamos usar uma abordagem mais direta se necessário, mas por enquanto:
|
|
||||||
procShowCursor.Call(uintptr(s))
|
procShowCursor.Call(uintptr(s))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,3 +253,218 @@ func (h *windowsInputHandler) GetScreenResolution() (int32, int32) {
|
|||||||
h_res, _, _ := procGetSystemMetrics.Call(SM_CYSCREEN)
|
h_res, _, _ := procGetSystemMetrics.Call(SM_CYSCREEN)
|
||||||
return int32(w), int32(h_res)
|
return int32(w), int32(h_res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *windowsInputHandler) RequestFocus() {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsInputHandler) SetCursorClip(clip bool) {
|
||||||
|
// Removido temporariamente para testes
|
||||||
|
if !clip {
|
||||||
|
procClipCursor.Call(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsInputHandler) GetWindowPos() (int32, int32) {
|
||||||
|
if h.hwnd != 0 {
|
||||||
|
var rect RECT
|
||||||
|
procGetWindowRect.Call(h.hwnd, uintptr(unsafe.Pointer(&rect)))
|
||||||
|
return rect.Left, rect.Top
|
||||||
|
}
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsInputHandler) GetWindowSize() (int32, int32) {
|
||||||
|
if h.hwnd != 0 {
|
||||||
|
var rect RECT
|
||||||
|
procGetWindowRect.Call(h.hwnd, uintptr(unsafe.Pointer(&rect)))
|
||||||
|
return rect.Right - rect.Left, rect.Bottom - rect.Top
|
||||||
|
}
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsInputHandler) GetMonitorWorkArea() (x, y, w, ht int32) {
|
||||||
|
if h.hwnd == 0 {
|
||||||
|
return 0, 0, 1920, 1080
|
||||||
|
}
|
||||||
|
monitor, _, _ := procMonitorFromWindow.Call(h.hwnd, 2) // MONITOR_DEFAULTTONEAREST
|
||||||
|
if monitor == 0 {
|
||||||
|
return 0, 0, 1920, 1080
|
||||||
|
}
|
||||||
|
var mi MONITORINFO
|
||||||
|
mi.CbSize = uint32(unsafe.Sizeof(mi))
|
||||||
|
procGetMonitorInfoW.Call(monitor, uintptr(unsafe.Pointer(&mi)))
|
||||||
|
return mi.RcWork.Left, mi.RcWork.Top,
|
||||||
|
mi.RcWork.Right - mi.RcWork.Left,
|
||||||
|
mi.RcWork.Bottom - mi.RcWork.Top
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *windowsInputHandler) MoveWindow(x, y, w, ht int32, topmost bool) {
|
||||||
|
if h.hwnd == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// HWND_TOPMOST = -1, HWND_NOTOPMOST = -2
|
||||||
|
zorder := ^uintptr(1) // HWND_NOTOPMOST
|
||||||
|
if topmost {
|
||||||
|
zorder = ^uintptr(0) // HWND_TOPMOST
|
||||||
|
}
|
||||||
|
const SWP_NOACTIVATE = 0x0010
|
||||||
|
procSetWindowPos.Call(
|
||||||
|
h.hwnd, zorder,
|
||||||
|
uintptr(x), uintptr(y), uintptr(w), uintptr(ht),
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRestoredRect retorna posição e tamanho do KVMote no estado restaurado,
|
||||||
|
// mesmo que a janela esteja minimizada (GetWindowRect falha nesse caso).
|
||||||
|
func (h *windowsInputHandler) GetRestoredRect() (x, y, w, ht int32) {
|
||||||
|
if h.hwnd == 0 {
|
||||||
|
return 0, 0, 800, 600
|
||||||
|
}
|
||||||
|
var wp WINDOWPLACEMENT
|
||||||
|
wp.Length = uint32(unsafe.Sizeof(wp))
|
||||||
|
procGetWindowPlacement.Call(h.hwnd, uintptr(unsafe.Pointer(&wp)))
|
||||||
|
r := wp.RcNormalPosition
|
||||||
|
return r.Left, r.Top, r.Right - r.Left, r.Bottom - r.Top
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
neonMagenta = 0x00FF00FF // #FF00FF 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, neonMagenta)
|
||||||
|
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
|
||||||
|
}
|
||||||
@ -40,10 +40,17 @@ const (
|
|||||||
LayoutUsIntl
|
LayoutUsIntl
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type WindowManager interface {
|
||||||
|
SetMiniMode()
|
||||||
|
RestoreNormalMode()
|
||||||
|
}
|
||||||
|
|
||||||
type Engine struct {
|
type Engine struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
|
rawScrollOnce sync.Once
|
||||||
transport transport.Transport
|
transport transport.Transport
|
||||||
inputHandler input.InputHandler
|
inputHandler input.InputHandler
|
||||||
|
winManager WindowManager
|
||||||
|
|
||||||
clientMode bool
|
clientMode bool
|
||||||
clientPos ClientPos
|
clientPos ClientPos
|
||||||
@ -66,6 +73,13 @@ 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
|
||||||
|
|
||||||
|
kvmoteOrigX, kvmoteOrigY int32 // posição original da janela KVMote
|
||||||
|
kvmoteOrigW, kvmoteOrigH int32 // tamanho original da janela KVMote
|
||||||
|
kvmoteWasMinimized bool // se estava minimizado ao entrar em client mode
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewEngine(t transport.Transport, h input.InputHandler) *Engine {
|
func NewEngine(t transport.Transport, h input.InputHandler) *Engine {
|
||||||
@ -76,35 +90,81 @@ func NewEngine(t transport.Transport, h input.InputHandler) *Engine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *Engine) SetWindowManager(wm WindowManager) {
|
||||||
|
e.winManager = wm
|
||||||
|
}
|
||||||
|
|
||||||
func (e *Engine) Transport() transport.Transport {
|
func (e *Engine) Transport() transport.Transport {
|
||||||
return e.transport
|
return e.transport
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *Engine) GetScreenResolution() (int32, int32) {
|
||||||
|
return e.inputHandler.GetScreenResolution()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) GetWindowSize() (int32, int32) {
|
||||||
|
return e.inputHandler.GetWindowSize()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) GetWindowPos() (int32, int32) {
|
||||||
|
return e.inputHandler.GetWindowPos()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) GetMonitorWorkArea() (x, y, w, h int32) {
|
||||||
|
return e.inputHandler.GetMonitorWorkArea()
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
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) processarScroll(data uint32) {
|
func (e *Engine) onRawScroll(delta int16) {
|
||||||
|
e.mu.Lock()
|
||||||
|
defer e.mu.Unlock()
|
||||||
|
|
||||||
|
if !e.clientMode || !e.transport.IsConnected() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
e.scrollActive = true
|
e.scrollActive = true
|
||||||
e.scrollTimer = time.Now()
|
e.scrollTimer = time.Now()
|
||||||
|
|
||||||
delta := int32(int16(data >> 16))
|
// Mesmo acumulador e divisor do HandleManualScroll
|
||||||
e.wheelAccum += delta
|
// delta vem em unidades WHEEL_DELTA (±120 por notch)
|
||||||
|
e.wheelAccum += int32(delta)
|
||||||
const Divisor = 40 // Bem sensível para touchpad
|
const Divisor = 30
|
||||||
toSend := e.wheelAccum / Divisor
|
toSend := e.wheelAccum / Divisor
|
||||||
|
|
||||||
if toSend != 0 {
|
if toSend != 0 {
|
||||||
e.wheelAccum -= toSend * Divisor
|
e.wheelAccum -= toSend * Divisor
|
||||||
err := e.transport.Send([]byte{'W', byte(int8(clamp(int(toSend), -127, 127)))})
|
val := int8(clamp(int(toSend), -127, 127))
|
||||||
if err == nil {
|
go func(v int8) {
|
||||||
LogDebug(fmt.Sprintf("SCROLL: delta=%d enviado=%d", delta, toSend))
|
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()
|
||||||
defer e.mu.Unlock()
|
defer e.mu.Unlock()
|
||||||
@ -113,6 +173,11 @@ func (e *Engine) onMouse(ev input.MouseEvent) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LOG PARA DIAGNÓSTICO: Registrar qualquer mensagem que não seja movimento simples (0x0200)
|
||||||
|
if ev.Message != 0x0200 {
|
||||||
|
go LogDebug(fmt.Sprintf("MSG MOUSE: 0x%X | ClientMode: %v", ev.Message, e.clientMode))
|
||||||
|
}
|
||||||
|
|
||||||
if !e.clientMode {
|
if !e.clientMode {
|
||||||
if ev.Message == 0x0200 && e.isAtExitEdge(ev.Point) {
|
if ev.Message == 0x0200 && e.isAtExitEdge(ev.Point) {
|
||||||
e.enterClientMode(ev.Point)
|
e.enterClientMode(ev.Point)
|
||||||
@ -123,28 +188,23 @@ func (e *Engine) onMouse(ev input.MouseEvent) bool {
|
|||||||
|
|
||||||
// ─── MODO CLIENTE ATIVO ───
|
// ─── MODO CLIENTE ATIVO ───
|
||||||
|
|
||||||
// Log de qualquer evento que não seja movimento simples (para descobrir o ID do touchpad)
|
|
||||||
if ev.Message != 0x0200 {
|
|
||||||
LogDebug(fmt.Sprintf("Evento Mouse: 0x%X | Data: %d", ev.Message, ev.Data))
|
|
||||||
}
|
|
||||||
|
|
||||||
switch ev.Message {
|
switch ev.Message {
|
||||||
case 0x020A, 0x020E: // Roda Vertical ou Horizontal
|
case 0x020A, 0x020E: // Roda Vertical ou Horizontal
|
||||||
e.processarScroll(ev.Data)
|
// Não bloquear: evento passa para o webview → JS wheel → HandleManualScroll
|
||||||
return true
|
// (WH_MOUSE_LL não recebe scroll de touchpad precision; mini janela captura via JS)
|
||||||
|
e.scrollActive = true
|
||||||
|
e.scrollTimer = time.Now()
|
||||||
|
return false
|
||||||
|
|
||||||
case 0x0200: // Move
|
case 0x0200: // Move
|
||||||
if e.isWarping { e.isWarping = false; return true }
|
if e.isWarping { e.isWarping = false; return true }
|
||||||
|
|
||||||
if e.scrollActive {
|
if e.scrollActive {
|
||||||
if time.Since(e.scrollTimer) > 300*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
|
||||||
}
|
}
|
||||||
e.lastRawPos = ev.Point
|
} else {
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
dx, dy := ev.Point.X-e.lastRawPos.X, ev.Point.Y-e.lastRawPos.Y
|
dx, dy := ev.Point.X-e.lastRawPos.X, ev.Point.Y-e.lastRawPos.Y
|
||||||
e.virtualX += dx
|
e.virtualX += dx
|
||||||
e.virtualY += dy
|
e.virtualY += dy
|
||||||
@ -157,20 +217,20 @@ func (e *Engine) onMouse(ev input.MouseEvent) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
e.virtualX, e.virtualY = 0, 0
|
e.virtualX, e.virtualY = 0, 0
|
||||||
return true
|
} else if time.Since(e.mouseThrottle) >= 40*time.Millisecond {
|
||||||
}
|
|
||||||
|
|
||||||
w, h := e.inputHandler.GetScreenResolution()
|
|
||||||
e.isWarping = true
|
|
||||||
e.inputHandler.SetCursorPos(w/2, h/2)
|
|
||||||
e.lastRawPos = input.Point{X: w / 2, Y: h / 2}
|
|
||||||
|
|
||||||
if time.Since(e.mouseThrottle) >= 40*time.Millisecond {
|
|
||||||
e.mouseThrottle = time.Now()
|
e.mouseThrottle = time.Now()
|
||||||
sdx, sdy := int8(clamp(int(e.pendingDX), -127, 127)), int8(clamp(int(e.pendingDY), -127, 127))
|
sdx, sdy := int8(clamp(int(e.pendingDX), -127, 127)), int8(clamp(int(e.pendingDY), -127, 127))
|
||||||
e.pendingDX, e.pendingDY = 0, 0
|
e.pendingDX, e.pendingDY = 0, 0
|
||||||
e.transport.SendLossy([]byte{'M', byte(sdx), byte(sdy)})
|
e.transport.SendLossy([]byte{'M', byte(sdx), byte(sdy)})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-parka cursor no canto inferior direito (perto do relógio/bateria)
|
||||||
|
w, h := e.inputHandler.GetScreenResolution()
|
||||||
|
cx, cy := w-150, h-160
|
||||||
|
e.isWarping = true
|
||||||
|
e.inputHandler.SetCursorPos(cx, cy)
|
||||||
|
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
|
||||||
@ -214,11 +274,42 @@ func (e *Engine) enterClientMode(p input.Point) {
|
|||||||
e.wheelAccum = 0
|
e.wheelAccum = 0
|
||||||
e.mouseThrottle = time.Now()
|
e.mouseThrottle = time.Now()
|
||||||
|
|
||||||
|
// Salva estado original do KVMote (posição/tamanho/minimizado) e move pra direita
|
||||||
|
kvHwnd := e.inputHandler.GetAppHwnd()
|
||||||
|
e.kvmoteWasMinimized = e.inputHandler.IsMinimized(kvHwnd)
|
||||||
|
e.kvmoteOrigX, e.kvmoteOrigY, e.kvmoteOrigW, e.kvmoteOrigH = e.inputHandler.GetRestoredRect()
|
||||||
|
if e.kvmoteWasMinimized {
|
||||||
|
e.inputHandler.RestoreWindow(kvHwnd)
|
||||||
|
}
|
||||||
|
sw, sh := e.inputHandler.GetScreenResolution()
|
||||||
|
e.inputHandler.MoveWindow(sw-e.kvmoteOrigW, sh-350, e.kvmoteOrigW, 280, false)
|
||||||
|
LogDebug(fmt.Sprintf("KVMote movido para direita: x=%d y=%d w=%d h=300", sw-e.kvmoteOrigW, sh-350, e.kvmoteOrigW))
|
||||||
|
|
||||||
|
// Salva janela em foco e traz KVMote pra frente, se necessário
|
||||||
|
fgHwnd := e.inputHandler.GetForegroundWindow()
|
||||||
|
if fgHwnd != kvHwnd {
|
||||||
|
e.prevForegroundHwnd = fgHwnd
|
||||||
|
e.prevWasActive = true
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parka cursor no canto inferior direito (perto do relógio/bateria)
|
||||||
w, h := e.inputHandler.GetScreenResolution()
|
w, h := e.inputHandler.GetScreenResolution()
|
||||||
|
cx, cy := w-150, h-160
|
||||||
e.isWarping = true
|
e.isWarping = true
|
||||||
e.inputHandler.SetCursorPos(w/2, h/2)
|
e.inputHandler.SetCursorPos(cx, cy)
|
||||||
e.lastRawPos = input.Point{X: w / 2, Y: h / 2}
|
e.lastRawPos = input.Point{X: cx, Y: cy}
|
||||||
e.inputHandler.ShowCursor(false)
|
|
||||||
|
e.transport.Send([]byte{'A'})
|
||||||
e.transport.Send([]byte{'O'})
|
e.transport.Send([]byte{'O'})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -226,10 +317,34 @@ func (e *Engine) exitClientMode() {
|
|||||||
LogDebug("Saindo Modo Cliente.")
|
LogDebug("Saindo Modo Cliente.")
|
||||||
e.clientMode = false
|
e.clientMode = false
|
||||||
e.lastModeChange = time.Now()
|
e.lastModeChange = time.Now()
|
||||||
|
|
||||||
|
if e.winManager != nil {
|
||||||
|
e.winManager.RestoreNormalMode()
|
||||||
|
}
|
||||||
|
|
||||||
e.inputHandler.ShowCursor(true)
|
e.inputHandler.ShowCursor(true)
|
||||||
|
|
||||||
|
// Restaura posição/tamanho original do KVMote
|
||||||
|
if e.kvmoteOrigW > 0 {
|
||||||
|
e.inputHandler.MoveWindow(e.kvmoteOrigX, e.kvmoteOrigY, e.kvmoteOrigW, e.kvmoteOrigH, false)
|
||||||
|
LogDebug(fmt.Sprintf("KVMote restaurado: x=%d y=%d w=%d h=%d", e.kvmoteOrigX, e.kvmoteOrigY, e.kvmoteOrigW, e.kvmoteOrigH))
|
||||||
|
}
|
||||||
|
if e.kvmoteWasMinimized {
|
||||||
|
e.inputHandler.MinimizeWindow(e.inputHandler.GetAppHwnd())
|
||||||
|
e.kvmoteWasMinimized = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// Retornamos o cursor exatamente para a borda onde ele entrou
|
||||||
switch e.clientPos {
|
switch e.clientPos {
|
||||||
case PosRight: ret = input.Point{X: w - Offset, Y: e.edgeEntry.Y}
|
case PosRight: ret = input.Point{X: w - Offset, Y: e.edgeEntry.Y}
|
||||||
case PosLeft: ret = input.Point{X: Offset, Y: e.edgeEntry.Y}
|
case PosLeft: ret = input.Point{X: Offset, Y: e.edgeEntry.Y}
|
||||||
@ -242,6 +357,35 @@ func (e *Engine) exitClientMode() {
|
|||||||
e.transport.Send([]byte{'A'})
|
e.transport.Send([]byte{'A'})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *Engine) HandleManualScroll(delta int) {
|
||||||
|
e.mu.Lock()
|
||||||
|
defer e.mu.Unlock()
|
||||||
|
|
||||||
|
if !e.clientMode || !e.transport.IsConnected() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.scrollActive = true
|
||||||
|
e.scrollTimer = time.Now()
|
||||||
|
|
||||||
|
// Acumulamos o delta da UI para não perder movimentos pequenos
|
||||||
|
e.wheelAccum += int32(-delta) // Invertemos o delta da UI para bater com o padrão HID
|
||||||
|
|
||||||
|
// Divisor menor = Mais sensível
|
||||||
|
const Divisor = 15
|
||||||
|
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("UI SCROLL -> WHEEL %d (accum remain=%d)", v, e.wheelAccum))
|
||||||
|
}(val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (e *Engine) onKey(ev input.KeyboardEvent) bool {
|
func (e *Engine) onKey(ev input.KeyboardEvent) bool {
|
||||||
e.mu.Lock()
|
e.mu.Lock()
|
||||||
defer e.mu.Unlock()
|
defer e.mu.Unlock()
|
||||||
|
|||||||
11
main.go
11
main.go
@ -6,6 +6,7 @@ import (
|
|||||||
"github.com/wailsapp/wails/v2"
|
"github.com/wailsapp/wails/v2"
|
||||||
"github.com/wailsapp/wails/v2/pkg/options"
|
"github.com/wailsapp/wails/v2/pkg/options"
|
||||||
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/options/windows"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed all:frontend/dist
|
//go:embed all:frontend/dist
|
||||||
@ -20,6 +21,11 @@ func main() {
|
|||||||
Title: "KVMote",
|
Title: "KVMote",
|
||||||
Width: 400,
|
Width: 400,
|
||||||
Height: 550,
|
Height: 550,
|
||||||
|
DisableResize: false,
|
||||||
|
Fullscreen: false,
|
||||||
|
Frameless: false,
|
||||||
|
MinWidth: 200,
|
||||||
|
MinHeight: 50,
|
||||||
AssetServer: &assetserver.Options{
|
AssetServer: &assetserver.Options{
|
||||||
Assets: assets,
|
Assets: assets,
|
||||||
},
|
},
|
||||||
@ -28,6 +34,11 @@ func main() {
|
|||||||
Bind: []interface{}{
|
Bind: []interface{}{
|
||||||
app,
|
app,
|
||||||
},
|
},
|
||||||
|
Windows: &windows.Options{
|
||||||
|
DisableWindowIcon: false,
|
||||||
|
WebviewIsTransparent: false,
|
||||||
|
WindowIsTranslucent: false,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user