mirror of
https://github.com/kasmtech/KasmVNC.git
synced 2024-12-01 12:33:20 +01:00
25b8e64adb
This change adds support for the VMware Mouse Position pseudo-encoding[1], which is used to notify VNC clients when X11 clients call `XWarpPointer()`[2]. This function is called by SDL (and other similar libraries) when they detect that the server does not support native relative motion, like some RFB clients. With this, RFB clients can choose to adjust the local cursor position under certain circumstances to match what the server has set. For instance, if pointer lock has been enabled on the client's machine and the cursor is not being drawn locally, the local position of the cursor is irrelevant, so the RFB client can use what the server sends as the canonical absolute position of the cursor. This ultimately enables the possibility of games (especially FPS games) to behave how users expect (if the clients implement the corresponding change). Part of: #619 1: https://github.com/rfbproto/rfbproto/blob/master/rfbproto.rst#vmware-cursor-position-pseudo-encoding 2: https://tronche.com/gui/x/xlib/input/XWarpPointer.html 3: https://hg.libsdl.org/SDL/file/28e3b60e2131/src/events/SDL_mouse.c#l804
649 lines
16 KiB
C++
649 lines
16 KiB
C++
/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved.
|
|
* Copyright (C) 2011 D. R. Commander. All Rights Reserved.
|
|
* Copyright 2009-2017 Pierre Ossman for Cendio AB
|
|
*
|
|
* This is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This software is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this software; if not, write to the Free Software
|
|
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
|
|
* USA.
|
|
*/
|
|
#include <stdio.h>
|
|
#include <rdr/OutStream.h>
|
|
#include <rfb/msgTypes.h>
|
|
#include <rfb/fenceTypes.h>
|
|
#include <rfb/Exception.h>
|
|
#include <rfb/ConnParams.h>
|
|
#include <rfb/UpdateTracker.h>
|
|
#include <rfb/Encoder.h>
|
|
#include <rfb/SMsgWriter.h>
|
|
#include <rfb/LogWriter.h>
|
|
#include <rfb/ledStates.h>
|
|
|
|
using namespace rfb;
|
|
|
|
static LogWriter vlog("SMsgWriter");
|
|
|
|
SMsgWriter::SMsgWriter(ConnParams* cp_, rdr::OutStream* os_)
|
|
: cp(cp_), os(os_),
|
|
nRectsInUpdate(0), nRectsInHeader(0),
|
|
needSetDesktopSize(false), needExtendedDesktopSize(false),
|
|
needSetDesktopName(false), needSetCursor(false),
|
|
needSetXCursor(false), needSetCursorWithAlpha(false),
|
|
needCursorPos(false),
|
|
needLEDState(false), needQEMUKeyEvent(false)
|
|
{
|
|
}
|
|
|
|
SMsgWriter::~SMsgWriter()
|
|
{
|
|
}
|
|
|
|
void SMsgWriter::writeServerInit()
|
|
{
|
|
os->writeU16(cp->width);
|
|
os->writeU16(cp->height);
|
|
cp->pf().write(os);
|
|
os->writeString(cp->name());
|
|
endMsg();
|
|
}
|
|
|
|
void SMsgWriter::writeSetColourMapEntries(int firstColour, int nColours,
|
|
const rdr::U16 red[],
|
|
const rdr::U16 green[],
|
|
const rdr::U16 blue[])
|
|
{
|
|
startMsg(msgTypeSetColourMapEntries);
|
|
os->pad(1);
|
|
os->writeU16(firstColour);
|
|
os->writeU16(nColours);
|
|
for (int i = firstColour; i < firstColour+nColours; i++) {
|
|
os->writeU16(red[i]);
|
|
os->writeU16(green[i]);
|
|
os->writeU16(blue[i]);
|
|
}
|
|
endMsg();
|
|
}
|
|
|
|
void SMsgWriter::writeBell()
|
|
{
|
|
startMsg(msgTypeBell);
|
|
endMsg();
|
|
}
|
|
|
|
void SMsgWriter::writeServerCutText(const char* str, int len)
|
|
{
|
|
startMsg(msgTypeServerCutText);
|
|
os->pad(3);
|
|
os->writeU32(len);
|
|
os->writeBytes(str, len);
|
|
endMsg();
|
|
}
|
|
|
|
void SMsgWriter::writeStats(const char* str, int len)
|
|
{
|
|
startMsg(msgTypeStats);
|
|
os->pad(3);
|
|
os->writeU32(len);
|
|
os->writeBytes(str, len);
|
|
endMsg();
|
|
}
|
|
|
|
void SMsgWriter::writeFence(rdr::U32 flags, unsigned len, const char data[])
|
|
{
|
|
if (!cp->supportsFence)
|
|
throw Exception("Client does not support fences");
|
|
if (len > 64)
|
|
throw Exception("Too large fence payload");
|
|
if ((flags & ~fenceFlagsSupported) != 0)
|
|
throw Exception("Unknown fence flags");
|
|
|
|
startMsg(msgTypeServerFence);
|
|
os->pad(3);
|
|
|
|
os->writeU32(flags);
|
|
|
|
os->writeU8(len);
|
|
|
|
if (len > 0)
|
|
os->writeBytes(data, len);
|
|
|
|
endMsg();
|
|
}
|
|
|
|
void SMsgWriter::writeEndOfContinuousUpdates()
|
|
{
|
|
if (!cp->supportsContinuousUpdates)
|
|
throw Exception("Client does not support continuous updates");
|
|
|
|
startMsg(msgTypeEndOfContinuousUpdates);
|
|
endMsg();
|
|
}
|
|
|
|
bool SMsgWriter::writeSetDesktopSize() {
|
|
if (!cp->supportsDesktopResize)
|
|
return false;
|
|
|
|
needSetDesktopSize = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool SMsgWriter::writeExtendedDesktopSize() {
|
|
if (!cp->supportsExtendedDesktopSize)
|
|
return false;
|
|
|
|
needExtendedDesktopSize = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool SMsgWriter::writeExtendedDesktopSize(rdr::U16 reason, rdr::U16 result,
|
|
int fb_width, int fb_height,
|
|
const ScreenSet& layout) {
|
|
ExtendedDesktopSizeMsg msg;
|
|
|
|
if (!cp->supportsExtendedDesktopSize)
|
|
return false;
|
|
|
|
msg.reason = reason;
|
|
msg.result = result;
|
|
msg.fb_width = fb_width;
|
|
msg.fb_height = fb_height;
|
|
msg.layout = layout;
|
|
|
|
extendedDesktopSizeMsgs.push_back(msg);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool SMsgWriter::writeSetDesktopName() {
|
|
if (!cp->supportsDesktopRename)
|
|
return false;
|
|
|
|
needSetDesktopName = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool SMsgWriter::writeSetCursor()
|
|
{
|
|
if (!cp->supportsLocalCursor)
|
|
return false;
|
|
|
|
needSetCursor = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool SMsgWriter::writeSetXCursor()
|
|
{
|
|
if (!cp->supportsLocalXCursor)
|
|
return false;
|
|
|
|
needSetXCursor = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool SMsgWriter::writeSetCursorWithAlpha()
|
|
{
|
|
if (!cp->supportsLocalCursorWithAlpha)
|
|
return false;
|
|
|
|
needSetCursorWithAlpha = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
void SMsgWriter::writeCursorPos()
|
|
{
|
|
if (!cp->supportsEncoding(pseudoEncodingVMwareCursorPosition))
|
|
throw Exception("Client does not support cursor position");
|
|
|
|
needCursorPos = true;
|
|
}
|
|
|
|
bool SMsgWriter::writeLEDState()
|
|
{
|
|
if (!cp->supportsLEDState)
|
|
return false;
|
|
if (cp->ledState() == ledUnknown)
|
|
return false;
|
|
|
|
needLEDState = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool SMsgWriter::writeQEMUKeyEvent()
|
|
{
|
|
if (!cp->supportsQEMUKeyEvent)
|
|
return false;
|
|
|
|
needQEMUKeyEvent = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool SMsgWriter::needFakeUpdate()
|
|
{
|
|
if (needSetDesktopName)
|
|
return true;
|
|
if (needSetCursor || needSetXCursor || needSetCursorWithAlpha)
|
|
return true;
|
|
if (needCursorPos)
|
|
return true;
|
|
if (needLEDState)
|
|
return true;
|
|
if (needQEMUKeyEvent)
|
|
return true;
|
|
if (needNoDataUpdate())
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
bool SMsgWriter::needNoDataUpdate()
|
|
{
|
|
if (needSetDesktopSize)
|
|
return true;
|
|
if (needExtendedDesktopSize || !extendedDesktopSizeMsgs.empty())
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
void SMsgWriter::writeNoDataUpdate()
|
|
{
|
|
int nRects;
|
|
|
|
nRects = 0;
|
|
|
|
if (needSetDesktopSize)
|
|
nRects++;
|
|
if (needExtendedDesktopSize)
|
|
nRects++;
|
|
if (!extendedDesktopSizeMsgs.empty())
|
|
nRects += extendedDesktopSizeMsgs.size();
|
|
|
|
writeFramebufferUpdateStart(nRects);
|
|
writeNoDataRects();
|
|
writeFramebufferUpdateEnd();
|
|
}
|
|
|
|
void SMsgWriter::writeFramebufferUpdateStart(int nRects)
|
|
{
|
|
startMsg(msgTypeFramebufferUpdate);
|
|
os->pad(1);
|
|
|
|
if (nRects != 0xFFFF) {
|
|
if (needSetDesktopName)
|
|
nRects++;
|
|
if (needSetCursor)
|
|
nRects++;
|
|
if (needSetXCursor)
|
|
nRects++;
|
|
if (needSetCursorWithAlpha)
|
|
nRects++;
|
|
if (needCursorPos)
|
|
nRects++;
|
|
if (needLEDState)
|
|
nRects++;
|
|
if (needQEMUKeyEvent)
|
|
nRects++;
|
|
}
|
|
|
|
os->writeU16(nRects);
|
|
|
|
nRectsInUpdate = 0;
|
|
if (nRects == 0xFFFF)
|
|
nRectsInHeader = 0;
|
|
else
|
|
nRectsInHeader = nRects;
|
|
|
|
writePseudoRects();
|
|
}
|
|
|
|
void SMsgWriter::writeFramebufferUpdateEnd()
|
|
{
|
|
if (nRectsInUpdate != nRectsInHeader && nRectsInHeader)
|
|
throw Exception("SMsgWriter::writeFramebufferUpdateEnd: "
|
|
"nRects out of sync");
|
|
|
|
if (nRectsInHeader == 0) {
|
|
// Send last rect. marker
|
|
os->writeS16(0);
|
|
os->writeS16(0);
|
|
os->writeU16(0);
|
|
os->writeU16(0);
|
|
os->writeU32(pseudoEncodingLastRect);
|
|
}
|
|
|
|
endMsg();
|
|
}
|
|
|
|
void SMsgWriter::writeCopyRect(const Rect& r, int srcX, int srcY)
|
|
{
|
|
startRect(r,encodingCopyRect);
|
|
os->writeU16(srcX);
|
|
os->writeU16(srcY);
|
|
endRect();
|
|
}
|
|
|
|
void SMsgWriter::startRect(const Rect& r, int encoding)
|
|
{
|
|
if (++nRectsInUpdate > nRectsInHeader && nRectsInHeader)
|
|
throw Exception("SMsgWriter::startRect: nRects out of sync");
|
|
|
|
os->writeS16(r.tl.x);
|
|
os->writeS16(r.tl.y);
|
|
os->writeU16(r.width());
|
|
os->writeU16(r.height());
|
|
os->writeU32(encoding);
|
|
}
|
|
|
|
void SMsgWriter::endRect()
|
|
{
|
|
os->flush();
|
|
}
|
|
|
|
void SMsgWriter::startMsg(int type)
|
|
{
|
|
os->writeU8(type);
|
|
}
|
|
|
|
void SMsgWriter::endMsg()
|
|
{
|
|
os->flush();
|
|
}
|
|
|
|
void SMsgWriter::writePseudoRects()
|
|
{
|
|
if (needSetCursor) {
|
|
const Cursor& cursor = cp->cursor();
|
|
|
|
rdr::U8Array data(cursor.width()*cursor.height() * (cp->pf().bpp/8));
|
|
rdr::U8Array mask(cursor.getMask());
|
|
|
|
const rdr::U8* in;
|
|
rdr::U8* out;
|
|
|
|
in = cursor.getBuffer();
|
|
out = data.buf;
|
|
for (int i = 0;i < cursor.width()*cursor.height();i++) {
|
|
cp->pf().bufferFromRGB(out, in, 1);
|
|
in += 4;
|
|
out += cp->pf().bpp/8;
|
|
}
|
|
|
|
writeSetCursorRect(cursor.width(), cursor.height(),
|
|
cursor.hotspot().x, cursor.hotspot().y,
|
|
data.buf, mask.buf);
|
|
needSetCursor = false;
|
|
}
|
|
|
|
if (needSetXCursor) {
|
|
const Cursor& cursor = cp->cursor();
|
|
rdr::U8Array bitmap(cursor.getBitmap());
|
|
rdr::U8Array mask(cursor.getMask());
|
|
|
|
writeSetXCursorRect(cursor.width(), cursor.height(),
|
|
cursor.hotspot().x, cursor.hotspot().y,
|
|
bitmap.buf, mask.buf);
|
|
needSetXCursor = false;
|
|
}
|
|
|
|
if (needSetCursorWithAlpha) {
|
|
const Cursor& cursor = cp->cursor();
|
|
|
|
writeSetCursorWithAlphaRect(cursor.width(), cursor.height(),
|
|
cursor.hotspot().x, cursor.hotspot().y,
|
|
cursor.getBuffer());
|
|
needSetCursorWithAlpha = false;
|
|
}
|
|
|
|
if (needCursorPos) {
|
|
const Point& cursorPos = cp->cursorPos();
|
|
|
|
if (cp->supportsEncoding(pseudoEncodingVMwareCursorPosition)) {
|
|
writeSetVMwareCursorPositionRect(cursorPos.x, cursorPos.y);
|
|
} else {
|
|
throw Exception("Client does not support cursor position");
|
|
}
|
|
|
|
needCursorPos = false;
|
|
}
|
|
|
|
if (needSetDesktopName) {
|
|
writeSetDesktopNameRect(cp->name());
|
|
needSetDesktopName = false;
|
|
}
|
|
|
|
if (needLEDState) {
|
|
writeLEDStateRect(cp->ledState());
|
|
needLEDState = false;
|
|
}
|
|
|
|
if (needQEMUKeyEvent) {
|
|
writeQEMUKeyEventRect();
|
|
needQEMUKeyEvent = false;
|
|
}
|
|
}
|
|
|
|
void SMsgWriter::writeNoDataRects()
|
|
{
|
|
// Start with specific ExtendedDesktopSize messages
|
|
if (!extendedDesktopSizeMsgs.empty()) {
|
|
std::list<ExtendedDesktopSizeMsg>::const_iterator ri;
|
|
|
|
for (ri = extendedDesktopSizeMsgs.begin();ri != extendedDesktopSizeMsgs.end();++ri) {
|
|
writeExtendedDesktopSizeRect(ri->reason, ri->result,
|
|
ri->fb_width, ri->fb_height, ri->layout);
|
|
}
|
|
|
|
extendedDesktopSizeMsgs.clear();
|
|
}
|
|
|
|
// Send this before SetDesktopSize to make life easier on the clients
|
|
if (needExtendedDesktopSize) {
|
|
writeExtendedDesktopSizeRect(0, 0, cp->width, cp->height,
|
|
cp->screenLayout);
|
|
needExtendedDesktopSize = false;
|
|
}
|
|
|
|
// Some clients assume this is the last rectangle so don't send anything
|
|
// more after this
|
|
if (needSetDesktopSize) {
|
|
writeSetDesktopSizeRect(cp->width, cp->height);
|
|
needSetDesktopSize = false;
|
|
}
|
|
}
|
|
|
|
void SMsgWriter::writeSetDesktopSizeRect(int width, int height)
|
|
{
|
|
if (!cp->supportsDesktopResize)
|
|
throw Exception("Client does not support desktop resize");
|
|
if (++nRectsInUpdate > nRectsInHeader && nRectsInHeader)
|
|
throw Exception("SMsgWriter::writeSetDesktopSizeRect: nRects out of sync");
|
|
|
|
os->writeS16(0);
|
|
os->writeS16(0);
|
|
os->writeU16(width);
|
|
os->writeU16(height);
|
|
os->writeU32(pseudoEncodingDesktopSize);
|
|
}
|
|
|
|
void SMsgWriter::writeExtendedDesktopSizeRect(rdr::U16 reason,
|
|
rdr::U16 result,
|
|
int fb_width,
|
|
int fb_height,
|
|
const ScreenSet& layout)
|
|
{
|
|
ScreenSet::const_iterator si;
|
|
|
|
if (!cp->supportsExtendedDesktopSize)
|
|
throw Exception("Client does not support extended desktop resize");
|
|
if (++nRectsInUpdate > nRectsInHeader && nRectsInHeader)
|
|
throw Exception("SMsgWriter::writeExtendedDesktopSizeRect: nRects out of sync");
|
|
|
|
os->writeU16(reason);
|
|
os->writeU16(result);
|
|
os->writeU16(fb_width);
|
|
os->writeU16(fb_height);
|
|
os->writeU32(pseudoEncodingExtendedDesktopSize);
|
|
|
|
os->writeU8(layout.num_screens());
|
|
os->pad(3);
|
|
|
|
for (si = layout.begin();si != layout.end();++si) {
|
|
os->writeU32(si->id);
|
|
os->writeU16(si->dimensions.tl.x);
|
|
os->writeU16(si->dimensions.tl.y);
|
|
os->writeU16(si->dimensions.width());
|
|
os->writeU16(si->dimensions.height());
|
|
os->writeU32(si->flags);
|
|
}
|
|
}
|
|
|
|
void SMsgWriter::writeSetDesktopNameRect(const char *name)
|
|
{
|
|
if (!cp->supportsDesktopRename)
|
|
throw Exception("Client does not support desktop rename");
|
|
if (++nRectsInUpdate > nRectsInHeader && nRectsInHeader)
|
|
throw Exception("SMsgWriter::writeSetDesktopNameRect: nRects out of sync");
|
|
|
|
os->writeS16(0);
|
|
os->writeS16(0);
|
|
os->writeU16(0);
|
|
os->writeU16(0);
|
|
os->writeU32(pseudoEncodingDesktopName);
|
|
os->writeString(name);
|
|
}
|
|
|
|
void SMsgWriter::writeSetCursorRect(int width, int height,
|
|
int hotspotX, int hotspotY,
|
|
const void* data, const void* mask)
|
|
{
|
|
if (!cp->supportsLocalCursor)
|
|
throw Exception("Client does not support local cursors");
|
|
if (++nRectsInUpdate > nRectsInHeader && nRectsInHeader)
|
|
throw Exception("SMsgWriter::writeSetCursorRect: nRects out of sync");
|
|
|
|
os->writeS16(hotspotX);
|
|
os->writeS16(hotspotY);
|
|
os->writeU16(width);
|
|
os->writeU16(height);
|
|
os->writeU32(pseudoEncodingCursor);
|
|
os->writeBytes(data, width * height * (cp->pf().bpp/8));
|
|
os->writeBytes(mask, (width+7)/8 * height);
|
|
}
|
|
|
|
void SMsgWriter::writeSetXCursorRect(int width, int height,
|
|
int hotspotX, int hotspotY,
|
|
const void* data, const void* mask)
|
|
{
|
|
if (!cp->supportsLocalXCursor)
|
|
throw Exception("Client does not support local cursors");
|
|
if (++nRectsInUpdate > nRectsInHeader && nRectsInHeader)
|
|
throw Exception("SMsgWriter::writeSetXCursorRect: nRects out of sync");
|
|
|
|
os->writeS16(hotspotX);
|
|
os->writeS16(hotspotY);
|
|
os->writeU16(width);
|
|
os->writeU16(height);
|
|
os->writeU32(pseudoEncodingXCursor);
|
|
if (width * height > 0) {
|
|
os->writeU8(255);
|
|
os->writeU8(255);
|
|
os->writeU8(255);
|
|
os->writeU8(0);
|
|
os->writeU8(0);
|
|
os->writeU8(0);
|
|
os->writeBytes(data, (width+7)/8 * height);
|
|
os->writeBytes(mask, (width+7)/8 * height);
|
|
}
|
|
}
|
|
|
|
void SMsgWriter::writeSetCursorWithAlphaRect(int width, int height,
|
|
int hotspotX, int hotspotY,
|
|
const rdr::U8* data)
|
|
{
|
|
if (!cp->supportsLocalCursorWithAlpha)
|
|
throw Exception("Client does not support local cursors");
|
|
if (++nRectsInUpdate > nRectsInHeader && nRectsInHeader)
|
|
throw Exception("SMsgWriter::writeSetCursorWithAlphaRect: nRects out of sync");
|
|
|
|
os->writeS16(hotspotX);
|
|
os->writeS16(hotspotY);
|
|
os->writeU16(width);
|
|
os->writeU16(height);
|
|
os->writeU32(pseudoEncodingCursorWithAlpha);
|
|
|
|
// FIXME: Use an encoder with compression?
|
|
os->writeU32(encodingRaw);
|
|
|
|
// Alpha needs to be pre-multiplied
|
|
for (int i = 0;i < width*height;i++) {
|
|
os->writeU8((unsigned)data[0] * data[3] / 255);
|
|
os->writeU8((unsigned)data[1] * data[3] / 255);
|
|
os->writeU8((unsigned)data[2] * data[3] / 255);
|
|
os->writeU8(data[3]);
|
|
data += 4;
|
|
}
|
|
}
|
|
|
|
void SMsgWriter::writeSetVMwareCursorPositionRect(int hotspotX, int hotspotY)
|
|
{
|
|
if (!cp->supportsEncoding(pseudoEncodingVMwareCursorPosition))
|
|
throw Exception("Client does not support cursor position");
|
|
if (++nRectsInUpdate > nRectsInHeader && nRectsInHeader)
|
|
throw Exception("SMsgWriter::writeSetVMwareCursorRect: nRects out of sync");
|
|
|
|
os->writeS16(hotspotX);
|
|
os->writeS16(hotspotY);
|
|
os->writeU16(0);
|
|
os->writeU16(0);
|
|
os->writeU32(pseudoEncodingVMwareCursorPosition);
|
|
}
|
|
|
|
void SMsgWriter::writeLEDStateRect(rdr::U8 state)
|
|
{
|
|
if (!cp->supportsLEDState)
|
|
throw Exception("Client does not support LED state updates");
|
|
if (cp->ledState() == ledUnknown)
|
|
throw Exception("Server does not support LED state updates");
|
|
if (++nRectsInUpdate > nRectsInHeader && nRectsInHeader)
|
|
throw Exception("SMsgWriter::writeLEDStateRect: nRects out of sync");
|
|
|
|
os->writeS16(0);
|
|
os->writeS16(0);
|
|
os->writeU16(0);
|
|
os->writeU16(0);
|
|
os->writeU32(pseudoEncodingLEDState);
|
|
os->writeU8(state);
|
|
}
|
|
|
|
void SMsgWriter::writeQEMUKeyEventRect()
|
|
{
|
|
if (!cp->supportsQEMUKeyEvent)
|
|
throw Exception("Client does not support QEMU extended key events");
|
|
if (++nRectsInUpdate > nRectsInHeader && nRectsInHeader)
|
|
throw Exception("SMsgWriter::writeQEMUKeyEventRect: nRects out of sync");
|
|
|
|
os->writeS16(0);
|
|
os->writeS16(0);
|
|
os->writeU16(0);
|
|
os->writeU16(0);
|
|
os->writeU32(pseudoEncodingQEMUKeyEvent);
|
|
}
|