mirror of
https://github.com/xcp-ng/xenadmin.git
synced 2024-11-25 06:16:37 +01:00
d7b519a53c
Signed-off-by: Konstantina Chremmou <Konstantina.Chremmou@cloud.com>
1425 lines
46 KiB
C#
1425 lines
46 KiB
C#
/* Copyright (c) Cloud Software Group, Inc.
|
|
*
|
|
* Redistribution and use in source and binary forms,
|
|
* with or without modification, are permitted provided
|
|
* that the following conditions are met:
|
|
*
|
|
* * Redistributions of source code must retain the above
|
|
* copyright notice, this list of conditions and the
|
|
* following disclaimer.
|
|
* * Redistributions in binary form must reproduce the above
|
|
* copyright notice, this list of conditions and the
|
|
* following disclaimer in the documentation and/or other
|
|
* materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
|
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
|
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
|
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
|
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
|
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
|
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
|
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
|
* SUCH DAMAGE.
|
|
*/
|
|
|
|
using System;
|
|
using System.Drawing;
|
|
using System.Drawing.Drawing2D;
|
|
using System.Windows.Forms;
|
|
using System.Runtime.InteropServices;
|
|
using System.Threading;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Text.RegularExpressions;
|
|
using DotNetVnc;
|
|
using XenAdmin.Core;
|
|
using System.Linq;
|
|
using XenCenterLib;
|
|
|
|
namespace XenAdmin.ConsoleView
|
|
{
|
|
public class VNCGraphicsClient : UserControl, IVNCGraphicsClient, IRemoteConsole
|
|
{
|
|
public const int BORDER_PADDING = 5;
|
|
public const int BORDER_WIDTH = 1;
|
|
|
|
private const int MOUSE_EVENTS_BEFORE_UPDATE = 2;
|
|
private const int MOUSE_EVENTS_DROPPED = 5; // should this be proportional to bandwidth?
|
|
|
|
private const int WM_KEYDOWN = 0x100;
|
|
private const int WM_SYSKEYDOWN = 0x104;
|
|
|
|
private static readonly log4net.ILog Log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
|
|
|
|
public event Action<object, Exception> ErrorOccurred;
|
|
public event EventHandler ConnectionSuccess;
|
|
private VNCStream _vncStream;
|
|
|
|
/// <summary>
|
|
/// connected implies that vncStream is non-null and ready to be used.
|
|
/// </summary>
|
|
private volatile bool _connected;
|
|
public bool Connected => _connected;
|
|
|
|
/// <summary>
|
|
/// terminated means that we have been told to disconnect. connected starts off false, becomes
|
|
/// true when a connection is made, and then becomes false again when connection goes away for
|
|
/// whatever reason. In contrast, terminated may become true even before a connection has been
|
|
/// made.
|
|
/// </summary>
|
|
private volatile bool _terminated;
|
|
public bool Terminated => _terminated;
|
|
|
|
private CustomCursor _remoteCursor;
|
|
private readonly CustomCursor _localCursor = new CustomCursor(Images.StaticImages.vnc_local_cursor, 2, 2);
|
|
private bool _cursorOver;
|
|
|
|
/// <summary>
|
|
/// This field is locked before any drawing is done through backGraphics or frontGraphics.
|
|
/// It is set in the constructor below, and then there is a delicate handover during desktop
|
|
/// resize to keep this safe.
|
|
/// </summary>
|
|
private Bitmap _backBuffer;
|
|
|
|
/// <summary>
|
|
/// The contents of the backbuffer are interesting if the backbuffer has been drawn to,
|
|
/// and we will present them to the user even after disconnect (if they are not interesting
|
|
/// we just show a blank rectangle instead).
|
|
/// </summary>
|
|
private volatile bool _backBufferInteresting;
|
|
|
|
/// <summary>
|
|
/// A Graphics object onto backBuffer. Access to this field must always be locked under backBuffer.
|
|
/// This field will be set to null in Dispose.
|
|
/// </summary>
|
|
private Graphics _backGraphics;
|
|
|
|
/// <summary>
|
|
/// Graphics on actual screen. Access to this field must always be locked under backBuffer.
|
|
/// This field will be set to null in Dispose.
|
|
/// </summary>
|
|
private Graphics _frontGraphics;
|
|
|
|
private readonly object _mouseEventLock = new object();
|
|
|
|
private Rectangle _damage = Rectangle.Empty;
|
|
|
|
private float _scale;
|
|
private float _dx;
|
|
private float _dy;
|
|
private float _oldDx;
|
|
private float _oldDy;
|
|
private int _bump;
|
|
private bool _scaling;
|
|
private bool _sendScanCodes = true;
|
|
private bool _useSource;
|
|
|
|
private static bool _handlingChange;
|
|
private bool _updateClipboardOnFocus;
|
|
private string _clipboardStash = string.Empty;
|
|
private int _currentMouseState;
|
|
/*
|
|
* We're going to track how many moves we have between screen updates
|
|
* to stop the mouse running away with long updates.
|
|
* Unfortunately we sometimes seem to send moves and get no update
|
|
* back, so we cap the number of 'dropped' moves.
|
|
*/
|
|
private volatile int _mouseMoved;
|
|
private volatile int _mouseNotMoved;
|
|
|
|
private MouseEventArgs _pending ;
|
|
private int _pendingState;
|
|
private int _lastState;
|
|
|
|
private Set<Keys> _pressedKeys = new Set<Keys>();
|
|
private Set<int> _pressedScans = new Set<int>();
|
|
|
|
private bool _modifierKeyPressedAlone;
|
|
private bool _helperIsPaused = true;
|
|
private bool _displayBorder = true;
|
|
private bool _altGrReleaseSent;
|
|
|
|
public bool UseSource
|
|
{
|
|
set => _useSource = value;
|
|
}
|
|
|
|
private bool TalkingToVNCTerm => !_sendScanCodes && _useSource;
|
|
|
|
public string UUID => SourceVM.uuid;
|
|
|
|
public XenAPI.VM SourceVM { get; set; }
|
|
|
|
public string VmName => SourceVM.name_label;
|
|
|
|
public object Console;
|
|
|
|
public event EventHandler DesktopResized;
|
|
|
|
public bool UseQemuExtKeyEncoding { set; private get; }
|
|
|
|
public VNCGraphicsClient(ContainerControl parent)
|
|
{
|
|
Program.AssertOnEventThread();
|
|
|
|
this.SetStyle(ControlStyles.AllPaintingInWmPaint
|
|
| ControlStyles.UserPaint
|
|
| ControlStyles.ResizeRedraw, true);
|
|
SetStyle(ControlStyles.Opaque, false);
|
|
|
|
_frontGraphics = CreateGraphics();
|
|
SetupGraphicsOptions(_frontGraphics);
|
|
_backBuffer = new Bitmap(640, 480, _frontGraphics);
|
|
_backGraphics = Graphics.FromImage(_backBuffer);
|
|
SetupGraphicsOptions(_backGraphics);
|
|
using (SolidBrush backBrush = new SolidBrush(BackColor))
|
|
{
|
|
_backGraphics.FillRectangle(backBrush, 0, 0, 640, 480);
|
|
}
|
|
DesktopSize = new Size(640, 480);
|
|
|
|
#pragma warning disable 0219
|
|
IntPtr _ = Handle;
|
|
#pragma warning restore 0219
|
|
|
|
Clip.ClipboardChanged += ClipboardChanged;
|
|
parent.Controls.Add(this);
|
|
}
|
|
|
|
private static void SetupGraphicsOptions(Graphics g)
|
|
{
|
|
g.CompositingQuality = CompositingQuality.AssumeLinear;
|
|
g.InterpolationMode = InterpolationMode.Default;
|
|
g.SmoothingMode = SmoothingMode.Default;
|
|
}
|
|
|
|
protected override void Dispose(bool disposing)
|
|
{
|
|
try
|
|
{
|
|
if (!disposing)
|
|
return;
|
|
|
|
Clip.ClipboardChanged -= ClipboardChanged;
|
|
|
|
Disconnect();
|
|
|
|
lock (_backBuffer)
|
|
{
|
|
_frontGraphics.Dispose();
|
|
_backGraphics.Dispose();
|
|
_backBuffer.Dispose();
|
|
_backGraphics = null;
|
|
_frontGraphics = null;
|
|
}
|
|
|
|
if (_remoteCursor != null)
|
|
{
|
|
_remoteCursor.Dispose();
|
|
_remoteCursor = null;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
base.Dispose(disposing);
|
|
}
|
|
}
|
|
|
|
internal void Connect(Stream stream, char[] password)
|
|
{
|
|
Program.AssertOnEventThread();
|
|
|
|
_vncStream = new VNCStream(this, stream, _helperIsPaused);
|
|
_vncStream.ErrorOccurred += OnError;
|
|
_vncStream.ConnectionSuccess += vncStream_ConnectionSuccess;
|
|
_connected = true;
|
|
_vncStream.Connect(password);
|
|
}
|
|
|
|
private void vncStream_ConnectionSuccess(object sender, EventArgs e)
|
|
{
|
|
Program.AssertOffEventThread();
|
|
|
|
// Set the remote clipboard based on the current contents.
|
|
Program.Invoke(this, (EventHandler)ClipboardChanged, null, null);
|
|
|
|
if (ConnectionSuccess != null)
|
|
Program.Invoke(this, ConnectionSuccess, sender, e);
|
|
}
|
|
|
|
private void OnError(object sender, Exception e)
|
|
{
|
|
Program.AssertOffEventThread();
|
|
System.Diagnostics.Debug.Assert(sender == _vncStream); // Please see to CA-236844 if this assertion fails
|
|
|
|
if (sender != _vncStream)
|
|
return;
|
|
|
|
_connected = false;
|
|
if (ErrorOccurred != null)
|
|
ErrorOccurred(this, e);
|
|
}
|
|
|
|
private void Disconnect()
|
|
{
|
|
_connected = false;
|
|
_terminated = true;
|
|
if (_vncStream != null)
|
|
{
|
|
_vncStream.ErrorOccurred -= OnError;
|
|
_vncStream.ConnectionSuccess -= vncStream_ConnectionSuccess;
|
|
}
|
|
VNCStream s = _vncStream;
|
|
_vncStream = null;
|
|
s?.Close();
|
|
}
|
|
|
|
private bool RedirectingClipboard()
|
|
{
|
|
return Properties.Settings.Default.ClipboardAndPrinterRedirection;
|
|
}
|
|
|
|
private void ClipboardChanged(object sender, EventArgs args)
|
|
{
|
|
Program.AssertOnEventThread();
|
|
|
|
if (!RedirectingClipboard())
|
|
return;
|
|
|
|
try
|
|
{
|
|
if (!_handlingChange && _connected)
|
|
{
|
|
if (Focused)
|
|
SetConsoleClipboard();
|
|
else
|
|
_updateClipboardOnFocus = true;
|
|
}
|
|
}
|
|
catch (IOException)
|
|
{
|
|
// The server's gone away -- that's fine.
|
|
}
|
|
catch (Exception exn)
|
|
{
|
|
Log.Warn(exn, exn);
|
|
// Nothing more we can do with this.
|
|
}
|
|
}
|
|
|
|
private void SetConsoleClipboard()
|
|
{
|
|
try
|
|
{
|
|
_handlingChange = true;
|
|
string text = Clip.ClipboardText;
|
|
if (TalkingToVNCTerm)
|
|
text = text.Replace("\r\n", "\n");
|
|
_vncStream.ClientCutText(text);
|
|
_updateClipboardOnFocus = false;
|
|
}
|
|
finally
|
|
{
|
|
_handlingChange = false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Records damage to the screen.
|
|
/// </summary>
|
|
private void Damage(int x, int y, int width, int height)
|
|
{
|
|
Program.AssertOffEventThread();
|
|
|
|
Rectangle r = new Rectangle(x, y, width, height);
|
|
|
|
if (_scaling)
|
|
{
|
|
r.Inflate(_bump, _bump); // Fix for scaling issues
|
|
}
|
|
|
|
if (_damage.IsEmpty)
|
|
{
|
|
_damage = r;
|
|
}
|
|
else
|
|
{
|
|
_damage = Rectangle.Union(_damage, r);
|
|
}
|
|
}
|
|
|
|
private void CheckAssertion(bool assertion, String message, params Object[] args)
|
|
{
|
|
if (!assertion)
|
|
{
|
|
Log.Error("Bad VNC server message: " + String.Format(message, args));
|
|
}
|
|
}
|
|
|
|
#region Functions called by vncStream to update display
|
|
|
|
public void ClientDrawImage(Bitmap image, int x, int y, int width, int height)
|
|
{
|
|
Program.AssertOffEventThread();
|
|
|
|
Damage(x, y, width, height);
|
|
lock (_backBuffer)
|
|
{
|
|
try
|
|
{
|
|
if (_backGraphics != null)
|
|
_backGraphics.DrawImageUnscaled(image, x, y);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
// We seem to be very occasionally getting weird exception from this. These are probably due to
|
|
// bad server messages, so we can just log and ignore them
|
|
|
|
Log.Error("Error drawing image from server", e);
|
|
|
|
try
|
|
{
|
|
CheckAssertion(image.Width == width, "Width {0} != {1}", image.Width, width);
|
|
CheckAssertion(image.Height == height, "Height {0} != {1}", image.Height, height);
|
|
CheckAssertion(x < DesktopSize.Width, "x {0} >= {1}", x, DesktopSize.Width);
|
|
CheckAssertion(y < DesktopSize.Height, "y {0} >= {1}", y, DesktopSize.Height);
|
|
CheckAssertion(x + width <= DesktopSize.Width, "x + width {0} + {1} > {2}", x, width, DesktopSize.Width);
|
|
CheckAssertion(y + height <= DesktopSize.Height, "y + height {0} + {1} > {2}", y, height, DesktopSize.Height);
|
|
}
|
|
catch
|
|
{
|
|
// ignored
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Custom Cursor handling
|
|
|
|
// Taken from
|
|
// http://www.codeproject.com/cs/miscctrl/DragDropTreeview.asp?df=100&forumid=84437&exp=0&select=1838138#xx1838138xx
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
public struct IconInfo
|
|
{
|
|
public bool fIcon;
|
|
public int xHotspot;
|
|
public int yHotspot;
|
|
public IntPtr hbmMask;
|
|
public IntPtr hbmColor;
|
|
}
|
|
|
|
[DllImport("user32.dll")]
|
|
public static extern IntPtr CreateIconIndirect(ref IconInfo iconinfo);
|
|
|
|
[DllImport("user32.dll")]
|
|
public static extern bool DestroyIcon(IntPtr hIcon);
|
|
|
|
private class CustomCursor : IDisposable
|
|
{
|
|
private IntPtr _handle = IntPtr.Zero;
|
|
|
|
internal CustomCursor(Bitmap bitmap, int x, int y)
|
|
{
|
|
var iconInfo = new IconInfo
|
|
{
|
|
fIcon = false,
|
|
xHotspot = x,
|
|
yHotspot = y,
|
|
hbmMask = bitmap.GetHbitmap(),
|
|
hbmColor = bitmap.GetHbitmap()
|
|
};
|
|
|
|
_handle = CreateIconIndirect(ref iconInfo);
|
|
Cursor = new Cursor(_handle);
|
|
}
|
|
|
|
~CustomCursor()
|
|
{
|
|
Dispose(false);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
GC.SuppressFinalize(this);
|
|
Dispose(true);
|
|
}
|
|
|
|
protected virtual void Dispose(bool disposing)
|
|
{
|
|
try
|
|
{
|
|
if (_handle != IntPtr.Zero)
|
|
DestroyIcon(_handle);
|
|
}
|
|
catch
|
|
{
|
|
// ignored
|
|
}
|
|
finally
|
|
{
|
|
_handle = IntPtr.Zero;
|
|
}
|
|
}
|
|
|
|
public Cursor Cursor { get; }
|
|
}
|
|
|
|
public void ClientSetCursor(Bitmap image, int x, int y, int width, int height)
|
|
{
|
|
Program.AssertOffEventThread();
|
|
|
|
_remoteCursor?.Dispose();
|
|
_remoteCursor = new CustomCursor(image, x, y);
|
|
|
|
Program.Invoke(this, () =>
|
|
{
|
|
if (_cursorOver)
|
|
Cursor = _remoteCursor.Cursor;
|
|
});
|
|
}
|
|
|
|
#endregion
|
|
|
|
public void ClientCopyRectangle(int x, int y, int width, int height, int dx, int dy)
|
|
{
|
|
Program.AssertOffEventThread();
|
|
|
|
Damage(dx, dy, width, height);
|
|
lock (_backBuffer)
|
|
{
|
|
if (_backGraphics != null)
|
|
GraphicsUtils.copyRect(_backBuffer, x, y, width, height, _backGraphics, dx, dy);
|
|
}
|
|
}
|
|
|
|
public void ClientFillRectangle(int x, int y, int width, int height, Color color)
|
|
{
|
|
Program.AssertOffEventThread();
|
|
|
|
Damage(x, y, width, height);
|
|
lock (_backBuffer)
|
|
{
|
|
if (_backGraphics != null)
|
|
{
|
|
using (SolidBrush backBrush = new SolidBrush(color))
|
|
{
|
|
_backGraphics.FillRectangle(backBrush, x, y, width, height);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public void ClientFrameBufferUpdate()
|
|
{
|
|
Program.AssertOffEventThread();
|
|
|
|
lock (_backBuffer)
|
|
{
|
|
try
|
|
{
|
|
if (!_damage.IsEmpty)
|
|
{
|
|
if (_frontGraphics != null)
|
|
_frontGraphics.DrawImage(_backBuffer, _damage, _damage, GraphicsUnit.Pixel);
|
|
_damage = Rectangle.Empty;
|
|
}
|
|
|
|
_backBufferInteresting = true;
|
|
}
|
|
catch
|
|
{
|
|
// ignored
|
|
}
|
|
}
|
|
|
|
/*
|
|
* If there is a pending mouse event, send it.
|
|
* Also reset the mouse event counters
|
|
*/
|
|
lock (_mouseEventLock)
|
|
{
|
|
if (_pending != null)
|
|
{
|
|
MouseEvent(_pendingState, _pending.X, _pending.Y);
|
|
_pending = null;
|
|
}
|
|
|
|
_mouseMoved = 0;
|
|
_mouseNotMoved = 0;
|
|
}
|
|
}
|
|
|
|
public void ClientBell()
|
|
{
|
|
}
|
|
|
|
public void ClientCutText(String text)
|
|
{
|
|
Program.AssertOffEventThread();
|
|
|
|
if (TalkingToVNCTerm)
|
|
{
|
|
// Lets translate from unix line endings to windows ones...
|
|
_clipboardStash = Regex.Replace(text, "\r?\n", "\r\n");
|
|
}
|
|
else
|
|
{
|
|
if (!RedirectingClipboard())
|
|
return;
|
|
Program.Invoke(this, () =>
|
|
{
|
|
if (Clipboard.ContainsText() && Clipboard.GetText() == text)
|
|
return;
|
|
Clip.SetClipboardText(text);
|
|
});
|
|
}
|
|
}
|
|
|
|
public void ClientDesktopSize(int width, int height)
|
|
{
|
|
Program.AssertOffEventThread();
|
|
|
|
// Cannot do an invoke with a locked back buffer, as it may event thread
|
|
// (onPaint) tried to lock back buffer as well - therefore deadlock.
|
|
|
|
Program.Invoke(this, () =>
|
|
{
|
|
Bitmap oldBackBuffer;
|
|
lock (_backBuffer)
|
|
{
|
|
if (width <= 0)
|
|
width = 1;
|
|
if (height <= 0)
|
|
height = 1;
|
|
|
|
if (width == DesktopSize.Width &&
|
|
height == DesktopSize.Height)
|
|
{
|
|
return;
|
|
}
|
|
|
|
oldBackBuffer = _backBuffer;
|
|
|
|
_backGraphics.Dispose();
|
|
_frontGraphics.Dispose();
|
|
|
|
_frontGraphics = CreateGraphics();
|
|
SetupGraphicsOptions(_frontGraphics);
|
|
|
|
Bitmap newBackBuffer = new Bitmap(width, height, _frontGraphics);
|
|
_backGraphics = Graphics.FromImage(newBackBuffer);
|
|
SetupGraphicsOptions(_backGraphics);
|
|
|
|
using (SolidBrush backBrush = new SolidBrush(BackColor))
|
|
{
|
|
_backGraphics.FillRectangle(backBrush, 0, 0, width, height);
|
|
}
|
|
|
|
DesktopSize = new Size(width, height);
|
|
|
|
// Now that backGraphics is valid, we can switch backBuffer. We're relying on backBuffer
|
|
// as our lock.
|
|
_backBuffer = newBackBuffer;
|
|
}
|
|
|
|
lock (_backBuffer)
|
|
{
|
|
SetupScaling();
|
|
}
|
|
|
|
Invalidate();
|
|
Update();
|
|
|
|
oldBackBuffer.Dispose();
|
|
|
|
OnDesktopResized();
|
|
});
|
|
}
|
|
|
|
private void OnDesktopResized()
|
|
{
|
|
Program.AssertOnEventThread();
|
|
|
|
if (DesktopResized != null)
|
|
DesktopResized(this, null);
|
|
}
|
|
|
|
protected override void OnScroll(ScrollEventArgs se)
|
|
{
|
|
base.OnScroll(se);
|
|
|
|
if (!_scaling && se.OldValue != se.NewValue)
|
|
{
|
|
if (se.ScrollOrientation == ScrollOrientation.HorizontalScroll)
|
|
{
|
|
_dx = -1 * se.NewValue + (_displayBorder ? BORDER_PADDING : 0);
|
|
lock (_backBuffer)
|
|
{
|
|
if (_frontGraphics != null)
|
|
{
|
|
_frontGraphics.ResetTransform();
|
|
_frontGraphics.TranslateTransform(_dx, _dy);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (se.ScrollOrientation == ScrollOrientation.VerticalScroll)
|
|
{
|
|
_dy = (-1 * se.NewValue) + (_displayBorder ? BORDER_PADDING : 0);
|
|
lock (_backBuffer)
|
|
{
|
|
if (_frontGraphics != null)
|
|
{
|
|
_frontGraphics.ResetTransform();
|
|
_frontGraphics.TranslateTransform(_dx, _dy);
|
|
}
|
|
}
|
|
}
|
|
|
|
this.Refresh();
|
|
}
|
|
}
|
|
|
|
private void MyPaintBackground(Graphics g, int x, int y, int width, int height)
|
|
{
|
|
Rectangle r = new Rectangle(x, y, width, height);
|
|
|
|
// Hack pulled from Control.cs:PaintTransparentBackground
|
|
if (Application.RenderWithVisualStyles)
|
|
{
|
|
ButtonRenderer.DrawParentBackground(g, r, this);
|
|
}
|
|
else
|
|
{
|
|
using (Brush backBrush = new SolidBrush(BackColor))
|
|
{
|
|
g.FillRectangle(backBrush, r);
|
|
}
|
|
}
|
|
}
|
|
|
|
protected override void OnPaintBackground(PaintEventArgs e)
|
|
{
|
|
//do nothing
|
|
}
|
|
|
|
protected override void OnPaint(PaintEventArgs e)
|
|
{
|
|
Program.AssertOnEventThread();
|
|
|
|
if (_connected || _backBufferInteresting)
|
|
{
|
|
/*
|
|
* Draw the background by working out the surrounding bars
|
|
* as opposed to just one big black rect, to reduce flicker.
|
|
*/
|
|
|
|
// Draw two vertical bars at either end
|
|
|
|
int w = (int)_dx;
|
|
|
|
Graphics g = e.Graphics;
|
|
|
|
lock (_backBuffer)
|
|
{
|
|
if (_frontGraphics != null)
|
|
_frontGraphics.DrawImageUnscaled(_backBuffer, 0, 0);
|
|
}
|
|
|
|
if (w > 0)
|
|
MyPaintBackground(g, 0, 0, w + 1, ClientSize.Height);
|
|
|
|
w = (int)(ClientSize.Width - _dx - (DesktopSize.Width * _scale)) + 1;
|
|
|
|
if (w > 0)
|
|
MyPaintBackground(g, ClientSize.Width - w, 0, w, ClientSize.Height);
|
|
|
|
// Draw two horizontal bars at top and bottom
|
|
|
|
int h = (int)_dy;
|
|
|
|
if (h > 0)
|
|
MyPaintBackground(g, 0, 0, Size.Width, h + 1);
|
|
|
|
h = (int)(ClientSize.Height - _dy - (DesktopSize.Height * _scale)) + 1;
|
|
|
|
if (h > 0)
|
|
MyPaintBackground(g, 0, ClientSize.Height - h, Size.Width, h);
|
|
}
|
|
else
|
|
{
|
|
base.OnPaintBackground(e);
|
|
}
|
|
}
|
|
|
|
protected override void OnResize(EventArgs e)
|
|
{
|
|
Program.AssertOnEventThread();
|
|
base.OnResize(e);
|
|
|
|
/*
|
|
* Need to recreate the front graphics or we
|
|
* get weird clipping problems.
|
|
*/
|
|
lock (_backBuffer)
|
|
{
|
|
if (_frontGraphics != null)
|
|
_frontGraphics.Dispose();
|
|
_frontGraphics = CreateGraphics();
|
|
SetupGraphicsOptions(_frontGraphics);
|
|
SetupScaling();
|
|
|
|
// We don't need to redraw, as the window will repaint us
|
|
}
|
|
}
|
|
|
|
private void MouseEvent(int state, int x, int y)
|
|
{
|
|
DoIfConnected(() =>
|
|
{
|
|
if (TalkingToVNCTerm && _lastState == 4 && state == 0)
|
|
{
|
|
ShowPopupMenu(x, y);
|
|
}
|
|
else if (_scaling)
|
|
{
|
|
_vncStream.PointerEvent(state, (int)((x - _dx) / _scale), (int)((y - _dy) / _scale));
|
|
}
|
|
else
|
|
{
|
|
_vncStream.PointerEvent(state, x - (int)_dx, y - (int)_dy);
|
|
}
|
|
|
|
_lastState = state;
|
|
});
|
|
}
|
|
|
|
private void ShowPopupMenu(int x, int y)
|
|
{
|
|
ToolStripDropDownMenu popupMenu = new ToolStripDropDownMenu();
|
|
|
|
ToolStripMenuItem copyItem = new ToolStripMenuItem(Messages.COPY);
|
|
|
|
copyItem.Image = Images.StaticImages.copy_16;
|
|
copyItem.Click += copyItem_Click;
|
|
|
|
popupMenu.Items.Add(copyItem);
|
|
if (SourceVM != null && SourceVM.power_state == XenAPI.vm_power_state.Running)
|
|
{
|
|
ToolStripMenuItem pasteItem = new ToolStripMenuItem(Messages.PASTE);
|
|
pasteItem.Image = Images.StaticImages.paste_16;
|
|
pasteItem.Click += pasteItem_Click;
|
|
|
|
popupMenu.Items.Add(pasteItem);
|
|
}
|
|
popupMenu.Show(this, x, y);
|
|
}
|
|
|
|
private void copyItem_Click(object sender, EventArgs e)
|
|
{
|
|
System.Diagnostics.Trace.Assert(TalkingToVNCTerm);
|
|
Program.AssertOnEventThread();
|
|
if (_clipboardStash != "")
|
|
{
|
|
Clip.SetClipboardText(_clipboardStash);
|
|
}
|
|
}
|
|
|
|
private void pasteItem_Click(object sender, EventArgs e)
|
|
{
|
|
System.Diagnostics.Trace.Assert(TalkingToVNCTerm);
|
|
Program.AssertOnEventThread();
|
|
|
|
if (Clipboard.ContainsText())
|
|
{
|
|
_vncStream.ClientCutText(Clipboard.GetText().Replace("\r\n", "\n"));
|
|
MouseEvent(2, 0, 0);
|
|
MouseEvent(0, 0, 0);
|
|
}
|
|
}
|
|
|
|
private void DisableMenuShortcuts()
|
|
{
|
|
Program.MainWindow.MenuShortcutsEnabled = false;
|
|
}
|
|
|
|
private void EnableMenuShortcuts()
|
|
{
|
|
Program.MainWindow.MenuShortcutsEnabled = true;
|
|
}
|
|
|
|
protected override void OnGotFocus(EventArgs e)
|
|
{
|
|
Program.AssertOnEventThread();
|
|
base.OnGotFocus(e);
|
|
|
|
DisableMenuShortcuts();
|
|
|
|
if (_sendScanCodes)
|
|
{
|
|
InterceptKeys.grabKeys(KeyScan, false);
|
|
}
|
|
|
|
if (_updateClipboardOnFocus)
|
|
SetConsoleClipboard();
|
|
}
|
|
|
|
protected override void OnLostFocus(EventArgs e)
|
|
{
|
|
Program.AssertOnEventThread();
|
|
base.OnLostFocus(e);
|
|
|
|
EnableMenuShortcuts();
|
|
|
|
InterceptKeys.releaseKeys();
|
|
|
|
_cursorOver = false;
|
|
|
|
Cursor = Cursors.Default;
|
|
|
|
//Release any held keys
|
|
DoIfConnected(() =>
|
|
{
|
|
foreach (Keys key in _pressedKeys)
|
|
{
|
|
// This won't release composite key events atm.
|
|
int sym = KeyMap.translateKey(ConsoleKeyHandler.GetSimpleKey(key));
|
|
if (sym > 0)
|
|
_vncStream.keyCodeEvent(false, sym);
|
|
}
|
|
|
|
foreach (int key in _pressedScans)
|
|
{
|
|
_vncStream.keyScanEvent(false, key, -1, UseQemuExtKeyEncoding);
|
|
}
|
|
});
|
|
|
|
_pressedKeys = new Set<Keys>();
|
|
_pressedScans = new Set<int>();
|
|
}
|
|
|
|
protected override void OnMouseMove(MouseEventArgs e)
|
|
{
|
|
// Only hide the mouse when over the actual screen,
|
|
// some parts of this control will be window blinds
|
|
|
|
if (Focused && Connected)
|
|
{
|
|
int top = (int)_dy;
|
|
int left = (int)_dx;
|
|
int bottom = (int)((DesktopSize.Height * _scale) + top);
|
|
int right = (int)((DesktopSize.Width * _scale) + left);
|
|
|
|
if (e.X > left && e.X < right
|
|
&& e.Y > top && e.Y < bottom)
|
|
{
|
|
_cursorOver = true;
|
|
|
|
if (_remoteCursor == null)
|
|
Cursor = _localCursor.Cursor;
|
|
else
|
|
Cursor = _remoteCursor.Cursor;
|
|
|
|
lock (_mouseEventLock)
|
|
{
|
|
if (_mouseMoved < MOUSE_EVENTS_BEFORE_UPDATE)
|
|
{
|
|
_mouseMoved++;
|
|
|
|
MouseEvent(_currentMouseState, e.X, e.Y);
|
|
}
|
|
else if (_mouseNotMoved > MOUSE_EVENTS_DROPPED)
|
|
{
|
|
_mouseMoved = 0;
|
|
_mouseNotMoved = 0;
|
|
}
|
|
else
|
|
{
|
|
_mouseNotMoved++;
|
|
|
|
_pendingState = _currentMouseState;
|
|
_pending = e;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_cursorOver = false;
|
|
Cursor = Cursors.Default;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// We're not going to buffer any clicks etc.
|
|
/// Don't forget to clear any pending moves.
|
|
/// </summary>
|
|
protected override void OnMouseDown(MouseEventArgs e)
|
|
{
|
|
//added this line to track mouse clicks for automatic switch to RDP
|
|
//if there are any issues with focus start looking here
|
|
base.OnMouseDown(e);
|
|
Focus();
|
|
|
|
_pending = null;
|
|
switch (e.Button)
|
|
{
|
|
case MouseButtons.Left:
|
|
_currentMouseState |= 1;
|
|
break;
|
|
case MouseButtons.Right:
|
|
_currentMouseState |= 4;
|
|
break;
|
|
case MouseButtons.Middle:
|
|
_currentMouseState |= 2;
|
|
break;
|
|
}
|
|
MouseEvent(_currentMouseState, e.X, e.Y);
|
|
}
|
|
|
|
protected override void OnMouseUp(MouseEventArgs e)
|
|
{
|
|
_pending = null;
|
|
switch (e.Button)
|
|
{
|
|
case MouseButtons.Left:
|
|
_currentMouseState &= ~1;
|
|
break;
|
|
case MouseButtons.Right:
|
|
_currentMouseState &= ~4;
|
|
break;
|
|
case MouseButtons.Middle:
|
|
_currentMouseState &= ~2;
|
|
break;
|
|
}
|
|
MouseEvent(_currentMouseState, e.X, e.Y);
|
|
}
|
|
|
|
protected override void OnMouseWheel(MouseEventArgs e)
|
|
{
|
|
if (Focused)
|
|
{
|
|
DoIfConnected(() => _vncStream.PointerWheelEvent((int)((e.X - _dx) / _scale), (int)((e.Y - _dy) / _scale),
|
|
e.Delta * -SystemInformation.MouseWheelScrollLines / 120));
|
|
|
|
}
|
|
}
|
|
|
|
protected override void OnMouseLeave(EventArgs e)
|
|
{
|
|
Cursor = Cursors.Default;
|
|
_cursorOver = false;
|
|
base.OnMouseLeave(e);
|
|
_currentMouseState = 0;
|
|
_mouseMoved = 0;
|
|
}
|
|
|
|
protected override bool ProcessTabKey(bool forward)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
private void DoIfConnected(MethodInvoker methodInvoker)
|
|
{
|
|
if (_connected)
|
|
{
|
|
try
|
|
{
|
|
methodInvoker();
|
|
}
|
|
catch (IOException)
|
|
{
|
|
// The server's gone away -- that's fine.
|
|
}
|
|
catch (Exception exn)
|
|
{
|
|
Log.Warn(exn, exn);
|
|
// Nothing more we can do with this.
|
|
}
|
|
}
|
|
}
|
|
|
|
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
|
|
{
|
|
bool down = ((msg.Msg == WM_KEYDOWN) || (msg.Msg == WM_SYSKEYDOWN));
|
|
|
|
Keys key = keyData;
|
|
|
|
if ((key & Keys.Control) == Keys.Control)
|
|
key &= ~Keys.Control;
|
|
|
|
if ((key & Keys.Alt) == Keys.Alt)
|
|
key &= ~Keys.Alt;
|
|
|
|
if ((key & Keys.Shift) == Keys.Shift)
|
|
key &= ~Keys.Shift;
|
|
|
|
// use TranslateKeyMessage to identify if Left or Right modifier keys have been pressed/released
|
|
Keys extKey = ConsoleKeyHandler.TranslateKeyMessage(msg);
|
|
|
|
return KeySym(down, key, extKey);
|
|
}
|
|
|
|
protected override void OnKeyUp(KeyEventArgs e)
|
|
{
|
|
// we cannot identify if Left or Right modifier keys have been pressed
|
|
e.Handled = KeySym(false, e.KeyCode, e.KeyCode);
|
|
}
|
|
|
|
private void SendAltGrKeySym(bool pressed)
|
|
{
|
|
KeySym_(pressed, Keys.ControlKey);
|
|
KeySym_(pressed, Keys.Menu);
|
|
}
|
|
|
|
private void HandleAltGrKeySym(bool pressed, Keys key)
|
|
{
|
|
if (pressed)
|
|
{
|
|
if (_pressedKeys.Count > 2 && key != Keys.ControlKey && key != Keys.Menu)
|
|
{
|
|
bool isAltGrPressed = _pressedKeys.Where(
|
|
extKey => ConsoleKeyHandler.GetSimpleKey(extKey) == Keys.ControlKey ||
|
|
ConsoleKeyHandler.GetSimpleKey(extKey) == Keys.Menu).ToList().Count == 2;
|
|
|
|
if (isAltGrPressed &&
|
|
(KeyMap.IsMapped(key) && _altGrReleaseSent || !KeyMap.IsMapped(key) && !_altGrReleaseSent))
|
|
{
|
|
SendAltGrKeySym(_altGrReleaseSent);
|
|
_altGrReleaseSent = !_altGrReleaseSent;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (key == Keys.ControlKey || key == Keys.Menu)
|
|
_altGrReleaseSent = false;
|
|
}
|
|
}
|
|
|
|
private bool KeySym(bool pressed, Keys key, Keys extendedKey)
|
|
{
|
|
if (_sendScanCodes)
|
|
return true;
|
|
|
|
if (KeyHandler.handleExtras(pressed, _pressedKeys, KeyHandler.ExtraKeys, extendedKey, KeyHandler.ModifierKeys, ref _modifierKeyPressedAlone))
|
|
{
|
|
if (!pressed && _modifierKeyPressedAlone)
|
|
{
|
|
// send key up anyway
|
|
_modifierKeyPressedAlone = false;
|
|
return KeySym_(false, key);
|
|
}
|
|
Parent.Focus();
|
|
return true;
|
|
}
|
|
|
|
HandleAltGrKeySym(pressed, key);
|
|
|
|
// on keyup, try to remove extended keys (i.e. LControlKey, LControlKey, RShiftKey, LShiftKey, RMenu, LMenu)
|
|
// we need to do this here, because we cannot otherwise distinguish between Left and Right modifier keys on KeyUp
|
|
if (!pressed)
|
|
{
|
|
List<Keys> extendedKeys = ConsoleKeyHandler.GetExtendedKeys(key);
|
|
foreach (var k in extendedKeys)
|
|
{
|
|
_pressedKeys.Remove(k);
|
|
}
|
|
}
|
|
|
|
return KeySym_(pressed, key);
|
|
}
|
|
|
|
private bool KeySym_(bool pressed, Keys key)
|
|
{
|
|
int keysym = KeyMap.translateKey(key);
|
|
|
|
if (keysym > 0)
|
|
{
|
|
DoIfConnected(() => _vncStream.keyCodeEvent(pressed, keysym));
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private void KeyScan(bool pressed, int scanCode, int keySym)
|
|
{
|
|
if (KeyHandler.handleExtras(pressed, _pressedScans, KeyHandler.ExtraScans, scanCode, KeyHandler.ModifierScans, ref _modifierKeyPressedAlone))
|
|
{
|
|
if (!pressed && _modifierKeyPressedAlone)
|
|
{
|
|
// send key up anyway
|
|
_modifierKeyPressedAlone = false;
|
|
KeyScan_(false, scanCode, keySym);
|
|
return;
|
|
}
|
|
this.Focus();
|
|
return;
|
|
}
|
|
|
|
KeyScan_(pressed, scanCode, keySym);
|
|
}
|
|
|
|
private void KeyScan_(bool pressed, int scanCode, int keySym = -1)
|
|
{
|
|
DoIfConnected(() => _vncStream.keyScanEvent(pressed, scanCode, keySym, UseQemuExtKeyEncoding));
|
|
}
|
|
|
|
#region IRemoteConsole implementation
|
|
|
|
public ConsoleKeyHandler KeyHandler { get; set; }
|
|
|
|
public Control ConsoleControl => this;
|
|
|
|
public void Activate()
|
|
{
|
|
Select();
|
|
}
|
|
|
|
public void DisconnectAndDispose()
|
|
{
|
|
Disconnect();
|
|
Dispose();
|
|
}
|
|
|
|
public void Pause()
|
|
{
|
|
_helperIsPaused = true;
|
|
_vncStream?.Pause();
|
|
}
|
|
|
|
public void UnPause()
|
|
{
|
|
_helperIsPaused = false;
|
|
_vncStream?.UnPause();
|
|
}
|
|
|
|
public void SendCAD()
|
|
{
|
|
if (_sendScanCodes)
|
|
{
|
|
KeyScan_(true, ConsoleKeyHandler.CTRL_SCAN);
|
|
KeyScan_(true, ConsoleKeyHandler.ALT_SCAN);
|
|
KeyScan_(true, ConsoleKeyHandler.DEL_SCAN);
|
|
|
|
KeyScan_(false, ConsoleKeyHandler.CTRL_SCAN);
|
|
KeyScan_(false, ConsoleKeyHandler.ALT_SCAN);
|
|
KeyScan_(false, ConsoleKeyHandler.DEL_SCAN);
|
|
}
|
|
else
|
|
{
|
|
KeySym_(true, Keys.ControlKey);
|
|
KeySym_(true, Keys.Menu);
|
|
KeySym_(true, Keys.Delete);
|
|
|
|
KeySym_(false, Keys.ControlKey);
|
|
KeySym_(false, Keys.Menu);
|
|
KeySym_(false, Keys.Delete);
|
|
}
|
|
}
|
|
|
|
public Image Snapshot()
|
|
{
|
|
while (_vncStream == null)
|
|
Thread.Sleep(100);
|
|
|
|
_vncStream.UnPause(true); //request full update
|
|
|
|
lock (_vncStream.updateMonitor)
|
|
{
|
|
Monitor.Wait(_vncStream.updateMonitor, 1000);
|
|
}
|
|
|
|
lock (_backBuffer)
|
|
{
|
|
Image image = new Bitmap(_backBuffer);
|
|
return image;
|
|
}
|
|
}
|
|
|
|
public bool SendScanCodes
|
|
{
|
|
set
|
|
{
|
|
Program.AssertOnEventThread();
|
|
if (_sendScanCodes == value)
|
|
return;
|
|
|
|
Log.InfoFormat("VNCGraphicsClient.SetSendScanCodes newSendScanCodes={0}", value);
|
|
|
|
if (!value)
|
|
{
|
|
InterceptKeys.releaseKeys();
|
|
}
|
|
else if (Focused)
|
|
{
|
|
InterceptKeys.grabKeys(KeyScan, false);
|
|
}
|
|
|
|
_sendScanCodes = value;
|
|
}
|
|
}
|
|
|
|
public bool Scaling
|
|
{
|
|
get
|
|
{
|
|
Program.AssertOnEventThread();
|
|
return _scaling;
|
|
}
|
|
set
|
|
{
|
|
Program.AssertOnEventThread();
|
|
|
|
if (_scaling == value)
|
|
return;
|
|
|
|
// it going to scaling, quickly save the old dx, dy values
|
|
if (value)
|
|
{
|
|
_oldDx = _dx;
|
|
_oldDy = _dy;
|
|
}
|
|
|
|
lock (_backBuffer)
|
|
{
|
|
_scaling = value;
|
|
SetupScaling();
|
|
}
|
|
Invalidate();
|
|
}
|
|
}
|
|
/// <summary>
|
|
/// Invoke holding backBuffer lock
|
|
/// </summary>
|
|
private void SetupScaling()
|
|
{
|
|
Program.AssertOnEventThread();
|
|
|
|
if (_frontGraphics == null)
|
|
return;
|
|
|
|
if (_scaling)
|
|
{
|
|
AutoScroll = false;
|
|
|
|
float xScale = Size.Width /
|
|
(float)(_displayBorder ? DesktopSize.Width + BORDER_PADDING * 3 : DesktopSize.Width);
|
|
float yScale = Size.Height /
|
|
(float)(_displayBorder ? DesktopSize.Height + BORDER_PADDING * 3 : DesktopSize.Height);
|
|
|
|
_scale = xScale > yScale ? yScale : xScale;
|
|
_scale = _scale > 0.01 ? _scale : (float)0.01;
|
|
|
|
_bump = (int)Math.Ceiling(1 / _scale);
|
|
|
|
// Now do the offset
|
|
|
|
_dx = (Size.Width - DesktopSize.Width * _scale) / 2;
|
|
_dy = (Size.Height - DesktopSize.Height * _scale) / 2;
|
|
|
|
Matrix transform = new Matrix();
|
|
transform.Translate(_dx, _dy);
|
|
transform.Scale(_scale, _scale);
|
|
|
|
_frontGraphics.Transform = transform;
|
|
}
|
|
else
|
|
{
|
|
_scale = 1;
|
|
_bump = 0;
|
|
|
|
if (_connected)
|
|
{
|
|
AutoScrollMinSize = new Size(
|
|
_displayBorder ? DesktopSize.Width + BORDER_PADDING + BORDER_PADDING : DesktopSize.Width,
|
|
_displayBorder ? DesktopSize.Height + BORDER_PADDING + BORDER_PADDING : DesktopSize.Height);
|
|
}
|
|
else
|
|
{
|
|
AutoScrollMinSize = new Size(0, 0);
|
|
}
|
|
|
|
AutoScroll = true;
|
|
|
|
if (Size.Height >= (_displayBorder ? DesktopSize.Height + BORDER_PADDING + BORDER_PADDING : DesktopSize.Height))
|
|
{
|
|
_dy = ((float) Size.Height - DesktopSize.Height) / 2;
|
|
}
|
|
else
|
|
{
|
|
if (_displayBorder)
|
|
{
|
|
_dy = BORDER_PADDING;
|
|
AutoScrollPosition = new Point(BORDER_PADDING, (int)_oldDy);
|
|
}
|
|
else
|
|
{
|
|
_dy = 0;
|
|
AutoScrollPosition = new Point(0, (int)_oldDy);
|
|
}
|
|
}
|
|
|
|
if (Size.Width >= (_displayBorder ? DesktopSize.Width + BORDER_PADDING + BORDER_PADDING : DesktopSize.Width))
|
|
{
|
|
_dx = ((float)Size.Width - DesktopSize.Width) / 2;
|
|
}
|
|
else
|
|
{
|
|
if (_displayBorder)
|
|
{
|
|
_dx = BORDER_PADDING;
|
|
AutoScrollPosition = new Point((int)_oldDx, BORDER_PADDING);
|
|
}
|
|
else
|
|
{
|
|
_dx = 0;
|
|
AutoScrollPosition = new Point((int)_oldDx, 0);
|
|
}
|
|
}
|
|
|
|
Matrix transform = new Matrix();
|
|
transform.Translate(_dx, _dy);
|
|
|
|
_frontGraphics.Transform = transform;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether or not to display the blue rectangle around the control when it has focus.
|
|
/// </summary>
|
|
public bool DisplayBorder
|
|
{
|
|
set
|
|
{
|
|
_displayBorder = value;
|
|
lock (_backBuffer)
|
|
{
|
|
SetupScaling();
|
|
}
|
|
Invalidate();
|
|
Update();
|
|
}
|
|
}
|
|
|
|
public Size DesktopSize { get; set; }
|
|
|
|
public Rectangle ConsoleBounds =>
|
|
_scaling
|
|
? new Rectangle((int)_dx, (int)_dy, Size.Width - 2 * (int)_dx, Size.Height - 2 * (int)_dy)
|
|
: new Rectangle((int)_dx, (int)_dy, DesktopSize.Width, DesktopSize.Height);
|
|
|
|
#endregion
|
|
}
|
|
}
|