Compare commits
No commits in common. "879ae24ca656412f8e66315d38bbdd0fc5cfa00c" and "552b2ca62c40d7b9163b2a7eacfc77d9d5371fc1" have entirely different histories.
879ae24ca6
...
552b2ca62c
@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"permissions": {
|
|
||||||
"allow": [
|
|
||||||
"Bash(go build:*)",
|
|
||||||
"Bash(git:*)"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
23
CLAUDE.md
23
CLAUDE.md
@ -1,8 +1,4 @@
|
|||||||
# CLAUDE.md
|
# CLAUDE.md — KVMote (Go/Wails)
|
||||||
|
|
||||||
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`).
|
||||||
|
|
||||||
@ -34,21 +30,16 @@ 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; implementa WindowManager
|
app.go — App struct, métodos expostos ao frontend
|
||||||
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
|
||||||
@ -129,17 +120,9 @@ 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 nem da goroutine do overlay
|
- **runtime.LockOSThread:** nunca remover da goroutine de hooks
|
||||||
- **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)
|
|
||||||
|
|||||||
@ -1,307 +0,0 @@
|
|||||||
/*
|
|
||||||
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,16 +8,12 @@ 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
|
||||||
@ -25,33 +21,9 @@ 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)
|
||||||
app := &App{
|
return &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
|
||||||
@ -104,7 +76,3 @@ 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,27 +115,9 @@
|
|||||||
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()" style="cursor: default !important;">
|
<body x-data="kvmApp()">
|
||||||
<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>
|
||||||
|
|
||||||
@ -197,23 +179,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
|
|
||||||
<template x-if="mode === 'mini'">
|
<div class="status-bar">
|
||||||
<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 {
|
||||||
@ -222,31 +192,6 @@
|
|||||||
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 => {
|
||||||
@ -276,14 +221,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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,12 +7,6 @@ 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,22 +14,10 @@ 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,19 +25,4 @@ 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,5 +1,4 @@
|
|||||||
//go:build windows
|
//go:build windows
|
||||||
|
|
||||||
package input
|
package input
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -25,85 +24,6 @@ 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 (
|
||||||
@ -120,7 +40,7 @@ func init() {
|
|||||||
|
|
||||||
type MSLLHOOKSTRUCT struct {
|
type MSLLHOOKSTRUCT struct {
|
||||||
Pt Point
|
Pt Point
|
||||||
MouseData uint32
|
MouseData uint32 // Mantemos uint32 mas vamos converter no callback
|
||||||
Flags uint32
|
Flags uint32
|
||||||
Time uint32
|
Time uint32
|
||||||
DwExtraInfo uintptr
|
DwExtraInfo uintptr
|
||||||
@ -138,7 +58,6 @@ type windowsInputHandler struct {
|
|||||||
mouseHook uintptr
|
mouseHook uintptr
|
||||||
keyHook uintptr
|
keyHook uintptr
|
||||||
tid uint32
|
tid uint32
|
||||||
hwnd uintptr
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewInputHandler() InputHandler {
|
func NewInputHandler() InputHandler {
|
||||||
@ -146,10 +65,6 @@ 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() {
|
||||||
@ -166,7 +81,7 @@ func (h *windowsInputHandler) Install(ctx context.Context, onMouse func(MouseEve
|
|||||||
Data: info.MouseData,
|
Data: info.MouseData,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Se a engine tratar o evento (retornar true), bloqueamos o Windows
|
// Se a engine tratar o evento, não passamos para o próximo hook
|
||||||
if onMouse(ev) {
|
if onMouse(ev) {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
@ -245,6 +160,7 @@ 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))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -253,218 +169,3 @@ 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
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,203 +0,0 @@
|
|||||||
//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,17 +40,10 @@ 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
|
||||||
@ -73,13 +66,6 @@ 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 {
|
||||||
@ -90,81 +76,35 @@ 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) onRawScroll(delta int16) {
|
func (e *Engine) processarScroll(data uint32) {
|
||||||
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()
|
||||||
|
|
||||||
// Mesmo acumulador e divisor do HandleManualScroll
|
delta := int32(int16(data >> 16))
|
||||||
// delta vem em unidades WHEEL_DELTA (±120 por notch)
|
e.wheelAccum += delta
|
||||||
e.wheelAccum += int32(delta)
|
|
||||||
const Divisor = 30
|
const Divisor = 40 // Bem sensível para touchpad
|
||||||
toSend := e.wheelAccum / Divisor
|
toSend := e.wheelAccum / Divisor
|
||||||
|
|
||||||
if toSend != 0 {
|
if toSend != 0 {
|
||||||
e.wheelAccum -= toSend * Divisor
|
e.wheelAccum -= toSend * Divisor
|
||||||
val := int8(clamp(int(toSend), -127, 127))
|
err := e.transport.Send([]byte{'W', byte(int8(clamp(int(toSend), -127, 127)))})
|
||||||
go func(v int8) {
|
if err == nil {
|
||||||
e.transport.Send([]byte{'W', byte(v)})
|
LogDebug(fmt.Sprintf("SCROLL: delta=%d enviado=%d", delta, toSend))
|
||||||
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()
|
||||||
@ -173,11 +113,6 @@ 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)
|
||||||
@ -188,24 +123,29 @@ 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
|
||||||
// Não bloquear: evento passa para o webview → JS wheel → HandleManualScroll
|
e.processarScroll(ev.Data)
|
||||||
// (WH_MOUSE_LL não recebe scroll de touchpad precision; mini janela captura via JS)
|
return true
|
||||||
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) > 250*time.Millisecond {
|
if time.Since(e.scrollTimer) > 300*time.Millisecond {
|
||||||
e.scrollActive = false
|
e.scrollActive = false
|
||||||
e.virtualX, e.virtualY = 0, 0
|
e.virtualX, e.virtualY = 0, 0
|
||||||
}
|
}
|
||||||
} else {
|
e.lastRawPos = ev.Point
|
||||||
dx, dy := ev.Point.X-e.lastRawPos.X, ev.Point.Y-e.lastRawPos.Y
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
e.pendingDX += dx
|
e.pendingDX += dx
|
||||||
@ -217,20 +157,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
|
||||||
} else if time.Since(e.mouseThrottle) >= 40*time.Millisecond {
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
@ -274,42 +214,11 @@ 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(cx, cy)
|
e.inputHandler.SetCursorPos(w/2, h/2)
|
||||||
e.lastRawPos = input.Point{X: cx, Y: cy}
|
e.lastRawPos = input.Point{X: w / 2, Y: h / 2}
|
||||||
|
e.inputHandler.ShowCursor(false)
|
||||||
e.transport.Send([]byte{'A'})
|
|
||||||
e.transport.Send([]byte{'O'})
|
e.transport.Send([]byte{'O'})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -317,34 +226,10 @@ 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}
|
||||||
@ -357,35 +242,6 @@ 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,7 +6,6 @@ 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
|
||||||
@ -21,11 +20,6 @@ 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,
|
||||||
},
|
},
|
||||||
@ -34,11 +28,6 @@ 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