/* Copyright (c) Citrix Systems, Inc. * All rights reserved. * * 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 static readonly log4net.ILog Log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); public event Action ErrorOccurred = null; public event EventHandler ConnectionSuccess = null; private VNCStream vncStream = null; /// /// connected implies that vncStream is non-null and ready to be used. /// private volatile bool connected = false; public bool Connected { get { return connected; } } /// /// 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. /// private volatile bool terminated = false; public bool Terminated { get { return terminated; } } private CustomCursor RemoteCursor = null; private CustomCursor LocalCursor = new CustomCursor(XenAdmin.Properties.Resources.vnc_local_cursor, 2, 2); /// /// 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. /// private Bitmap backBuffer = null; /// /// 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). /// private volatile bool backBufferInteresting = false; /// /// A Graphics object onto backBuffer. Access to this field must always be locked under backBuffer. /// This field will be set to null in Dispose. /// private Graphics backGraphics = null; /// /// Graphics on actual screen. Access to this field must always be locked under backBuffer. /// This field will be set to null in Dispose. /// private Graphics frontGraphics = null; private Object mouseEventLock = new Object(); private Rectangle damage = Rectangle.Empty; private float scale; private float dx; private float dy; private float oldDx = 0; private float oldDy = 0; private int Bump; private bool scaling = false; private bool sendScanCodes = true; private bool useSource = false; public bool UseSource { set { useSource = value; } } public bool talkingToVNCTerm { get { return !sendScanCodes && useSource; } } public string UUID { get { return sourceVM.uuid; } } private XenAPI.VM sourceVM; public XenAPI.VM SourceVM { get { return sourceVM; } set { sourceVM = value; } } public string VmName { get { return sourceVM.name_label; } } public object Console; public event EventHandler DesktopResized = null; 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(); this.vncStream = new VNCStream(this, stream, helperIsPaused); this.vncStream.ErrorOccurred += this.OnError; this.vncStream.ConnectionSuccess += this.vncStream_ConnectionSuccess; this.connected = true; this.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; this.connected = false; if (ErrorOccurred != null) ErrorOccurred(this, e); } /// /// Nothrow guarantee. /// public void Disconnect() { connected = false; terminated = true; if (vncStream != null) { vncStream.ErrorOccurred -= OnError; vncStream.ConnectionSuccess -= vncStream_ConnectionSuccess; } VNCStream s = vncStream; vncStream = null; if (s != null) { s.Close(); } } private bool RedirectingClipboard() { return XenAdmin.Properties.Settings.Default.ClipboardAndPrinterRedirection; } private static bool handlingChange = false; private bool updateClipboardOnFocus = false; private void ClipboardChanged(object sender, EventArgs args) { Program.AssertOnEventThread(); if (!RedirectingClipboard()) return; try { if (!handlingChange && connected) { if (Focused) setConsoleClipboard(); else updateClipboardOnFocus = true; } } catch (System.IO.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; } } /// /// Records damage to the screen. /// 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 (this.damage.IsEmpty) { this.damage = r; } else { this.damage = Rectangle.Union(this.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(); //GraphicsUtils.startTime(); 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 wierd 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 { } return; } } //GraphicsUtils.endTime("drawImage"); } #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); class CustomCursor { private Cursor cursor = null; private IntPtr handle = IntPtr.Zero; internal CustomCursor(Bitmap bitmap, int x, int y) { ICONINFO iconInfo = new ICONINFO(); iconInfo.fIcon = false; iconInfo.xHotspot = x; iconInfo.yHotspot = y; iconInfo.hbmMask = bitmap.GetHbitmap(); iconInfo.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 { } finally { handle = IntPtr.Zero; } } public Cursor Cursor { get { return cursor; } } } public void ClientSetCursor(Bitmap image, int x, int y, int width, int height) { Program.AssertOffEventThread(); if (RemoteCursor != null) RemoteCursor.Dispose(); RemoteCursor = new CustomCursor(image, x, y); Program.Invoke(this, delegate() { if (cursorOver) this.Cursor = RemoteCursor.Cursor; }); } #endregion public void ClientCopyRectangle(int x, int y, int width, int height, int dx, int dy) { Program.AssertOffEventThread(); //GraphicsUtils.startTime(); Damage(dx, dy, width, height); lock (backBuffer) { if (backGraphics != null) GraphicsUtils.copyRect(backBuffer, x, y, width, height, backGraphics, dx, dy); } //GraphicsUtils.endTime("copyRectangle"); } public void ClientFillRectangle(int x, int y, int width, int height, Color color) { Program.AssertOffEventThread(); //GraphicsUtils.startTime(); Damage(x, y, width, height); lock (backBuffer) { if (backGraphics != null) { using (SolidBrush backBrush = new SolidBrush(color)) { backGraphics.FillRectangle(backBrush, x, y, width, height); } } } //GraphicsUtils.endTime("FillRect"); } public void ClientFrameBufferUpdate() { Program.AssertOffEventThread(); //GraphicsUtils.startTime(); lock (backBuffer) { try { if (!damage.IsEmpty) { if (frontGraphics != null) frontGraphics.DrawImage(backBuffer, damage, damage, GraphicsUnit.Pixel); damage = Rectangle.Empty; } backBufferInteresting = true; } catch { } } /* * If theres a pending mouse event, send it. * Also reset the mouse event counters */ lock (this.mouseEventLock) { if (this.pending != null) { mouseEvent(this.pendingState, this.pending.X, this.pending.Y); this.pending = null; } this.mouseMoved = 0; this.mouseNotMoved = 0; } //GraphicsUtils.endTime("franeBufferUpdate"); } public void ClientBell() { } private String clipboardStash = ""; public void ClientCutText(String text) { Program.AssertOffEventThread(); if (talkingToVNCTerm) { // Lets translate from unix line endings to windows ones... clipboardStash = toWindowsLineEndings(text); } else { if (!RedirectingClipboard()) return; Program.Invoke(this, delegate() { if (Clipboard.ContainsText() && Clipboard.GetText() == text) return; Clip.SetClipboardText(text); }); } } // This is a cut-and-paste of Helpers.toWindowsLineEndings. This avoids // the standalone VNC control depending upon Helpers. private static string toWindowsLineEndings(string input) { return Regex.Replace(input, "\r?\n", "\r\n"); } 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, delegate() { Bitmap old_back_buffer; lock (backBuffer) { if (width <= 0) width = 1; if (height <= 0) height = 1; if (width == DesktopSize.Width && height == DesktopSize.Height) { return; } old_back_buffer = backBuffer; backGraphics.Dispose(); frontGraphics.Dispose(); frontGraphics = CreateGraphics(); setupGraphicsOptions(frontGraphics); Bitmap new_back_buffer = new Bitmap(width, height, frontGraphics); backGraphics = Graphics.FromImage(new_back_buffer); 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 = new_back_buffer; } lock (backBuffer) { SetupScaling(); } Invalidate(); Update(); old_back_buffer.Dispose(); OnDesktopResized(); }); } private void OnDesktopResized() { Program.AssertOnEventThread(); if (DesktopResized != null) DesktopResized(this, null); } /** * Overridden Events */ protected override void OnScroll(ScrollEventArgs se) { base.OnScroll(se); if (!this.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) { //base.OnPaintBackground(e); } protected override void OnPaint(PaintEventArgs e) { Program.AssertOnEventThread(); //base.OnPaint(e); if (this.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;//Math.Max(displayBorder ? BORDER_PADDING : 0,(int)Math.Ceiling((this.Size.Width - (this.DesktopSize.Width * this.scale)) / 2)); Graphics g = e.Graphics; //GraphicsUtils.startTime(); lock (backBuffer) { if (frontGraphics != null) frontGraphics.DrawImageUnscaled(backBuffer, 0, 0); } //GraphicsUtils.endTime("OnPaint"); 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); /* * Draw the focus rect */ /*if (Focused) { DrawFocusRect(e); }*/ } 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 int currentMouseState = 0; /* * 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 = 0; private volatile int mouseNotMoved = 0; private const int MOUSE_EVENTS_BEFORE_UPDATE = 2; private const int MOUSE_EVENTS_DROPPED = 5; // should this be proportional to bandwidth? private MouseEventArgs pending = null; private int pendingState = 0; private bool cursorOver = false; private int last_state = 0; private void mouseEvent(int state, int x, int y) { DoIfConnected(delegate() { if (talkingToVNCTerm && last_state == 4 && state == 0) { ShowPopupMenu(x, y); } else if (this.scaling) { this.vncStream.pointerEvent(state, (int)((x - dx) / this.scale), (int)((y - dy) / this.scale)); } else { this.vncStream.pointerEvent(state, x - (int)dx, y - (int)dy); } last_state = state; }); } private void ShowPopupMenu(int x, int y) { ToolStripDropDownMenu popupMenu = new ToolStripDropDownMenu(); ToolStripMenuItem copyItem = new ToolStripMenuItem(Messages.COPY); copyItem.Image = XenAdmin.Properties.Resources.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 = XenAdmin.Properties.Resources.paste_16; pasteItem.Click += pasteItem_Click; popupMenu.Items.Add(pasteItem); } popupMenu.Show(this, x, y); } void copyItem_Click(object sender, EventArgs e) { System.Diagnostics.Trace.Assert(talkingToVNCTerm); Program.AssertOnEventThread(); if (clipboardStash != "") { Clip.SetClipboardText(clipboardStash); } } 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); } } public void DisableMenuShortcuts() { Program.MainWindow.MenuShortcuts = false; } public void EnableMenuShortcuts() { Program.MainWindow.MenuShortcuts = true; } protected override void OnGotFocus(EventArgs e) { Program.AssertOnEventThread(); base.OnGotFocus(e); DisableMenuShortcuts(); if (sendScanCodes) { InterceptKeys.grabKeys(this.keyScan, false); } if (updateClipboardOnFocus) setConsoleClipboard(); /*Invalidate(); Update();*/ } protected override void OnLostFocus(EventArgs e) { Program.AssertOnEventThread(); base.OnLostFocus(e); EnableMenuShortcuts(); InterceptKeys.releaseKeys(); cursorOver = false; Cursor = Cursors.Default; //Release any held keys DoIfConnected(delegate() { foreach (Keys key in pressedKeys) { // This won't release composite key events atm. int sym = KeyMap.translateKey(ConsoleKeyHandler.GetSimpleKey(key)); if (sym > 0) this.vncStream.keyCodeEvent(false, sym); } foreach (int key in pressedScans) { this.vncStream.keyScanEvent(false, key, -1, UseQemuExtKeyEncoding); } }); this.pressedKeys = new Set(); this.pressedScans = new Set(); /*Invalidate(); Update();*/ } /* We don't want this functionality anymore (see CA-39469 Console window loses focus when alt-tab-ing) protected override void WndProc(ref Message m) { base.WndProc(ref m); // If the user alt-tabs to a different window, make sure when they come back that // focus is NOT given to the VNC control, since this is confusing. // Low-level intervention required: listen for the 'gain focus event'. // See http://msdn2.microsoft.com/en-us/library/ms646282.aspx and // http://www.pinvoke.net/default.aspx/Enums/WindowsMessages.html if (m.Msg == XenAdmin.Core.Win32.WM_SETFOCUS) { // m.WParam gives us the handle of the window that had focus immediately before // this control was granted focus. If it was from a window outside the app, // then it is set to zero (or so it seems if (m.WParam.ToInt64() == 0L) { // Divert focus to a parent control if (this.Parent.Parent != null) { // Always executed, fingers crossed this.Parent.Parent.Focus(); } } } }*/ 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 (this.mouseEventLock) { if (this.mouseMoved < MOUSE_EVENTS_BEFORE_UPDATE) { this.mouseMoved++; mouseEvent(currentMouseState, e.X, e.Y); } else if (this.mouseNotMoved > MOUSE_EVENTS_DROPPED) { this.mouseMoved = 0; this.mouseNotMoved = 0; } else { this.mouseNotMoved++; this.pendingState = currentMouseState; this.pending = e; } } } else { cursorOver = false; Cursor = Cursors.Default; } } } /* * We're not going to buffer any clicks etc. * Don't forget to clear any pending moves. */ protected override void OnMouseDown(MouseEventArgs e) { //added this line to track mouse clicks for automatic switch to RDP //if there are any issues with focu start looking here base.OnMouseDown(e); this.Focus(); this.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) { this.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(delegate() { this.vncStream.pointerWheelEvent((int)((e.X - dx) / this.scale), (int)((e.Y - dy) / this.scale), e.Delta * -SystemInformation.MouseWheelScrollLines / 120); }); } } protected override void OnMouseLeave(EventArgs e) { Cursor = Cursors.Default; cursorOver = false; base.OnMouseLeave(e); this.currentMouseState = 0; this.mouseMoved = 0; } private Set pressedKeys = new Set(); private Set pressedScans = new Set(); private bool modifierKeyPressedAlone = false; protected override bool ProcessTabKey(bool forward) { /*if (sendScanCodes) return true; DoIfConnected((MethodInvoker)delegate() { this.Keysym(true, Keys.Tab); this.Keysym(false, Keys.Tab); });*/ return true; } private void DoIfConnected(MethodInvoker methodInvoker) { if (connected) { try { methodInvoker(); } catch (System.IO.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) { const int WM_KEYDOWN = 0x100; const int WM_SYSKEYDOWN = 0x104; bool down = ((msg.Msg == WM_KEYDOWN) || (msg.Msg == WM_SYSKEYDOWN)); //System.Console.WriteLine(keyData + " " + down); Keys key = keyData; if ((key & Keys.Control) == Keys.Control) key = key & ~Keys.Control; if ((key & Keys.Alt) == Keys.Alt) key = key & ~Keys.Alt; if ((key & Keys.Shift) == Keys.Shift) key = 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 OnKeyDown(KeyEventArgs e) { //added this line to track key presses for automatic switch to RDP //if there are any issues with focu start looking here base.OnKeyDown(e); //e.Handled = Keysym(true, e.KeyCode); } 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 bool altGrReleaseSent; 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_(pressed, key); } this.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 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(delegate() { this.vncStream.keyCodeEvent(pressed, keysym); }); return true; } else { return false; } } public 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_(pressed, scanCode, keySym); return; } this.Focus(); return; } keyScan_(pressed, scanCode, keySym); } private void keyScan_(bool pressed, int scanCode) { keyScan_(pressed, scanCode, -1); } private void keyScan_(bool pressed, int scanCode, int keySym) { DoIfConnected(delegate() { this.vncStream.keyScanEvent(pressed, scanCode, keySym, UseQemuExtKeyEncoding); }); } public void Clear() { ThreadPool.QueueUserWorkItem(new WaitCallback(delegate(Object o) { ClientFillRectangle(0, 0, DesktopSize.Width, DesktopSize.Height, Color.Black); }), null); } #region IRemoteConsole implementation public ConsoleKeyHandler KeyHandler { get; set; } public Control ConsoleControl { get { return this; } } public void Activate() { this.Select(); } public void DisconnectAndDispose() { Disconnect(); Dispose(); } private bool helperIsPaused = true; public void Pause() { helperIsPaused = true; if (this.vncStream != null) this.vncStream.Pause(); } public void Unpause() { helperIsPaused = false; if (this.vncStream != null) this.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) { //if (!backBufferInteresting) // return null; Image image = new Bitmap(backBuffer); //Graphics graphics = Graphics.FromImage(image); //setupGraphicsOptions(graphics); //graphics.DrawImage(backBuffer, 0, 0, x, y); //graphics.Dispose(); return image; } } public bool SendScanCodes { set { Program.AssertOnEventThread(); SetSendScanCodes(value); } } private void SetSendScanCodes(bool newSendScanCodes) { Program.AssertOnEventThread(); if (sendScanCodes == newSendScanCodes) return; Log.InfoFormat("VNCGraphicsClient.SetSendScanCodes newSendScanCodes={0}", newSendScanCodes); if (!newSendScanCodes) { InterceptKeys.releaseKeys(); } else if (Focused) { InterceptKeys.grabKeys(this.keyScan, false); } sendScanCodes = newSendScanCodes; } public bool Scaling { get { Program.AssertOnEventThread(); return this.scaling; } set { Program.AssertOnEventThread(); if (scaling == value) return; // it going to scaling, quickly save the old dx, dy values if (value) { this.oldDx = dx; this.oldDy = dy; } lock (this.backBuffer) { this.scaling = value; this.SetupScaling(); } this.Invalidate(); } } /// /// Invoke holding backBuffer lock /// private void SetupScaling() { Program.AssertOnEventThread(); if (frontGraphics == null) return; if (this.scaling) { this.AutoScroll = false; float xScale = this.Size.Width / (float)(displayBorder ? this.DesktopSize.Width + BORDER_PADDING * 3 : this.DesktopSize.Width); float yScale = this.Size.Height / (float)(displayBorder ? this.DesktopSize.Height + BORDER_PADDING * 3 : this.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 = (this.Size.Width - (this.DesktopSize.Width * scale)) / 2; dy = (this.Size.Height - (this.DesktopSize.Height * scale)) / 2; Matrix transform = new Matrix(); transform.Translate(dx, dy); transform.Scale(scale, scale); this.frontGraphics.Transform = transform; } else { this.scale = 1; this.Bump = 0; if (this.connected) { this.AutoScrollMinSize = new Size( displayBorder ? DesktopSize.Width + BORDER_PADDING + BORDER_PADDING : DesktopSize.Width, displayBorder ? DesktopSize.Height + BORDER_PADDING + BORDER_PADDING : DesktopSize.Height); } else { this.AutoScrollMinSize = new Size(0, 0); } // The change of AutoScrollMinSize can trigger a resize event, which in turn can trigger // scaling to be turned off. If this happens, restart this calculation altogether. if (scaling) { SetupScaling(); return; } this.AutoScroll = true; if (this.Size.Height >= (displayBorder ? this.DesktopSize.Height + BORDER_PADDING + BORDER_PADDING : DesktopSize.Height)) { this.dy = (this.Size.Height - this.DesktopSize.Height) / 2; } else { if (displayBorder) { this.dy = BORDER_PADDING; this.AutoScrollPosition = new Point(BORDER_PADDING, (int)this.oldDy); } else { this.dy = 0; this.AutoScrollPosition = new Point(0, (int)this.oldDy); } } if (this.Size.Width >= (displayBorder ? this.DesktopSize.Width + BORDER_PADDING + BORDER_PADDING : DesktopSize.Width)) { this.dx = (this.Size.Width - this.DesktopSize.Width) / 2; } else { if (displayBorder) { this.dx = BORDER_PADDING; this.AutoScrollPosition = new Point((int)this.oldDx, BORDER_PADDING); } else { this.dx = 0; this.AutoScrollPosition = new Point((int)this.oldDx, 0); } } Matrix transform = new Matrix(); transform.Translate(dx, dy); this.frontGraphics.Transform = transform; } } private bool displayBorder = true; /// /// Whether or not to display the blue rectangle around the control when it has focus. /// public bool DisplayBorder { set { displayBorder = value; lock (backBuffer) { SetupScaling(); } this.Invalidate(); this.Update(); } } public Size DesktopSize { get; set; } public Rectangle ConsoleBounds { get { return 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 } }