Skip to content

Instantly share code, notes, and snippets.

@rubyu
Created April 25, 2018 01:33
Show Gist options
  • Save rubyu/4e2f6999f9e21be0da0233befa100a03 to your computer and use it in GitHub Desktop.
Save rubyu/4e2f6999f9e21be0da0233befa100a03 to your computer and use it in GitHub Desktop.
using System.Drawing;
using System.Threading;
using System.Windows.Forms;
using System.Collections.Concurrent;
using Crevice.Threading;
using Crevice.Logging;
public class GestureStrokeOverlayManager : IDisposable
{
protected class Message {}
protected class ResetMessage : Message {}
protected class DrawMessage : Message
{
public readonly IEnumerable<Crevice.Core.Stroke.Stroke> Strokes;
public readonly IEnumerable<Point> BufferedPoints;
public DrawMessage(IEnumerable<Crevice.Core.Stroke.Stroke> strokes, IEnumerable<Point> bufferedPoints)
{
this.Strokes = strokes;
this.BufferedPoints = bufferedPoints;
}
}
class OverlayForm : Form
{
class OverlayBuffer : IDisposable
{
public readonly Rectangle Rect;
public readonly BufferedGraphics Buffer;
public OverlayBuffer(Graphics g, Rectangle rect)
{
this.Rect = rect;
this.Buffer = BufferedGraphicsManager.Current.Allocate(g, Rect);
}
public void Dispose() => Buffer.Dispose();
}
private const int WS_EX_TOOLWINDOW = 0x00000080;
private const int WS_EX_TRANSPARENT = 0x00000020;
protected override CreateParams CreateParams
{
get
{
CreateParams cp = base.CreateParams;
cp.ExStyle = cp.ExStyle | WS_EX_TOOLWINDOW | WS_EX_TRANSPARENT;
return cp;
}
}
public void InvokeProperly(MethodInvoker invoker)
{
if (InvokeRequired)
{
Invoke(invoker);
}
else
{
invoker.Invoke();
}
}
private readonly GestureStrokeOverlayManager Manager;
private readonly Graphics graphics;
private OverlayBuffer bufferA = null;
private OverlayBuffer bufferB = null;
private readonly object _lockObject = new object();
public OverlayForm(GestureStrokeOverlayManager manager)
{
this.Manager = manager;
this.graphics = CreateGraphics();
this.graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
this.graphics.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality;
InitializeComponent();
}
public void Reset() =>
InvokeProperly(() =>
{
EraseStrokeOverlay(bufferA);
EraseStrokeOverlay(bufferB);
this.TopMost = true;
this.TopLevel = true;
});
public void Draw(IEnumerable<Crevice.Core.Stroke.Stroke> strokes, IEnumerable<Point> bufferedPoints, Rectangle rect, bool bufferUpdate = false) =>
InvokeProperly(() => DrawStrokeOverlay(strokes, bufferedPoints, bufferUpdate));
private void EraseStrokeOverlay(OverlayBuffer buffer)
{
lock (_lockObject)
{
buffer?.Buffer.Dispose();
buffer = null;
buffer.Buffer.Graphics.
}
Buffer = BufferedGraphicsManager.Current.Allocate(CreateGraphics(), DisplayRectangle);
Buffer.Render();
maxRenderedStrokeId = 0;
IsEmpty = true;
}
private void DrawStrokeOverlay(IEnumerable<Crevice.Core.Stroke.Stroke> strokes, IEnumerable<Point> bufferedPoints, bool bufferUpdate)
{
if (bufferUpdate)
{
using (Verbose.PrintElapsed("Allocating new buffer"))
{
var g = CreateGraphics();
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
g.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality;
Buffer = BufferedGraphicsManager.Current.Allocate(g, DisplayRectangle);
}
}
foreach (var (v, i) in strokes.Select((v, i) => (v, i)))
{
if (Manager.MessageQueue.Count > 0)
{
Verbose.Print("DrawStrokeOverlay was canceled.");
return;
}
if (i < maxRenderedStrokeId)
{
continue;
}
var pen = v != strokes.Last() ? Manager.NormalLinePen : Manager.NewLinePen;
Verbose.Print($"Drawing Stroke({i + 1}/{strokes.Count()}) of {v.Points.Count} points.");
Buffer.Graphics.DrawLines(pen, v.Points.Select(p => new Point(p.X - Location.X, p.Y - Location.Y)).ToArray());
}
if (bufferedPoints.Any())
{
var points = (strokes.Any() && strokes.Last().Points.Any() ? new List<Point> { strokes.Last().Points.Last() } : new List<Point>()).Concat(bufferedPoints).Append(Cursor.Position);
Verbose.Print($"Drawing undetermined stroke of {points.Count()} points.");
Buffer.Graphics.DrawLines(Manager.UndeterminedLinePen, points.Select(p => new Point(p.X - Location.X, p.Y - Location.Y)).ToArray());
}
using (Verbose.PrintElapsed("Rendering"))
{
Buffer.Render();
maxRenderedStrokeId = strokes.Count() - 1;
}
IsEmpty = false;
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
bufferA?.Buffer.Render();
bufferB?.Buffer.Render();
}
private System.ComponentModel.IContainer components = null;
protected override void Dispose(bool disposing)
{
if (disposing)
{
components?.Dispose();
bufferA?.Dispose();
bufferB?.Dispose();
graphics?.Dispose();
}
base.Dispose(disposing);
}
private void InitializeComponent()
{
this.SuspendLayout();
this.AutoScaleDimensions = new System.Drawing.SizeF(10F, 18F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(1, 1);
this.Name = "GestureStrokeOverlayForm";
this.Text = this.Name;
this.StartPosition = System.Windows.Forms.FormStartPosition.Manual;
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None;
this.ShowIcon = false;
this.ShowInTaskbar = false;
this.TopMost = true;
this.TopLevel = true;
this.BackColor = Color.Black;
if (!Manager.Debug)
{
this.TransparencyKey = Color.Black;
}
this.DoubleBuffered = true;
this.ResumeLayout(false);
}
}
public static Color DefaultNormalLineColor => Color.FromArgb(51, 153, 255);
public static Color DefaultNewLineColor => Color.FromArgb(255, 51, 153);
public static Color DefaultUndeterminedLineColor => Color.FromArgb(255, 141, 75);
public static float DefaultLineWidth => 4.0f;
public static int DefaultMinimalOverlaySize => 1200;
public readonly Pen NormalLinePen;
public readonly Pen NewLinePen;
public readonly Pen UndeterminedLinePen;
public readonly float LineWidth;
public readonly int MinimalOverlaySize;
public readonly bool Debug;
public void Reset()
{
try {
MessageQueue.Add(new ResetMessage());
}
catch(InvalidOperationException) {}
}
public void Draw(IEnumerable<Crevice.Core.Stroke.Stroke> strokes, IEnumerable<Point> bufferedPoints)
{
try {
MessageQueue.Add(new DrawMessage(strokes, bufferedPoints));
}
catch(InvalidOperationException) {}
}
protected readonly BlockingCollection<Message> MessageQueue = new BlockingCollection<Message>();
private readonly OverlayForm smallOverlay;
private readonly OverlayForm largeOverlay;
public GestureStrokeOverlayManager(
Color? normalLineColor = null,
Color? newLineColor = null,
Color? undeterminedLineColor = null,
float? lineWidth = null,
int? minimalOverlaySize = null,
bool debug = false)
{
LineWidth = lineWidth ?? DefaultLineWidth;
NormalLinePen = new Pen(normalLineColor ?? DefaultNormalLineColor, LineWidth);
NewLinePen = new Pen(newLineColor ?? DefaultNewLineColor, LineWidth);
UndeterminedLinePen = new Pen(undeterminedLineColor ?? DefaultUndeterminedLineColor, LineWidth);
MinimalOverlaySize = minimalOverlaySize ?? (debug ? 200 : DefaultMinimalOverlaySize);
Debug = debug;
smallOverlay = new OverlayForm(this);
largeOverlay = new OverlayForm(this);
RunMessageProcessTask();
}
public void Run()
{
smallOverlay.Show();
largeOverlay.Show();
}
protected Rectangle StrokesToRect(IEnumerable<Crevice.Core.Stroke.Stroke> strokes)
{
var points = strokes.SelectMany(x => x.Points);
var minX = points.Select(x => x.X).Min();
var minY = points.Select(x => x.Y).Min();
return new Rectangle(minX, minY, points.Select(x => x.X).Max() - minX, points.Select(x => x.Y).Max() - minY);
}
private bool RectIsContained(Rectangle rect, OverlayForm form) =>
form.Location.X < rect.X - LineWidth &&
form.Location.Y < rect.Y - LineWidth &&
rect.X + rect.Width + LineWidth * 2 < form.Location.X + form.Size.Width &&
rect.Y + rect.Height + LineWidth * 2 < form.Location.Y + form.Size.Height;
private bool RectIsIntersected(Rectangle rect, Screen s) =>
(s.Bounds.X <= rect.X && rect.X <= s.Bounds.X + s.Bounds.Width ||
s.Bounds.X <= rect.X + rect.Width && rect.X + rect.Width <= s.Bounds.X + s.Bounds.Width) &&
(s.Bounds.Y <= rect.Y && rect.Y <= s.Bounds.Y + s.Bounds.Height ||
s.Bounds.Y <= rect.Y + rect.Height && rect.Y + rect.Height <= s.Bounds.Y + s.Bounds.Height) ||
(rect.X < s.Bounds.X || s.Bounds.X + s.Bounds.Width < rect.X &&
rect.Y < s.Bounds.Y || s.Bounds.Y + s.Bounds.Height < rect.Y);
private void RunMessageProcessTask() =>
Task.Factory.StartNew(() =>
{
try
{
foreach (var message in MessageQueue.GetConsumingEnumerable())
{
if (_disposed) break;
try {
if (message is ResetMessage)
{
Verbose.Print("[GestureStrokeOverlayManager.ResetMessage]");
smallOverlay.Reset();
largeOverlay.Minimize();
}
else if (message is DrawMessage dm)
{
Verbose.Print("[GestureStrokeOverlayManager.DrawMessage]");
var rect = StrokesToRect(dm.Strokes);
if (smallOverlay.IsEmpty &&
MinimalOverlaySize > rect.Width &&
MinimalOverlaySize > rect.Height)
{
Verbose.Print("Mode: 0 (SmallOverlay)");
smallOverlay.InvokeProperly(() =>
{
Verbose.Print($"Initialzing SmallOverlay ...");
smallOverlay.Location = new Point(rect.X - MinimalOverlaySize / 2, rect.Y - MinimalOverlaySize / 2);
smallOverlay.Size = new Size(MinimalOverlaySize, MinimalOverlaySize);
Verbose.Print($"Location: {smallOverlay.Location}");
Verbose.Print($"Size: {smallOverlay.Size}");
});
smallOverlay.Draw(dm.Strokes, dm.BufferedPoints, rect, bufferUpdate: true);
}
else if (!smallOverlay.IsEmpty && RectIsContained(rect, smallOverlay))
{
Verbose.Print("Mode: 0 (SmallOverlay)");
smallOverlay.Draw(dm.Strokes, dm.BufferedPoints, rect, bufferUpdate: false);
}
else
{
Verbose.Print("Mode: 1 (LargeOverlay)");
var screens = Screen.AllScreens.Where(s => RectIsIntersected(rect, s));
foreach (var (s, i) in screens.Select((s, i) => (s, i)))
{
Verbose.Print($"Screen({i + 1}/{screens.Count()}) | {s}");
}
var overlayLocation = new Point(screens.Select(s => s.Bounds.X).Min(), screens.Select(s => s.Bounds.Y).Min());
var overlaySize = new Size(screens.Select(s => s.Bounds.X + s.Bounds.Width).Max() - overlayLocation.X, screens.Select(s => s.Bounds.Y + s.Bounds.Height).Max() - overlayLocation.Y);
if (largeOverlay.Location != overlayLocation ||
largeOverlay.Size != overlaySize)
{
largeOverlay.Reset();
largeOverlay.InvokeProperly(() =>
{
Verbose.Print($"Initialzing LargeOverlay ...");
largeOverlay.Location = overlayLocation;
largeOverlay.Size = overlaySize;
Verbose.Print($"Location: {largeOverlay.Location}");
Verbose.Print($"Size: {largeOverlay.Size}");
});
}
if (largeOverlay.IsEmpty)
{
largeOverlay.Draw(dm.Strokes, dm.BufferedPoints, rect, bufferUpdate: true);
}
else
{
largeOverlay.Draw(dm.Strokes, dm.BufferedPoints, rect, bufferUpdate: false);
smallOverlay.Reset();
}
}
}
}
catch(Exception ex)
{
Verbose.Print($"Error on MessageProcessTask: {ex.ToString()}");
}
}
smallOverlay.InvokeProperly(() =>
{
smallOverlay.Close();
smallOverlay.Dispose();
});
smallOverlay.InvokeProperly(() =>
{
largeOverlay.Close();
largeOverlay.Dispose();
});
NormalLinePen.Dispose();
NewLinePen.Dispose();
UndeterminedLinePen.Dispose();
}
catch (OperationCanceledException) {}
});
internal bool _disposed { get; private set; } = false;
public void Dispose()
{
if (_disposed) return;
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
_disposed = true;
if (disposing)
{
MessageQueue.CompleteAdding();
}
}
~GestureStrokeOverlayManager() => Dispose(false);
}
GestureStrokeOverlayManager gestureStrokeOverlayManager = null;
Config.Callback.MachineStart += (sender, e) => {
MainForm.BeginInvoke((MethodInvoker)(() => {
gestureStrokeOverlayManager = new GestureStrokeOverlayManager(debug: false);
gestureStrokeOverlayManager.Run();
}));
};
Config.Callback.MachineStop += (sender, e) => {
MainForm.BeginInvoke((MethodInvoker)(() => {
gestureStrokeOverlayManager?.Dispose();
}));
};
Config.Callback.StrokeUpdated += (sender, e) => {
gestureStrokeOverlayManager?.Draw(e.Strokes, (sender as Crevice.Core.Stroke.StrokeWatcher).GetBufferedPoints());
};
Config.Callback.StrokeReset += (sender, e) => {
gestureStrokeOverlayManager?.Reset();
};
Config.Callback.StateChanged += (sender, e) => {
gestureStrokeOverlayManager?.Reset();
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment