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`).
|
||||
|
||||
@ -30,16 +34,21 @@ Hardware suportado: ESP32-S3 (BLE NUS) e Arduino Leonardo + HC-06 (Serial SPP).
|
||||
|
||||
```
|
||||
main.go — entry point Wails, bind App
|
||||
app.go — App struct, métodos expostos ao frontend
|
||||
app.go — App struct, métodos expostos ao frontend; implementa WindowManager
|
||||
internal/
|
||||
transport/
|
||||
transport.go — interface Transport (Detect/Connect/Send/SendLossy...)
|
||||
ble_windows.go — BLE NUS via tinygo/bluetooth
|
||||
kvm/
|
||||
engine.go — lógica KVM: mouse, teclado, clipboard, modo cliente
|
||||
WindowManager interface (SetMiniMode/RestoreNormalMode)
|
||||
LogDebug → kvmote_debug.log (arquivo na raiz)
|
||||
input/
|
||||
input.go — interface InputHandler + tipos (Point, MouseEvent, KeyboardEvent)
|
||||
input_windows.go — hooks Win32, SetCursorPos, ShowCursor, GetSystemMetrics
|
||||
overlay_windows.go — janela Win32 nativa independente do Wails (neon "KVMote")
|
||||
aparece no canto durante modo cliente; usa sua própria goroutine
|
||||
com LockOSThread + message pump próprio
|
||||
frontend/
|
||||
dist/index.html — UI
|
||||
wails.json — config Wails
|
||||
@ -120,9 +129,17 @@ Saída: `build/bin/kvmote.exe`
|
||||
|
||||
---
|
||||
|
||||
## Mini-mode / Overlay
|
||||
|
||||
Ao entrar em modo cliente, `engine` chama `WindowManager.SetMiniMode()` → `App` emite evento `window-mode:mini` ao frontend e exibe `OverlayWindow` nativo Win32. `OverlayWindow` tem goroutine própria com `LockOSThread` e message pump independente do Wails — não interfere com WebView2, Raw Input, nem coordenadas Wails.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Cuidados
|
||||
|
||||
- **Scroll touchpad:** `scrollActive` e `scrollTimer` não devem ser resetados em enter/exitClientMode
|
||||
- **runtime.LockOSThread:** nunca remover da goroutine de hooks
|
||||
- **runtime.LockOSThread:** nunca remover da goroutine de hooks nem da goroutine do overlay
|
||||
- **isWarping flag:** previne loop infinito SetCursorPos → WM_MOUSEMOVE → SetCursorPos
|
||||
- **Mutex ordering:** engine.mu protege todo estado KVM; transport.mu protege conexão BLE
|
||||
- **overlayOnce:** `RegisterClassExW` só pode ser chamado uma vez por processo; overlay usa `sync.Once`
|
||||
- **LogDebug:** escreve em `kvmote_debug.log` na raiz do projeto (ignorado pelo git)
|
||||
|
||||
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);
|
||||
}
|
||||
40
app.go
40
app.go
@ -8,12 +8,16 @@ import (
|
||||
"kvmote/internal/input"
|
||||
"kvmote/internal/kvm"
|
||||
"kvmote/internal/transport"
|
||||
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
// App struct
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
engine *kvm.Engine
|
||||
ctx context.Context
|
||||
engine *kvm.Engine
|
||||
overlay *input.OverlayWindow
|
||||
isMini bool
|
||||
}
|
||||
|
||||
// NewApp creates a new App application struct
|
||||
@ -21,9 +25,33 @@ func NewApp() *App {
|
||||
t := transport.NewBleTransport()
|
||||
h := input.NewInputHandler()
|
||||
e := kvm.NewEngine(t, h)
|
||||
return &App{
|
||||
engine: e,
|
||||
app := &App{
|
||||
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
|
||||
@ -76,3 +104,7 @@ func (a *App) ChangeLayout(layout int) {
|
||||
func (a *App) SetPosition(pos int) {
|
||||
a.engine.SetPosition(pos)
|
||||
}
|
||||
|
||||
func (a *App) HandleScroll(delta int) {
|
||||
a.engine.HandleManualScroll(delta)
|
||||
}
|
||||
|
||||
167
frontend/dist/index.html
vendored
167
frontend/dist/index.html
vendored
@ -115,73 +115,103 @@
|
||||
font-weight: bold;
|
||||
}
|
||||
.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>
|
||||
</head>
|
||||
<body x-data="kvmApp()">
|
||||
<div class="app-container">
|
||||
<div class="section-title">Posição do PC Cliente:</div>
|
||||
<body x-data="kvmApp()" style="cursor: default !important;">
|
||||
<template x-if="mode === 'normal'">
|
||||
<div class="app-container">
|
||||
<div class="section-title">Posição do PC Cliente:</div>
|
||||
|
||||
<div class="monitor-grid">
|
||||
<!-- Top -->
|
||||
<button class="monitor-btn" style="grid-column: 2; grid-row: 1;"
|
||||
:class="pos === 'above' ? 'active' : ''" @click="setPos('above')">Acima</button>
|
||||
<div class="monitor-grid">
|
||||
<!-- Top -->
|
||||
<button class="monitor-btn" style="grid-column: 2; grid-row: 1;"
|
||||
:class="pos === 'above' ? 'active' : ''" @click="setPos('above')">Acima</button>
|
||||
|
||||
<!-- Left -->
|
||||
<button class="monitor-btn" style="grid-column: 1; grid-row: 2;"
|
||||
:class="pos === 'left' ? 'active' : ''" @click="setPos('left')">Esquerda</button>
|
||||
<!-- Left -->
|
||||
<button class="monitor-btn" style="grid-column: 1; grid-row: 2;"
|
||||
:class="pos === 'left' ? 'active' : ''" @click="setPos('left')">Esquerda</button>
|
||||
|
||||
<!-- Center -->
|
||||
<div class="host-pc">[HOST PC]</div>
|
||||
<!-- Center -->
|
||||
<div class="host-pc">[HOST PC]</div>
|
||||
|
||||
<!-- Right -->
|
||||
<button class="monitor-btn" style="grid-column: 3; grid-row: 2;"
|
||||
:class="pos === 'right' ? 'active' : ''" @click="setPos('right')">Direita</button>
|
||||
<!-- Right -->
|
||||
<button class="monitor-btn" style="grid-column: 3; grid-row: 2;"
|
||||
:class="pos === 'right' ? 'active' : ''" @click="setPos('right')">Direita</button>
|
||||
|
||||
<!-- Bottom -->
|
||||
<button class="monitor-btn" style="grid-column: 2; grid-row: 3;"
|
||||
:class="pos === 'below' ? 'active' : ''" @click="setPos('below')">Abaixo</button>
|
||||
</div>
|
||||
|
||||
<div class="is-flex is-align-items-center mb-4" style="font-size: 0.85rem; color: #888;">
|
||||
<span class="status-dot" :style="{ backgroundColor: detected ? '#3498db' : '#555' }"></span>
|
||||
<span x-text="detected ? 'KVMote (BLE) detectado' : 'Buscando dispositivo...'"></span>
|
||||
</div>
|
||||
|
||||
<div class="columns is-mobile is-gapless mb-2">
|
||||
<div class="column pr-1">
|
||||
<button class="btn-action" @click="detect()">Detectar</button>
|
||||
<!-- Bottom -->
|
||||
<button class="monitor-btn" style="grid-column: 2; grid-row: 3;"
|
||||
:class="pos === 'below' ? 'active' : ''" @click="setPos('below')">Abaixo</button>
|
||||
</div>
|
||||
<div class="column pl-1">
|
||||
<button class="btn-action btn-connect" @click="toggleConnect()" x-text="connected ? 'Desconectar' : 'Conectar'"></button>
|
||||
|
||||
<div class="is-flex is-align-items-center mb-4" style="font-size: 0.85rem; color: #888;">
|
||||
<span class="status-dot" :style="{ backgroundColor: detected ? '#3498db' : '#555' }"></span>
|
||||
<span x-text="detected ? 'KVMote (BLE) detectado' : 'Buscando dispositivo...'"></span>
|
||||
</div>
|
||||
|
||||
<div class="columns is-mobile is-gapless mb-2">
|
||||
<div class="column pr-1">
|
||||
<button class="btn-action" @click="detect()">Detectar</button>
|
||||
</div>
|
||||
<div class="column pl-1">
|
||||
<button class="btn-action btn-connect" @click="toggleConnect()" x-text="connected ? 'Desconectar' : 'Conectar'"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field mb-4">
|
||||
<div class="is-flex is-align-items-center is-justify-content-space-between">
|
||||
<label class="field-label">Layout do cliente:</label>
|
||||
<select class="custom-select" style="width: 60%;" x-model="layout" @change="setLayout()">
|
||||
<option value="0">US / EN</option>
|
||||
<option value="1">PT-BR ABNT2</option>
|
||||
<option value="2">US International</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn-action btn-cad" @click="sendCAD()">Enviar Ctrl+Alt+Del</button>
|
||||
|
||||
<div class="field mt-4">
|
||||
<div class="is-flex is-align-items-center is-justify-content-space-between">
|
||||
<label class="field-label">Ao fechar:</label>
|
||||
<select class="custom-select" style="width: 60%;">
|
||||
<option value="tray">Minimizar para Tray</option>
|
||||
<option value="exit">Fechar aplicação</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="field mb-4">
|
||||
<div class="is-flex is-align-items-center is-justify-content-space-between">
|
||||
<label class="field-label">Layout do cliente:</label>
|
||||
<select class="custom-select" style="width: 60%;" x-model="layout" @change="setLayout()">
|
||||
<option value="0">US / EN</option>
|
||||
<option value="1">PT-BR ABNT2</option>
|
||||
<option value="2">US International</option>
|
||||
</select>
|
||||
</div>
|
||||
<template x-if="mode === 'mini'">
|
||||
<div class="neon-container" id="mini-bg">
|
||||
<div class="neon-text" x-text="'KVMote [' + scrollCount + ']'"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<button class="btn-action btn-cad" @click="sendCAD()">Enviar Ctrl+Alt+Del</button>
|
||||
|
||||
<div class="field mt-4">
|
||||
<div class="is-flex is-align-items-center is-justify-content-space-between">
|
||||
<label class="field-label">Ao fechar:</label>
|
||||
<select class="custom-select" style="width: 60%;">
|
||||
<option value="tray">Minimizar para Tray</option>
|
||||
<option value="exit">Fechar aplicação</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-bar" x-show="mode === 'normal'">
|
||||
<span x-text="statusText"></span>
|
||||
</div>
|
||||
|
||||
<div class="status-bar">
|
||||
<span x-text="statusText"></span>
|
||||
<!-- Á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>
|
||||
@ -192,6 +222,31 @@
|
||||
statusText: 'Desconectado',
|
||||
pos: 'right',
|
||||
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() {
|
||||
if (this.connected) {
|
||||
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>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -7,6 +7,12 @@ export function Connect():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 SetMiniMode():Promise<void>;
|
||||
|
||||
export function SetPosition(arg1:number):Promise<void>;
|
||||
|
||||
@ -14,10 +14,22 @@ export function 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() {
|
||||
return window['go']['main']['App']['SendCtrlAltDel']();
|
||||
}
|
||||
|
||||
export function SetMiniMode() {
|
||||
return window['go']['main']['App']['SetMiniMode']();
|
||||
}
|
||||
|
||||
export function SetPosition(arg1) {
|
||||
return window['go']['main']['App']['SetPosition'](arg1);
|
||||
}
|
||||
|
||||
@ -25,4 +25,19 @@ type InputHandler interface {
|
||||
SetCursorPos(x, y int32) bool
|
||||
ShowCursor(show bool)
|
||||
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
|
||||
|
||||
package input
|
||||
|
||||
import (
|
||||
@ -11,19 +12,98 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
user32 = windows.NewLazySystemDLL("user32.dll")
|
||||
user32 = windows.NewLazySystemDLL("user32.dll")
|
||||
kernel32 = windows.NewLazySystemDLL("kernel32.dll")
|
||||
|
||||
procSetWindowsHookExW = user32.NewProc("SetWindowsHookExW")
|
||||
procUnhookWindowsHookEx = user32.NewProc("UnhookWindowsHookEx")
|
||||
procCallNextHookEx = user32.NewProc("CallNextHookEx")
|
||||
procGetMessageW = user32.NewProc("GetMessageW")
|
||||
procSetCursorPos = user32.NewProc("SetCursorPos")
|
||||
procShowCursor = user32.NewProc("ShowCursor")
|
||||
procGetModuleHandleW = kernel32.NewProc("GetModuleHandleW")
|
||||
procGetSystemMetrics = user32.NewProc("GetSystemMetrics")
|
||||
procSetProcessDPIAware = user32.NewProc("SetProcessDPIAware")
|
||||
procPostThreadMessageW = user32.NewProc("PostThreadMessageW")
|
||||
procSetWindowsHookExW = user32.NewProc("SetWindowsHookExW")
|
||||
procUnhookWindowsHookEx = user32.NewProc("UnhookWindowsHookEx")
|
||||
procCallNextHookEx = user32.NewProc("CallNextHookEx")
|
||||
procGetMessageW = user32.NewProc("GetMessageW")
|
||||
procSetCursorPos = user32.NewProc("SetCursorPos")
|
||||
procShowCursor = user32.NewProc("ShowCursor")
|
||||
procGetModuleHandleW = kernel32.NewProc("GetModuleHandleW")
|
||||
procGetSystemMetrics = user32.NewProc("GetSystemMetrics")
|
||||
procSetProcessDPIAware = user32.NewProc("SetProcessDPIAware")
|
||||
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 (
|
||||
@ -40,7 +120,7 @@ func init() {
|
||||
|
||||
type MSLLHOOKSTRUCT struct {
|
||||
Pt Point
|
||||
MouseData uint32 // Mantemos uint32 mas vamos converter no callback
|
||||
MouseData uint32
|
||||
Flags uint32
|
||||
Time uint32
|
||||
DwExtraInfo uintptr
|
||||
@ -58,6 +138,7 @@ type windowsInputHandler struct {
|
||||
mouseHook uintptr
|
||||
keyHook uintptr
|
||||
tid uint32
|
||||
hwnd uintptr
|
||||
}
|
||||
|
||||
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 {
|
||||
// GetForegroundWindow é system-wide (GetActiveWindow é thread-local e pode retornar 0)
|
||||
hwnd, _, _ := procGetForegroundWindow.Call()
|
||||
h.hwnd = hwnd
|
||||
|
||||
ready := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
@ -81,7 +166,7 @@ func (h *windowsInputHandler) Install(ctx context.Context, onMouse func(MouseEve
|
||||
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) {
|
||||
return 1
|
||||
}
|
||||
@ -160,7 +245,6 @@ func (h *windowsInputHandler) ShowCursor(show bool) {
|
||||
if show {
|
||||
s = 1
|
||||
}
|
||||
// Vamos usar uma abordagem mais direta se necessário, mas por enquanto:
|
||||
procShowCursor.Call(uintptr(s))
|
||||
}
|
||||
|
||||
@ -169,3 +253,218 @@ func (h *windowsInputHandler) GetScreenResolution() (int32, int32) {
|
||||
h_res, _, _ := procGetSystemMetrics.Call(SM_CYSCREEN)
|
||||
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
|
||||
)
|
||||
|
||||
type WindowManager interface {
|
||||
SetMiniMode()
|
||||
RestoreNormalMode()
|
||||
}
|
||||
|
||||
type Engine struct {
|
||||
mu sync.Mutex
|
||||
transport transport.Transport
|
||||
inputHandler input.InputHandler
|
||||
mu sync.Mutex
|
||||
rawScrollOnce sync.Once
|
||||
transport transport.Transport
|
||||
inputHandler input.InputHandler
|
||||
winManager WindowManager
|
||||
|
||||
clientMode bool
|
||||
clientPos ClientPos
|
||||
@ -66,6 +73,13 @@ type Engine struct {
|
||||
wheelAccum int32
|
||||
|
||||
mouseThrottle time.Time
|
||||
|
||||
prevForegroundHwnd uintptr // janela que estava em foco antes de entrar em client mode
|
||||
prevWasActive bool // true se salvamos uma janela diferente do KVMote
|
||||
|
||||
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 {
|
||||
@ -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 {
|
||||
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 {
|
||||
w, h := e.inputHandler.GetScreenResolution()
|
||||
LogDebug(fmt.Sprintf("Engine Iniciada. Tela: %dx%d. Pos: %v", w, h, e.clientPos))
|
||||
|
||||
// Registra apenas uma vez — Connect() pode chamar Start() múltiplas vezes
|
||||
e.rawScrollOnce.Do(func() {
|
||||
if err := e.inputHandler.RegisterRawScrollSink(e.onRawScroll); err != nil {
|
||||
LogDebug(fmt.Sprintf("Raw scroll sink falhou: %v", err))
|
||||
} else {
|
||||
LogDebug("Raw scroll sink registrado OK")
|
||||
}
|
||||
})
|
||||
|
||||
return e.inputHandler.Install(ctx, e.onMouse, e.onKey)
|
||||
}
|
||||
|
||||
func (e *Engine) 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.scrollTimer = time.Now()
|
||||
|
||||
delta := int32(int16(data >> 16))
|
||||
e.wheelAccum += delta
|
||||
|
||||
const Divisor = 40 // Bem sensível para touchpad
|
||||
// Mesmo acumulador e divisor do HandleManualScroll
|
||||
// delta vem em unidades WHEEL_DELTA (±120 por notch)
|
||||
e.wheelAccum += int32(delta)
|
||||
const Divisor = 30
|
||||
toSend := e.wheelAccum / Divisor
|
||||
|
||||
if toSend != 0 {
|
||||
e.wheelAccum -= toSend * Divisor
|
||||
err := e.transport.Send([]byte{'W', byte(int8(clamp(int(toSend), -127, 127)))})
|
||||
if err == nil {
|
||||
LogDebug(fmt.Sprintf("SCROLL: delta=%d enviado=%d", delta, toSend))
|
||||
}
|
||||
val := int8(clamp(int(toSend), -127, 127))
|
||||
go func(v int8) {
|
||||
e.transport.Send([]byte{'W', byte(v)})
|
||||
LogDebug(fmt.Sprintf("RAW SCROLL: delta=%d → W=%d", delta, v))
|
||||
}(val)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func (e *Engine) onMouse(ev input.MouseEvent) bool {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
@ -113,6 +173,11 @@ func (e *Engine) onMouse(ev input.MouseEvent) bool {
|
||||
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 ev.Message == 0x0200 && e.isAtExitEdge(ev.Point) {
|
||||
e.enterClientMode(ev.Point)
|
||||
@ -123,54 +188,49 @@ func (e *Engine) onMouse(ev input.MouseEvent) bool {
|
||||
|
||||
// ─── 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 {
|
||||
case 0x020A, 0x020E: // Roda Vertical ou Horizontal
|
||||
e.processarScroll(ev.Data)
|
||||
return true
|
||||
// Não bloquear: evento passa para o webview → JS wheel → HandleManualScroll
|
||||
// (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
|
||||
if e.isWarping { e.isWarping = false; return true }
|
||||
|
||||
if e.scrollActive {
|
||||
if time.Since(e.scrollTimer) > 300*time.Millisecond {
|
||||
if time.Since(e.scrollTimer) > 250*time.Millisecond {
|
||||
e.scrollActive = false
|
||||
e.virtualX, e.virtualY = 0, 0
|
||||
}
|
||||
e.lastRawPos = ev.Point
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
dx, dy := ev.Point.X-e.lastRawPos.X, ev.Point.Y-e.lastRawPos.Y
|
||||
e.virtualX += dx
|
||||
e.virtualY += dy
|
||||
e.pendingDX += dx
|
||||
e.pendingDY += dy
|
||||
|
||||
dx, dy := ev.Point.X - e.lastRawPos.X, ev.Point.Y - e.lastRawPos.Y
|
||||
e.virtualX += dx
|
||||
e.virtualY += dy
|
||||
e.pendingDX += dx
|
||||
e.pendingDY += dy
|
||||
|
||||
if e.shouldReturnToHost() {
|
||||
if time.Since(e.lastModeChange) > 800*time.Millisecond {
|
||||
e.exitClientMode()
|
||||
return true
|
||||
if e.shouldReturnToHost() {
|
||||
if time.Since(e.lastModeChange) > 800*time.Millisecond {
|
||||
e.exitClientMode()
|
||||
return true
|
||||
}
|
||||
e.virtualX, e.virtualY = 0, 0
|
||||
} else if time.Since(e.mouseThrottle) >= 40*time.Millisecond {
|
||||
e.mouseThrottle = time.Now()
|
||||
sdx, sdy := int8(clamp(int(e.pendingDX), -127, 127)), int8(clamp(int(e.pendingDY), -127, 127))
|
||||
e.pendingDX, e.pendingDY = 0, 0
|
||||
e.transport.SendLossy([]byte{'M', byte(sdx), byte(sdy)})
|
||||
}
|
||||
e.virtualX, e.virtualY = 0, 0
|
||||
return true
|
||||
}
|
||||
|
||||
// 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(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()
|
||||
sdx, sdy := int8(clamp(int(e.pendingDX), -127, 127)), int8(clamp(int(e.pendingDY), -127, 127))
|
||||
e.pendingDX, e.pendingDY = 0, 0
|
||||
e.transport.SendLossy([]byte{'M', byte(sdx), byte(sdy)})
|
||||
}
|
||||
e.inputHandler.SetCursorPos(cx, cy)
|
||||
e.lastRawPos = input.Point{X: cx, Y: cy}
|
||||
return true
|
||||
|
||||
case 0x0201: e.transport.Send([]byte{'D', 'L'}); return true
|
||||
@ -214,11 +274,42 @@ func (e *Engine) enterClientMode(p input.Point) {
|
||||
e.wheelAccum = 0
|
||||
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()
|
||||
cx, cy := w-150, h-160
|
||||
e.isWarping = true
|
||||
e.inputHandler.SetCursorPos(w/2, h/2)
|
||||
e.lastRawPos = input.Point{X: w / 2, Y: h / 2}
|
||||
e.inputHandler.ShowCursor(false)
|
||||
e.inputHandler.SetCursorPos(cx, cy)
|
||||
e.lastRawPos = input.Point{X: cx, Y: cy}
|
||||
|
||||
e.transport.Send([]byte{'A'})
|
||||
e.transport.Send([]byte{'O'})
|
||||
}
|
||||
|
||||
@ -226,10 +317,34 @@ func (e *Engine) exitClientMode() {
|
||||
LogDebug("Saindo Modo Cliente.")
|
||||
e.clientMode = false
|
||||
e.lastModeChange = time.Now()
|
||||
|
||||
if e.winManager != nil {
|
||||
e.winManager.RestoreNormalMode()
|
||||
}
|
||||
|
||||
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()
|
||||
var ret input.Point
|
||||
const Offset = 120
|
||||
// Retornamos o cursor exatamente para a borda onde ele entrou
|
||||
switch e.clientPos {
|
||||
case PosRight: ret = input.Point{X: w - 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'})
|
||||
}
|
||||
|
||||
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 {
|
||||
e.mu.Lock()
|
||||
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/pkg/options"
|
||||
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
||||
"github.com/wailsapp/wails/v2/pkg/options/windows"
|
||||
)
|
||||
|
||||
//go:embed all:frontend/dist
|
||||
@ -20,6 +21,11 @@ func main() {
|
||||
Title: "KVMote",
|
||||
Width: 400,
|
||||
Height: 550,
|
||||
DisableResize: false,
|
||||
Fullscreen: false,
|
||||
Frameless: false,
|
||||
MinWidth: 200,
|
||||
MinHeight: 50,
|
||||
AssetServer: &assetserver.Options{
|
||||
Assets: assets,
|
||||
},
|
||||
@ -28,6 +34,11 @@ func main() {
|
||||
Bind: []interface{}{
|
||||
app,
|
||||
},
|
||||
Windows: &windows.Options{
|
||||
DisableWindowIcon: false,
|
||||
WebviewIsTransparent: false,
|
||||
WindowIsTranslucent: false,
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user