mirror of
https://github.com/Ylianst/MeshCentral.git
synced 2024-12-18 11:21:38 +03:00
894 lines
40 KiB
JavaScript
894 lines
40 KiB
JavaScript
/**
|
||
* @description Remote Desktop
|
||
* @author Ylian Saint-Hilaire
|
||
* @version v0.0.2g
|
||
*/
|
||
|
||
// Construct a MeshServer object
|
||
var CreateAmtRemoteDesktop = function (divid, scrolldiv) {
|
||
var obj = {};
|
||
obj.canvasid = divid;
|
||
obj.CanvasId = Q(divid);
|
||
obj.scrolldiv = scrolldiv;
|
||
obj.canvas = Q(divid).getContext('2d');
|
||
obj.protocol = 2; // KVM
|
||
obj.state = 0;
|
||
obj.acc = null;
|
||
obj.ScreenWidth = 960;
|
||
obj.ScreenHeight = 700;
|
||
obj.width = 0;
|
||
obj.height = 0;
|
||
obj.rwidth = 0;
|
||
obj.rheight = 0;
|
||
obj.bpp = 2; // Bytes per pixel (1 or 2 supported)
|
||
obj.useZRLE = true;
|
||
obj.showmouse = true;
|
||
obj.buttonmask = 0;
|
||
obj.localKeyMap = true;
|
||
obj.spare = null;
|
||
obj.sparew = 0;
|
||
obj.spareh = 0;
|
||
obj.sparew2 = 0;
|
||
obj.spareh2 = 0;
|
||
obj.sparecache = {};
|
||
obj.ZRLEfirst = 1;
|
||
obj.onScreenSizeChange = null;
|
||
obj.frameRateDelay = 0;
|
||
// ###BEGIN###{DesktopRotation}
|
||
obj.noMouseRotate = false;
|
||
obj.rotation = 0;
|
||
// ###END###{DesktopRotation}
|
||
// ###BEGIN###{DesktopInband}
|
||
obj.kvmDataSupported = false;
|
||
obj.onKvmData = null;
|
||
obj.onKvmDataPending = [];
|
||
obj.onKvmDataAck = -1;
|
||
obj.holding = false;
|
||
obj.lastKeepAlive = Date.now();
|
||
// ###END###{DesktopInband}
|
||
|
||
obj.mNagleTimer = null; // Mouse motion slowdown timer
|
||
obj.mx = 0; // Last mouse x position
|
||
obj.my = 0; // Last mouse y position
|
||
// ###BEGIN###{DesktopFocus}
|
||
obj.ox = -1; // Old mouse x position
|
||
obj.oy = -1; // Old mouse y position
|
||
obj.focusmode = 0;
|
||
// ###END###{DesktopFocus}
|
||
// ###BEGIN###{Inflate}
|
||
obj.inflate = ZLIB.inflateInit(-15);
|
||
// ###END###{Inflate}
|
||
|
||
obj.xxStateChange = function (newstate) {
|
||
if (newstate == 0) {
|
||
obj.canvas.fillStyle = '#000000';
|
||
obj.canvas.fillRect(0, 0, obj.width, obj.height);
|
||
obj.canvas.canvas.width = obj.rwidth = obj.width = 640;
|
||
obj.canvas.canvas.height = obj.rheight = obj.height = 400;
|
||
QS(obj.canvasid).cursor = 'default';
|
||
} else {
|
||
QS(obj.canvasid).cursor = obj.showmouse ? 'default' : 'none';
|
||
}
|
||
}
|
||
|
||
function arrToStr(arr) { return String.fromCharCode.apply(null, arr); }
|
||
function strToArr(str) { var arr = new Uint8Array(str.length); for (var i = 0, j = str.length; i < j; ++i) { arr[i] = str.charCodeAt(i); } return arr }
|
||
|
||
obj.ProcessBinaryData = function (data) {
|
||
// Append to accumulator
|
||
if (obj.acc == null) {
|
||
obj.acc = new Uint8Array(data);
|
||
} else {
|
||
var tmp = new Uint8Array(obj.acc.byteLength + data.byteLength);
|
||
tmp.set(obj.acc, 0);
|
||
tmp.set(new Uint8Array(data), obj.acc.byteLength);
|
||
obj.acc = tmp;
|
||
}
|
||
|
||
while ((obj.acc != null) && (obj.acc.byteLength > 0)) {
|
||
//console.log('KAcc', obj.state, obj.acc);
|
||
var cmdsize = 0, accview = new DataView(obj.acc.buffer);
|
||
if ((obj.state == 0) && (obj.acc.byteLength >= 12)) {
|
||
// Getting handshake & version
|
||
cmdsize = 12;
|
||
//if (obj.acc.substring(0, 4) != "RFB ") { return obj.Stop(); }
|
||
//var version = parseFloat(obj.acc.substring(4, 11));
|
||
//console.log("KVersion: " + version);
|
||
obj.state = 1;
|
||
obj.send('RFB 003.008\n');
|
||
}
|
||
else if ((obj.state == 1) && (obj.acc.byteLength >= 1)) {
|
||
// Getting security options
|
||
cmdsize = obj.acc[0] + 1;
|
||
obj.send(String.fromCharCode(1)); // Send the "None" security type. Since we already authenticated using redirection digest auth, we don't need to do this again.
|
||
obj.state = 2;
|
||
}
|
||
else if ((obj.state == 2) && (obj.acc.byteLength >= 4)) {
|
||
// Getting security response
|
||
cmdsize = 4;
|
||
if (accview.getUint32(0) != 0) { return obj.Stop(); }
|
||
obj.send(String.fromCharCode(1)); // Send share desktop flag
|
||
obj.state = 3;
|
||
}
|
||
else if ((obj.state == 3) && (obj.acc.byteLength >= 24)) {
|
||
// Getting server init
|
||
// ###BEGIN###{DesktopRotation}
|
||
obj.rotation = 0; // We don't currently support screen init while rotated.
|
||
// ###END###{DesktopRotation}
|
||
var namelen = accview.getUint32(20);
|
||
if (obj.acc.byteLength < 24 + namelen) return;
|
||
cmdsize = 24 + namelen;
|
||
obj.canvas.canvas.width = obj.rwidth = obj.width = obj.ScreenWidth = accview.getUint16(0);
|
||
obj.canvas.canvas.height = obj.rheight = obj.height = obj.ScreenHeight = accview.getUint16(2);
|
||
|
||
// These are all values we don't really need, we are going to only run in RGB565 or RGB332 and not use the flexibility provided by these settings.
|
||
// Makes the javascript code smaller and maybe a bit faster.
|
||
/*
|
||
obj.xbpp = obj.acc[4];
|
||
obj.depth = obj.acc[5];
|
||
obj.bigend = obj.acc[6];
|
||
obj.truecolor = obj.acc[7];
|
||
obj.rmax = ReadShort(obj.acc, 8);
|
||
obj.gmax = ReadShort(obj.acc, 10);
|
||
obj.bmax = ReadShort(obj.acc, 12);
|
||
obj.rsh = obj.acc[14];
|
||
obj.gsh = obj.acc[15];
|
||
obj.bsh = obj.acc[16];
|
||
var name = obj.acc.substring(24, 24 + namelen);
|
||
console.log("name: " + name);
|
||
console.log("width: " + obj.width + ", height: " + obj.height);
|
||
console.log("bits-per-pixel: " + obj.xbpp);
|
||
console.log("depth: " + obj.depth);
|
||
console.log("big-endian-flag: " + obj.bigend);
|
||
console.log("true-colour-flag: " + obj.truecolor);
|
||
console.log("rgb max: " + obj.rmax + "," + obj.gmax + "," + obj.bmax);
|
||
console.log("rgb shift: " + obj.rsh + "," + obj.gsh + "," + obj.bsh);
|
||
*/
|
||
|
||
// SetEncodings, with AMT we can't omit RAW, must be specified.
|
||
// Intel AMT supports encodings: RAW (0), ZRLE (16), Desktop Size (0xFFFFFF21, -223)
|
||
|
||
var supportedEncodings = '';
|
||
if (obj.useZRLE) supportedEncodings += IntToStr(16);
|
||
supportedEncodings += IntToStr(0);
|
||
// ###BEGIN###{DesktopInband}
|
||
supportedEncodings += IntToStr(1092);
|
||
// ###END###{DesktopInband}
|
||
|
||
obj.send(String.fromCharCode(2, 0) + ShortToStr((supportedEncodings.length / 4) + 1) + supportedEncodings + IntToStr(-223)); // Supported Encodings + Desktop Size
|
||
|
||
// Set the pixel encoding to something much smaller
|
||
// obj.send(String.fromCharCode(0, 0, 0, 0, 16, 16, 0, 1) + ShortToStr(31) + ShortToStr(63) + ShortToStr(31) + String.fromCharCode(11, 5, 0, 0, 0, 0)); // Setup 16 bit color RGB565 (This is the default, so we don't need to set it)
|
||
if (obj.bpp == 1) obj.send(String.fromCharCode(0, 0, 0, 0, 8, 8, 0, 1) + ShortToStr(7) + ShortToStr(7) + ShortToStr(3) + String.fromCharCode(5, 2, 0, 0, 0, 0)); // Setup 8 bit color RGB332
|
||
|
||
obj.state = 4;
|
||
if (obj.parent) { obj.parent.xxStateChange(3); }
|
||
_SendRefresh();
|
||
//obj.timer = setInterval(obj.xxOnTimer, 50);
|
||
|
||
// ###BEGIN###{DesktopFocus}
|
||
obj.ox = -1; // Old mouse x position
|
||
// ###END###{DesktopFocus}
|
||
|
||
if (obj.onScreenSizeChange != null) { obj.onScreenSizeChange(obj, obj.ScreenWidth, obj.ScreenHeight); }
|
||
}
|
||
else if (obj.state == 4) {
|
||
switch (obj.acc[0]) {
|
||
case 0: // FramebufferUpdate
|
||
if (obj.acc.byteLength < 4) return;
|
||
obj.state = 100 + accview.getUint16(2); // Read the number of tiles that are going to be sent, add 100 and use that as our protocol state.
|
||
cmdsize = 4;
|
||
break;
|
||
case 2: // This is the bell, do nothing.
|
||
cmdsize = 1;
|
||
break;
|
||
case 3: // This is ServerCutText
|
||
if (obj.acc.byteLength < 8) return;
|
||
var len = accview.getUint32(4) + 8;
|
||
if (obj.acc.byteLength < len) return;
|
||
cmdsize = handleServerCutText(obj.acc, accview);
|
||
break;
|
||
}
|
||
}
|
||
else if ((obj.state > 100) && (obj.acc.byteLength >= 12)) {
|
||
var x = accview.getUint16(0),
|
||
y = accview.getUint16(2),
|
||
width = accview.getUint16(4),
|
||
height = accview.getUint16(6),
|
||
s = width * height,
|
||
encoding = accview.getUint32(8);
|
||
|
||
if (encoding < 17) {
|
||
if ((width < 1) || (width > 64) || (height < 1) || (height > 64)) { console.log("Invalid tile size (" + width + "," + height + "), disconnecting."); return obj.Stop(); }
|
||
|
||
// Set the spare bitmap to the right size if it's not already. This allows us to recycle the spare most if not all the time.
|
||
if ((obj.sparew != width) || (obj.spareh != height)) {
|
||
obj.sparew = obj.sparew2 = width;
|
||
obj.spareh = obj.spareh2 = height;
|
||
// ###BEGIN###{DesktopRotation}
|
||
if (obj.rotation == 1 || obj.rotation == 3) { obj.sparew2 = height, obj.spareh2 = width; }
|
||
// ###END###{DesktopRotation}
|
||
var xspacecachename = obj.sparew2 + 'x' + obj.spareh2;
|
||
obj.spare = obj.sparecache[xspacecachename];
|
||
if (!obj.spare) {
|
||
obj.sparecache[xspacecachename] = obj.spare = obj.canvas.createImageData(obj.sparew2, obj.spareh2);
|
||
var j = (obj.sparew2 * obj.spareh2) << 2;
|
||
for (var i = 3; i < j; i += 4) { obj.spare.data[i] = 0xFF; } // Set alpha channel to opaque.
|
||
}
|
||
}
|
||
}
|
||
|
||
if (encoding == 0xFFFFFF21) {
|
||
// Desktop Size (0xFFFFFF21, -223)
|
||
obj.canvas.canvas.width = obj.rwidth = obj.width = width;
|
||
obj.canvas.canvas.height = obj.rheight = obj.height = height;
|
||
obj.send(String.fromCharCode(3, 0, 0, 0, 0, 0) + ShortToStr(obj.width) + ShortToStr(obj.height)); // FramebufferUpdateRequest
|
||
cmdsize = 12;
|
||
if (obj.onScreenSizeChange != null) { obj.onScreenSizeChange(obj, obj.ScreenWidth, obj.ScreenHeight); }
|
||
//console.log("New desktop width: " + obj.width + ", height: " + obj.height);
|
||
} else if (encoding == 0) {
|
||
// RAW encoding
|
||
var ptr = 12, cs = 12 + (s * obj.bpp);
|
||
if (obj.acc.byteLength < cs) return; // Check we have all the data needed and we can only draw 64x64 tiles.
|
||
cmdsize = cs;
|
||
|
||
// CRITICAL LOOP, optimize this as much as possible
|
||
if (obj.bpp == 2) {
|
||
for (var i = 0; i < s; i++) { _setPixel16(accview.getUint16(ptr, true), i); ptr += 2; }
|
||
} else {
|
||
for (var i = 0; i < s; i++) { _setPixel8(obj.acc[ptr++], i); }
|
||
}
|
||
_putImage(obj.spare, x, y);
|
||
} else if (encoding == 16) {
|
||
// ZRLE encoding
|
||
if (obj.acc.byteLength < 16) return;
|
||
var datalen = accview.getUint32(12);
|
||
if (obj.acc.byteLength < (16 + datalen)) return;
|
||
|
||
// Process the ZLib header if this is the first block
|
||
var ptr = 16, delta = 5, dx = 0;
|
||
|
||
if ((datalen > 5) && (obj.acc[ptr] == 0) && (accview.getUint16(ptr + 1, true) == (datalen - delta))) {
|
||
// This is an uncompressed ZLib data block
|
||
_decodeLRE(obj.acc, ptr + 5, x, y, width, height, s, datalen);
|
||
}
|
||
// ###BEGIN###{Inflate}
|
||
else {
|
||
// This is compressed ZLib data, decompress and process it. (TODO: This need to be optimized, remove str/arr conversions)
|
||
var str = obj.inflate.inflate(arrToStr(new Uint8Array(obj.acc.buffer.slice(ptr, ptr + datalen - dx))));
|
||
if (str.length > 0) { _decodeLRE(strToArr(str), 0, x, y, width, height, s, str.length); } else { console.log("Invalid deflate data"); }
|
||
}
|
||
// ###END###{Inflate}
|
||
|
||
cmdsize = 16 + datalen;
|
||
} else { return obj.Stop(); }
|
||
if (--obj.state == 100) {
|
||
obj.state = 4;
|
||
if (obj.frameRateDelay == 0) {
|
||
_SendRefresh(); // Ask for new frame
|
||
} else {
|
||
setTimeout(_SendRefresh, obj.frameRateDelay); // Hold x miliseconds before asking for a new frame
|
||
}
|
||
}
|
||
}
|
||
|
||
//console.log('cmdsize', cmdsize);
|
||
if (cmdsize == 0) return;
|
||
if (cmdsize != obj.acc.byteLength) { obj.acc = new Uint8Array(obj.acc.buffer.slice(cmdsize)); } else { obj.acc = null; }
|
||
}
|
||
}
|
||
|
||
function _decodeLRE(data, ptr, x, y, width, height, s, datalen) {
|
||
var subencoding = data[ptr++], index, v, runlengthdecode, palette = {}, rlecount = 0, runlength = 0, i;
|
||
if (subencoding == 0) {
|
||
// RAW encoding
|
||
if (obj.bpp == 2) {
|
||
for (i = 0; i < s; i++) { _setPixel16(data[ptr++] + (data[ptr++] << 8), i); }
|
||
} else {
|
||
for (i = 0; i < s; i++) { _setPixel8(data[ptr++], i); }
|
||
}
|
||
_putImage(obj.spare, x, y);
|
||
}
|
||
else if (subencoding == 1) {
|
||
// Solid color tile
|
||
v = data[ptr++] + ((obj.bpp == 2) ? (data[ptr++] << 8) : 0);
|
||
obj.canvas.fillStyle = 'rgb(' + ((obj.bpp == 1) ? ((v & 224) + ',' + ((v & 28) << 3) + ',' + _fixColor((v & 3) << 6)) : (((v >> 8) & 248) + ',' + ((v >> 3) & 252) + ',' + ((v & 31) << 3))) + ')';
|
||
|
||
// ###BEGIN###{DesktopRotation}
|
||
var xx = _rotX(x, y);
|
||
y = _rotY(x, y);
|
||
x = xx;
|
||
// ###END###{DesktopRotation}
|
||
|
||
obj.canvas.fillRect(x, y, width, height);
|
||
}
|
||
else if (subencoding > 1 && subencoding < 17) { // Packed palette encoded tile
|
||
// Read the palette
|
||
var br = 4, bm = 15; // br is BitRead and bm is BitMask. By adjusting these two we can support all the variations in this encoding.
|
||
if (obj.bpp == 2) {
|
||
for (i = 0; i < subencoding; i++) { palette[i] = data[ptr++] + (data[ptr++] << 8); }
|
||
if (subencoding == 2) { br = 1; bm = 1; } else if (subencoding <= 4) { br = 2; bm = 3; } // Compute bits to read & bit mark
|
||
while (rlecount < s && ptr < data.byteLength) { v = data[ptr++]; for (i = (8 - br); i >= 0; i -= br) { _setPixel16(palette[(v >> i) & bm], rlecount++); } } // Display all the bits
|
||
} else {
|
||
for (i = 0; i < subencoding; i++) { palette[i] = data[ptr++]; }
|
||
if (subencoding == 2) { br = 1; bm = 1; } else if (subencoding <= 4) { br = 2; bm = 3; } // Compute bits to read & bit mark
|
||
while (rlecount < s && ptr < data.byteLength) { v = data[ptr++]; for (i = (8 - br); i >= 0; i -= br) { _setPixel8(palette[(v >> i) & bm], rlecount++); } } // Display all the bits
|
||
}
|
||
_putImage(obj.spare, x, y);
|
||
}
|
||
else if (subencoding == 128) { // RLE encoded tile
|
||
if (obj.bpp == 2) {
|
||
while (rlecount < s && ptr < data.byteLength) {
|
||
// Get the run color
|
||
v = data[ptr++] + (data[ptr++] << 8);
|
||
|
||
// Decode the run length. This is the fastest and most compact way I found to do this.
|
||
runlength = 1; do { runlength += (runlengthdecode = data[ptr++]); } while (runlengthdecode == 255);
|
||
|
||
// Draw a run
|
||
if (obj.rotation == 0) {
|
||
_setPixel16run(v, rlecount, runlength); rlecount += runlength;
|
||
} else {
|
||
while (--runlength >= 0) { _setPixel16(v, rlecount++); }
|
||
}
|
||
}
|
||
} else {
|
||
while (rlecount < s && ptr < data.byteLength) {
|
||
// Get the run color
|
||
v = data[ptr++];
|
||
|
||
// Decode the run length. This is the fastest and most compact way I found to do this.
|
||
runlength = 1; do { runlength += (runlengthdecode = data[ptr++]); } while (runlengthdecode == 255);
|
||
|
||
// Draw a run
|
||
if (obj.rotation == 0) {
|
||
_setPixel8run(v, rlecount, runlength); rlecount += runlength;
|
||
} else {
|
||
while (--runlength >= 0) { _setPixel8(v, rlecount++); }
|
||
}
|
||
}
|
||
}
|
||
_putImage(obj.spare, x, y);
|
||
}
|
||
else if (subencoding > 129) { // Palette RLE encoded tile
|
||
// Read the palette
|
||
if (obj.bpp == 2) {
|
||
for (i = 0; i < (subencoding - 128); i++) { palette[i] = data[ptr++] + (data[ptr++] << 8); }
|
||
} else {
|
||
for (i = 0; i < (subencoding - 128); i++) { palette[i] = data[ptr++]; }
|
||
}
|
||
|
||
// Decode RLE on palette
|
||
while (rlecount < s && ptr < data.byteLength) {
|
||
// Setup the run, get the color index and get the color from the palette.
|
||
runlength = 1; index = data[ptr++]; v = palette[index % 128];
|
||
|
||
// If the index starts with high order bit 1, this is a run and decode the run length.
|
||
if (index > 127) { do { runlength += (runlengthdecode = data[ptr++]); } while (runlengthdecode == 255); }
|
||
|
||
// Draw a run
|
||
if (obj.rotation == 0) {
|
||
if (obj.bpp == 2) {
|
||
_setPixel16run(v, rlecount, runlength); rlecount += runlength;
|
||
} else {
|
||
_setPixel8run(v, rlecount, runlength); rlecount += runlength;
|
||
}
|
||
} else {
|
||
if (obj.bpp == 2) {
|
||
while (--runlength >= 0) { _setPixel16(v, rlecount++); }
|
||
} else {
|
||
while (--runlength >= 0) { _setPixel8(v, rlecount++); }
|
||
}
|
||
}
|
||
}
|
||
_putImage(obj.spare, x, y);
|
||
}
|
||
}
|
||
|
||
// ###BEGIN###{DesktopInband}
|
||
obj.hold = function (holding) {
|
||
if (obj.holding == holding) return;
|
||
obj.holding = holding;
|
||
obj.canvas.fillStyle = '#000000';
|
||
obj.canvas.fillRect(0, 0, obj.width, obj.height); // Paint black
|
||
if (obj.holding == false) {
|
||
// Go back to normal operations
|
||
// Set canvas size and ask for full screen refresh
|
||
if ((obj.canvas.canvas.width != obj.width) || (obj.canvas.canvas.height != obj.height)) {
|
||
obj.canvas.canvas.width = obj.width; obj.canvas.canvas.height = obj.height;
|
||
if (obj.onScreenSizeChange != null) { obj.onScreenSizeChange(obj, obj.ScreenWidth, obj.ScreenHeight); } // ???
|
||
}
|
||
obj.Send(String.fromCharCode(3, 0, 0, 0, 0, 0) + ShortToStr(obj.width) + ShortToStr(obj.height)); // FramebufferUpdateRequest
|
||
} else {
|
||
obj.UnGrabMouseInput();
|
||
obj.UnGrabKeyInput();
|
||
}
|
||
}
|
||
// ###END###{DesktopInband}
|
||
|
||
function _putImage(i, x, y) {
|
||
// ###BEGIN###{DesktopInband}
|
||
if (obj.holding == true) return;
|
||
// ###END###{DesktopInband}
|
||
// ###BEGIN###{DesktopRotation}
|
||
var xx = _arotX(x, y);
|
||
y = _arotY(x, y);
|
||
x = xx;
|
||
// ###END###{DesktopRotation}
|
||
obj.canvas.putImageData(i, x, y);
|
||
}
|
||
|
||
// Set 8bit color RGB332
|
||
function _setPixel8(v, p) {
|
||
var pp = p << 2;
|
||
|
||
// ###BEGIN###{DesktopRotation}
|
||
if (obj.rotation > 0) {
|
||
if (obj.rotation == 1) { var x = p % obj.sparew, y = Math.floor(p / obj.sparew); p = (x * obj.sparew2) + (obj.sparew2 - 1 - y); pp = p << 2; }
|
||
else if (obj.rotation == 2) { pp = (obj.sparew * obj.spareh * 4) - 4 - pp; }
|
||
else if (obj.rotation == 3) { var x = p % obj.sparew, y = Math.floor(p / obj.sparew); p = ((obj.sparew2 - 1 - x) * obj.sparew2) + (y); pp = p << 2; }
|
||
}
|
||
// ###END###{DesktopRotation}
|
||
|
||
obj.spare.data[pp] = v & 224;
|
||
obj.spare.data[pp + 1] = (v & 28) << 3;
|
||
obj.spare.data[pp + 2] = _fixColor((v & 3) << 6);
|
||
}
|
||
|
||
// Set 16bit color RGB565
|
||
function _setPixel16(v, p) {
|
||
var pp = p << 2;
|
||
|
||
// ###BEGIN###{DesktopRotation}
|
||
if (obj.rotation > 0) {
|
||
if (obj.rotation == 1) { var x = p % obj.sparew, y = Math.floor(p / obj.sparew); p = (x * obj.sparew2) + (obj.sparew2 - 1 - y); pp = p << 2; }
|
||
else if (obj.rotation == 2) { pp = (obj.sparew * obj.spareh * 4) - 4 - pp; }
|
||
else if (obj.rotation == 3) { var x = p % obj.sparew, y = Math.floor(p / obj.sparew); p = ((obj.sparew2 - 1 - x) * obj.sparew2) + (y); pp = p << 2; }
|
||
}
|
||
// ###END###{DesktopRotation}
|
||
|
||
obj.spare.data[pp] = (v >> 8) & 248;
|
||
obj.spare.data[pp + 1] = (v >> 3) & 252;
|
||
obj.spare.data[pp + 2] = (v & 31) << 3;
|
||
}
|
||
|
||
// Set a run of 8bit color RGB332
|
||
function _setPixel8run(v, p, run) {
|
||
var pp = (p << 2), r = (v & 224), g = ((v & 28) << 3), b = (_fixColor((v & 3) << 6));
|
||
while (--run >= 0) { obj.spare.data[pp] = r; obj.spare.data[pp + 1] = g; obj.spare.data[pp + 2] = b; pp += 4; }
|
||
}
|
||
|
||
// Set a run of 16bit color RGB565
|
||
function _setPixel16run(v, p, run) {
|
||
var pp = (p << 2), r = ((v >> 8) & 248), g = ((v >> 3) & 252), b = ((v & 31) << 3);
|
||
while (--run >= 0) { obj.spare.data[pp] = r; obj.spare.data[pp + 1] = g; obj.spare.data[pp + 2] = b; pp += 4; }
|
||
}
|
||
|
||
// ###BEGIN###{DesktopRotation}
|
||
function _arotX(x, y) {
|
||
if (obj.rotation == 0) return x;
|
||
if (obj.rotation == 1) return obj.canvas.canvas.width - obj.sparew2 - y;
|
||
if (obj.rotation == 2) return obj.canvas.canvas.width - obj.sparew2 - x;
|
||
if (obj.rotation == 3) return y;
|
||
return 0;
|
||
}
|
||
|
||
function _arotY(x, y) {
|
||
if (obj.rotation == 0) return y;
|
||
if (obj.rotation == 1) return x;
|
||
if (obj.rotation == 2) return obj.canvas.canvas.height - obj.spareh2 - y;
|
||
if (obj.rotation == 3) return obj.canvas.canvas.height - obj.spareh - x;
|
||
return 0;
|
||
}
|
||
|
||
function _crotX(x, y) {
|
||
if (obj.rotation == 0) return x;
|
||
if (obj.rotation == 1) return y;
|
||
if (obj.rotation == 2) return obj.canvas.canvas.width - x;
|
||
if (obj.rotation == 3) return obj.canvas.canvas.height - y;
|
||
return 0;
|
||
}
|
||
|
||
function _crotY(x, y) {
|
||
if (obj.rotation == 0) return y;
|
||
if (obj.rotation == 1) return obj.canvas.canvas.width - x;
|
||
if (obj.rotation == 2) return obj.canvas.canvas.height - y;
|
||
if (obj.rotation == 3) return x;
|
||
return 0;
|
||
}
|
||
|
||
function _rotX(x, y) {
|
||
if (obj.rotation == 0) return x;
|
||
if (obj.rotation == 1) return x;
|
||
if (obj.rotation == 2) return x - obj.canvas.canvas.width;
|
||
if (obj.rotation == 3) return x - obj.canvas.canvas.height;
|
||
return 0;
|
||
}
|
||
|
||
function _rotY(x, y) {
|
||
if (obj.rotation == 0) return y;
|
||
if (obj.rotation == 1) return y - obj.canvas.canvas.width;
|
||
if (obj.rotation == 2) return y - obj.canvas.canvas.height;
|
||
if (obj.rotation == 3) return y;
|
||
return 0;
|
||
}
|
||
|
||
obj.tcanvas = null;
|
||
obj.setRotation = function (x) {
|
||
while (x < 0) { x += 4; }
|
||
var newrotation = x % 4;
|
||
//console.log('hard-rot: ' + newrotation);
|
||
// ###BEGIN###{DesktopInband}
|
||
if (obj.holding == true) { obj.rotation = newrotation; return; }
|
||
// ###END###{DesktopInband}
|
||
|
||
if (newrotation == obj.rotation) return true;
|
||
var rw = obj.canvas.canvas.width;
|
||
var rh = obj.canvas.canvas.height;
|
||
if (obj.rotation == 1 || obj.rotation == 3) { rw = obj.canvas.canvas.height; rh = obj.canvas.canvas.width; }
|
||
|
||
// Copy the canvas, put it back in the correct direction
|
||
if (obj.tcanvas == null) obj.tcanvas = document.createElement('canvas');
|
||
var tcanvasctx = obj.tcanvas.getContext('2d');
|
||
tcanvasctx.setTransform(1, 0, 0, 1, 0, 0);
|
||
tcanvasctx.canvas.width = rw;
|
||
tcanvasctx.canvas.height = rh;
|
||
tcanvasctx.rotate((obj.rotation * -90) * Math.PI / 180);
|
||
if (obj.rotation == 0) tcanvasctx.drawImage(obj.canvas.canvas, 0, 0);
|
||
if (obj.rotation == 1) tcanvasctx.drawImage(obj.canvas.canvas, -obj.canvas.canvas.width, 0);
|
||
if (obj.rotation == 2) tcanvasctx.drawImage(obj.canvas.canvas, -obj.canvas.canvas.width, -obj.canvas.canvas.height);
|
||
if (obj.rotation == 3) tcanvasctx.drawImage(obj.canvas.canvas, 0, -obj.canvas.canvas.height);
|
||
|
||
// Change the size and orientation and copy the canvas back into the rotation
|
||
if (obj.rotation == 0 || obj.rotation == 2) { obj.canvas.canvas.height = rw; obj.canvas.canvas.width = rh; }
|
||
if (obj.rotation == 1 || obj.rotation == 3) { obj.canvas.canvas.height = rh; obj.canvas.canvas.width = rw; }
|
||
obj.canvas.setTransform(1, 0, 0, 1, 0, 0);
|
||
obj.canvas.rotate((newrotation * 90) * Math.PI / 180);
|
||
obj.rotation = newrotation;
|
||
obj.canvas.drawImage(obj.tcanvas, _rotX(0, 0), _rotY(0, 0));
|
||
|
||
obj.width = obj.canvas.canvas.width;
|
||
obj.height = obj.canvas.canvas.height;
|
||
if (obj.onScreenResize != null) obj.onScreenResize(obj, obj.width, obj.height, obj.CanvasId);
|
||
return true;
|
||
}
|
||
// ###END###{DesktopRotation}
|
||
|
||
function _fixColor(c) { return (c > 127) ? (c + 32) : c; }
|
||
|
||
function _SendRefresh() {
|
||
// ###BEGIN###{DesktopInband}
|
||
if (obj.holding == true) return;
|
||
// ###END###{DesktopInband}
|
||
// ###BEGIN###{DesktopFocus}
|
||
if (obj.focusmode > 0) {
|
||
// Request only pixels around the last mouse position
|
||
var df = obj.focusmode * 2;
|
||
obj.send(String.fromCharCode(3, 1) + ShortToStr(Math.max(Math.min(obj.ox, obj.mx) - obj.focusmode, 0)) + ShortToStr(Math.max(Math.min(obj.oy, obj.my) - obj.focusmode, 0)) + ShortToStr(df + Math.abs(obj.ox - obj.mx)) + ShortToStr(df + Math.abs(obj.oy - obj.my))); // FramebufferUpdateRequest
|
||
obj.ox = obj.mx;
|
||
obj.oy = obj.my;
|
||
} else {
|
||
// ###END###{DesktopFocus}
|
||
// Request the entire screen
|
||
obj.send(String.fromCharCode(3, 1, 0, 0, 0, 0) + ShortToStr(obj.rwidth) + ShortToStr(obj.rheight)); // FramebufferUpdateRequest
|
||
// ###BEGIN###{DesktopFocus}
|
||
}
|
||
// ###END###{DesktopFocus}
|
||
}
|
||
|
||
obj.Start = function () {
|
||
obj.state = 0;
|
||
obj.acc = null;
|
||
obj.ZRLEfirst = 1;
|
||
// ###BEGIN###{Inflate}
|
||
obj.inflate.inflateReset();
|
||
// ###END###{Inflate}
|
||
// ###BEGIN###{DesktopInband}
|
||
obj.onKvmDataPending = [];
|
||
obj.onKvmDataAck = -1;
|
||
obj.kvmDataSupported = false;
|
||
// ###END###{DesktopInband}
|
||
for (var i in obj.sparecache) { delete obj.sparecache[i]; }
|
||
}
|
||
|
||
obj.Stop = function () { obj.UnGrabMouseInput(); obj.UnGrabKeyInput(); if (obj.parent) { obj.parent.Stop(); } }
|
||
obj.send = function (x) { if (obj.parent) { obj.parent.send(x); } }
|
||
|
||
var convertAmtKeyCodeTable = {
|
||
"Pause": 19,
|
||
"CapsLock": 20,
|
||
"Space": 32,
|
||
"Quote": 39,
|
||
"Minus": 45,
|
||
"NumpadMultiply": 42,
|
||
"NumpadAdd": 43,
|
||
"PrintScreen": 44,
|
||
"Comma": 44,
|
||
"NumpadSubtract": 45,
|
||
"NumpadDecimal": 46,
|
||
"Period": 46,
|
||
"Slash": 47,
|
||
"NumpadDivide": 47,
|
||
"Semicolon": 59,
|
||
"Equal": 61,
|
||
"OSLeft": 91,
|
||
"BracketLeft": 91,
|
||
"OSRight": 91,
|
||
"Backslash": 92,
|
||
"BracketRight": 93,
|
||
"ContextMenu": 93,
|
||
"Backquote": 96,
|
||
"NumLock": 144,
|
||
"ScrollLock": 145,
|
||
"Backspace": 0xff08,
|
||
"Tab": 0xff09,
|
||
"Enter": 0xff0d,
|
||
"NumpadEnter": 0xff0d,
|
||
"Escape": 0xff1b,
|
||
"Delete": 0xffff,
|
||
"Home": 0xff50,
|
||
"PageUp": 0xff55,
|
||
"PageDown": 0xff56,
|
||
"ArrowLeft": 0xff51,
|
||
"ArrowUp": 0xff52,
|
||
"ArrowRight": 0xff53,
|
||
"ArrowDown": 0xff54,
|
||
"End": 0xff57,
|
||
"Insert": 0xff63,
|
||
"F1": 0xffbe,
|
||
"F2": 0xffbf,
|
||
"F3": 0xffc0,
|
||
"F4": 0xffc1,
|
||
"F5": 0xffc2,
|
||
"F6": 0xffc3,
|
||
"F7": 0xffc4,
|
||
"F8": 0xffc5,
|
||
"F9": 0xffc6,
|
||
"F10": 0xffc7,
|
||
"F11": 0xffc8,
|
||
"F12": 0xffc9,
|
||
"ShiftLeft": 0xffe1,
|
||
"ShiftRight": 0xffe2,
|
||
"ControlLeft": 0xffe3,
|
||
"ControlRight": 0xffe4,
|
||
"AltLeft": 0xffe9,
|
||
"AltRight": 0xffea,
|
||
"MetaLeft": 0xffe7,
|
||
"MetaRight": 0xffe8
|
||
}
|
||
function convertAmtKeyCode(e) {
|
||
if (e.code.startsWith('Key') && e.code.length == 4) { return e.code.charCodeAt(3) + ((e.shiftKey == false) ? 32 : 0); }
|
||
if (e.code.startsWith('Digit') && e.code.length == 6) { return e.code.charCodeAt(5); }
|
||
if (e.code.startsWith('Numpad') && e.code.length == 7) { return e.code.charCodeAt(6); }
|
||
return convertAmtKeyCodeTable[e.code];
|
||
}
|
||
|
||
/*
|
||
Intel AMT only recognizes a small subset of keysym characters defined in the keysymdef.h so you don<6F>t need to
|
||
implement all the languages (this is taken care by the USB Scancode Extension in RFB4.0 protocol).
|
||
The only subset recognized by the FW is the defined by the following sets : XK_LATIN1 , XK_MISCELLANY, XK_3270, XK_XKB_KEYS, XK_KATAKANA.
|
||
In addition to keysymdef.h symbols there are 6 japanese extra keys that we do support:
|
||
|
||
#define XK_Intel_EU_102kbd_backslash_pipe_45 0x17170056 // European 102-key: 45 (backslash/pipe), usb Usage: 0x64
|
||
#define XK_Intel_JP_106kbd_yen_pipe 0x1717007d // Japanese 106-key: 14 (Yen/pipe), usb Usage: 0x89
|
||
#define XK_Intel_JP_106kbd_backslash_underbar 0x17170073 // Japanese 106-key: 56 (backslash/underbar), usb Usage: 0x87
|
||
#define XK_Intel_JP_106kbd_NoConvert 0x1717007b // Japanese 106-key: 131 (NoConvert), usb Usage: 0x8b
|
||
#define XK_Intel_JP_106kbd_Convert 0x17170079 // Japanese 106-key: 132 (Convert), usb Usage: 0x8a
|
||
#define XK_Intel_JP_106kbd_Hirigana_Katakana 0x17170070 // Japanese 106-key: 133 (Hirigana/Katakana), usb Usage: 0x88
|
||
*/
|
||
|
||
function _keyevent(d, e) {
|
||
if (!e) { e = window.event; }
|
||
|
||
if (e.code && (obj.localKeyMap == false)) {
|
||
// For new browsers, this mapping is keyboard language independent
|
||
var k = convertAmtKeyCode(e);
|
||
if (k != null) { obj.sendkey(k, d); }
|
||
} else {
|
||
// For older browsers, this mapping works best for EN-US keyboard
|
||
var k = e.keyCode, kk = k;
|
||
if (e.shiftKey == false && k >= 65 && k <= 90) kk = k + 32;
|
||
if (k >= 112 && k <= 124) kk = k + 0xFF4E;
|
||
if (k == 8) kk = 0xff08; // Backspace
|
||
if (k == 9) kk = 0xff09; // Tab
|
||
if (k == 13) kk = 0xff0d; // Return
|
||
if (k == 16) kk = 0xffe1; // Shift (Left)
|
||
if (k == 17) kk = 0xffe3; // Ctrl (Left)
|
||
if (k == 18) kk = 0xffe9; // Alt (Left)
|
||
if (k == 27) kk = 0xff1b; // ESC
|
||
if (k == 33) kk = 0xff55; // PageUp
|
||
if (k == 34) kk = 0xff56; // PageDown
|
||
if (k == 35) kk = 0xff57; // End
|
||
if (k == 36) kk = 0xff50; // Home
|
||
if (k == 37) kk = 0xff51; // Left
|
||
if (k == 38) kk = 0xff52; // Up
|
||
if (k == 39) kk = 0xff53; // Right
|
||
if (k == 40) kk = 0xff54; // Down
|
||
if (k == 45) kk = 0xff63; // Insert
|
||
if (k == 46) kk = 0xffff; // Delete
|
||
if (k >= 96 && k <= 105) kk = k - 48; // Key pad numbers
|
||
if (k == 106) kk = 42; // Pad *
|
||
if (k == 107) kk = 43; // Pad +
|
||
if (k == 109) kk = 45; // Pad -
|
||
if (k == 110) kk = 46; // Pad .
|
||
if (k == 111) kk = 47; // Pad /
|
||
if (k == 186) kk = 59; // ;
|
||
if (k == 187) kk = 61; // =
|
||
if (k == 188) kk = 44; // ,
|
||
if (k == 189) kk = 45; // -
|
||
if (k == 190) kk = 46; // .
|
||
if (k == 191) kk = 47; // /
|
||
if (k == 192) kk = 96; // `
|
||
if (k == 219) kk = 91; // [
|
||
if (k == 220) kk = 92; // \
|
||
if (k == 221) kk = 93; // ]
|
||
if (k == 222) kk = 39; // '
|
||
//console.log('Key' + d + ": " + k + " = " + kk);
|
||
obj.sendkey(kk, d);
|
||
}
|
||
return obj.haltEvent(e);
|
||
}
|
||
|
||
obj.sendkey = function (k, d) {
|
||
if (typeof k == 'object') {
|
||
var buf = ''; for (var i in k) { buf += (String.fromCharCode(4, k[i][1], 0, 0) + IntToStr(k[i][0])); } obj.send(buf);
|
||
} else {
|
||
obj.send(String.fromCharCode(4, d, 0, 0) + IntToStr(k));
|
||
}
|
||
}
|
||
|
||
function handleServerCutText(acc, accview) {
|
||
if (acc.byteLength < 8) return 0;
|
||
var len = accview.getUint32(4) + 8;
|
||
if (acc.byteLength < len) return 0;
|
||
// ###BEGIN###{DesktopInband}
|
||
if (obj.onKvmData != null) {
|
||
var d = arrToStr(acc.slice(8, len));
|
||
if ((d.length >= 16) && (d.substring(0, 15) == '\0KvmDataChannel')) {
|
||
if (obj.kvmDataSupported == false) { obj.kvmDataSupported = true; console.log('KVM Data Channel Supported.'); }
|
||
if (((obj.onKvmDataAck == -1) && (d.length == 16)) || (d.charCodeAt(15) != 0)) { obj.onKvmDataAck = true; }
|
||
//if (urlvars && urlvars['kvmdatatrace']) { console.log('KVM-Recv(' + (d.length - 16) + '): ' + d.substring(16)); }
|
||
if (d.length >= 16) { obj.onKvmData(d.substring(16)); } // Event the data and ack
|
||
if ((obj.onKvmDataAck == true) && (obj.onKvmDataPending.length > 0)) { obj.sendKvmData(obj.onKvmDataPending.shift()); } // Send pending data
|
||
}
|
||
}
|
||
// ###END###{DesktopInband}
|
||
return len;
|
||
}
|
||
|
||
// ###BEGIN###{DesktopInband}
|
||
obj.sendKvmData = function (x) {
|
||
if (obj.onKvmDataAck !== true) {
|
||
obj.onKvmDataPending.push(x);
|
||
} else {
|
||
//if (urlvars && urlvars['kvmdatatrace']) { console.log('KVM-Send(' + x.length + '): ' + x); }
|
||
x = '\0KvmDataChannel\0' + x;
|
||
obj.send(String.fromCharCode(6, 0, 0, 0) + IntToStr(x.length) + x);
|
||
obj.onKvmDataAck = false;
|
||
}
|
||
}
|
||
|
||
// Send a HWKVM keep alive if it's not been sent in the last 5 seconds.
|
||
obj.sendKeepAlive = function () {
|
||
if (obj.lastKeepAlive < Date.now() - 5000) { obj.lastKeepAlive = Date.now(); obj.send(String.fromCharCode(6, 0, 0, 0) + IntToStr(16) + '\0KvmDataChannel\0'); }
|
||
}
|
||
// ###END###{DesktopInband}
|
||
|
||
obj.SendCtrlAltDelMsg = function () { obj.sendcad(); }
|
||
obj.sendcad = function () { obj.sendkey([[0xFFE3, 1], [0xFFE9, 1], [0xFFFF, 1], [0xFFFF, 0], [0xFFE9, 0], [0xFFE3, 0]]); } // Control down, Alt down, Delete down, Delete up , Alt up , Control up
|
||
|
||
var _MouseInputGrab = false;
|
||
var _KeyInputGrab = false;
|
||
|
||
obj.GrabMouseInput = function () {
|
||
if (_MouseInputGrab == true) return;
|
||
var c = obj.canvas.canvas;
|
||
c.onmouseup = obj.mouseup;
|
||
c.onmousedown = obj.mousedown;
|
||
c.onmousemove = obj.mousemove;
|
||
//if (navigator.userAgent.match(/mozilla/i)) c.DOMMouseScroll = obj.xxDOMMouseScroll; else c.onmousewheel = obj.xxMouseWheel;
|
||
_MouseInputGrab = true;
|
||
}
|
||
|
||
obj.UnGrabMouseInput = function () {
|
||
if (_MouseInputGrab == false) return;
|
||
var c = obj.canvas.canvas;
|
||
c.onmousemove = null;
|
||
c.onmouseup = null;
|
||
c.onmousedown = null;
|
||
//if (navigator.userAgent.match(/mozilla/i)) c.DOMMouseScroll = null; else c.onmousewheel = null;
|
||
_MouseInputGrab = false;
|
||
}
|
||
|
||
obj.GrabKeyInput = function () {
|
||
if (_KeyInputGrab == true) return;
|
||
document.onkeyup = obj.handleKeyUp;
|
||
document.onkeydown = obj.handleKeyDown;
|
||
document.onkeypress = obj.handleKeys;
|
||
_KeyInputGrab = true;
|
||
}
|
||
|
||
obj.UnGrabKeyInput = function () {
|
||
if (_KeyInputGrab == false) return;
|
||
document.onkeyup = null;
|
||
document.onkeydown = null;
|
||
document.onkeypress = null;
|
||
_KeyInputGrab = false;
|
||
}
|
||
|
||
obj.handleKeys = function (e) { return obj.haltEvent(e); }
|
||
obj.handleKeyUp = function (e) { return _keyevent(0, e); }
|
||
obj.handleKeyDown = function (e) { return _keyevent(1, e); }
|
||
obj.haltEvent = function (e) { if (e.preventDefault) e.preventDefault(); if (e.stopPropagation) e.stopPropagation(); return false; }
|
||
|
||
// RFB "PointerEvent" and mouse handlers
|
||
obj.mousedblclick = function (e) { }
|
||
obj.mousedown = function (e) { obj.buttonmask |= (1 << e.button); return obj.mousemove(e, 1); }
|
||
obj.mouseup = function (e) { obj.buttonmask &= (0xFFFF - (1 << e.button)); return obj.mousemove(e, 1); }
|
||
obj.mousemove = function (e, force) {
|
||
if (obj.state < 4) return true;
|
||
var ScaleFactorHeight = (obj.canvas.canvas.height / Q(obj.canvasid).offsetHeight);
|
||
var ScaleFactorWidth = (obj.canvas.canvas.width / Q(obj.canvasid).offsetWidth);
|
||
var Offsets = obj.getPositionOfControl(Q(obj.canvasid));
|
||
obj.mx = ((event.pageX - Offsets[0]) * ScaleFactorWidth);
|
||
obj.my = ((event.pageY - Offsets[1]) * ScaleFactorHeight);
|
||
if (event.addx) { obj.mx += event.addx; }
|
||
if (event.addy) { obj.my += event.addy; }
|
||
|
||
// ###BEGIN###{DesktopRotation}
|
||
if (obj.noMouseRotate != true) {
|
||
obj.mx2 = _crotX(obj.mx, obj.my);
|
||
obj.my = _crotY(obj.mx, obj.my);
|
||
obj.mx = obj.mx2;
|
||
}
|
||
// ###END###{DesktopRotation}
|
||
|
||
// This is the mouse motion nagle timer. Slow down the mouse motion event rate.
|
||
if (force == 1) {
|
||
obj.send(String.fromCharCode(5, obj.buttonmask) + ShortToStr(obj.mx) + ShortToStr(obj.my));
|
||
if (obj.mNagleTimer != null) { clearTimeout(obj.mNagleTimer); obj.mNagleTimer = null; }
|
||
} else {
|
||
if (obj.mNagleTimer == null) {
|
||
obj.mNagleTimer = setTimeout(function () {
|
||
obj.send(String.fromCharCode(5, obj.buttonmask) + ShortToStr(obj.mx) + ShortToStr(obj.my));
|
||
obj.mNagleTimer = null;
|
||
}, 50);
|
||
}
|
||
}
|
||
|
||
// ###BEGIN###{DesktopFocus}
|
||
// Update focus area if we are in focus mode
|
||
QV('DeskFocus', obj.focusmode);
|
||
if (obj.focusmode != 0) {
|
||
var x = Math.min(obj.mx, obj.canvas.canvas.width - obj.focusmode),
|
||
y = Math.min(obj.my, obj.canvas.canvas.height - obj.focusmode),
|
||
df = obj.focusmode * 2,
|
||
c = Q(obj.canvasid),
|
||
qx = c.offsetHeight / obj.canvas.canvas.height,
|
||
qy = c.offsetWidth / obj.canvas.canvas.width,
|
||
q = QS('DeskFocus'),
|
||
ppos = obj.getPositionOfControl(Q(obj.canvasid).parentElement);
|
||
q.left = (Math.max(((x - obj.focusmode) * qx), 0) + (pos[0] - ppos[0])) + 'px';
|
||
q.top = (Math.max(((y - obj.focusmode) * qy), 0) + (pos[1] - ppos[1])) + 'px';
|
||
q.width = ((df * qx) - 6) + 'px';
|
||
q.height = ((df * qx) - 6) + 'px';
|
||
}
|
||
// ###END###{DesktopFocus}
|
||
|
||
return obj.haltEvent(e);
|
||
}
|
||
|
||
obj.getPositionOfControl = function (Control) {
|
||
var Position = Array(2);
|
||
Position[0] = Position[1] = 0;
|
||
while (Control) {
|
||
Position[0] += Control.offsetLeft;
|
||
Position[1] += Control.offsetTop;
|
||
Control = Control.offsetParent;
|
||
}
|
||
return Position;
|
||
}
|
||
|
||
return obj;
|
||
}
|