Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add mouse click effect capability for screen recording #7622

Closed
wants to merge 10 commits into from
2 changes: 2 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_m
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
# IDE0051: Remove unused private member
dotnet_diagnostic.IDE0051.severity = silent
# spacing options
csharp_space_after_keywords_in_control_flow_statements = true

# Xml project files
[*.{csproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}]
Expand Down
87 changes: 87 additions & 0 deletions ShareX.HelpersLib/Input/MouseClickEffectForm.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;

namespace ShareX.HelpersLib
{
// transparent form where mouse click effect will be drawn
public class MouseClickEffectForm : LayeredForm
{
protected override CreateParams CreateParams
{
get
{
// set style to layered topmost transparent
var createParams = base.CreateParams;
createParams.ExStyle |= (int)(WindowStyles.WS_EX_TOPMOST | WindowStyles.WS_EX_TRANSPARENT);
return createParams;
}
}

/// <summary>
/// Draw mouse effect on given mouse position
/// </summary>
public void DrawMouseEffect(MouseEventInfo eventInfo)
{
// color will come from customizable color setting when implemented
var color = eventInfo.ButtonState == ButtonState.LeftButtonDown ? Color.Red : Color.Blue;

CenterFormToCursorPosition(eventInfo.CursorPosition);
SelectBitmap(GetCircleImage(color), 100);
}

/// <summary>
/// Clear mouse effect
/// </summary>
public void ClearMouseEffect()
{
SelectBitmap(GetEmptyImage(), 1);
}

private Bitmap GetCircleImage(Color color)
{
var bmp = new Bitmap(ClientSize.Width, ClientSize.Height);

using (Graphics g = Graphics.FromImage(bmp))
{
Brush brush = new SolidBrush(Color.FromArgb(100, color));
var diameter = 20;
// Calculate the top-left corner to center the circle
var x = (ClientSize.Width - diameter) / 2;
var y = (ClientSize.Height - diameter) / 2;

// .NET GDI+ is not precise when drawign circles this would be better off with WPF/vector-based drawing
g.FillEllipse(brush, x, y, diameter, diameter);
}

return bmp;
}

private Bitmap GetEmptyImage()
{
var backgroundImage = new Bitmap(ClientSize.Width, ClientSize.Height);
var gBackgroundImage = Graphics.FromImage(backgroundImage);

gBackgroundImage.InterpolationMode = InterpolationMode.NearestNeighbor;
gBackgroundImage.SmoothingMode = SmoothingMode.HighSpeed;
gBackgroundImage.CompositingMode = CompositingMode.SourceCopy;
gBackgroundImage.CompositingQuality = CompositingQuality.HighSpeed;
gBackgroundImage.Clear(Color.FromArgb(0, 0, 0, 0));

return backgroundImage;
}

private void CenterFormToCursorPosition(Point cursorPosition)
{
var x = cursorPosition.X - (Width / 2);
var y = cursorPosition.Y - (Height / 2);
var flags = SetWindowPosFlags.SWP_NOSIZE |
SetWindowPosFlags.SWP_NOOWNERZORDER |
SetWindowPosFlags.SWP_NOACTIVATE;

// Center the form/circle at the current mouse cursor position
NativeMethods.SetWindowPos(Handle, (IntPtr)NativeConstants.HWND_TOPMOST, x, y, 0, 0, flags);
}
}
}
86 changes: 86 additions & 0 deletions ShareX.HelpersLib/Input/MouseClickEffectManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using System;

namespace ShareX.HelpersLib
{
public class MouseClickEffectManager : IDisposable
{
private bool disposed;
private bool showEffect;
private MouseHook mouseHook;
private MouseClickEffectForm clickEffectForm;

// start click mouse effects
public void Start()
{
showEffect = true;
clickEffectForm = new MouseClickEffectForm();
mouseHook = new MouseHook();
mouseHook.OnMouseEvent += OnMouseEvent;
clickEffectForm.Show();
}

/// <summary>
/// Mouse event handler
/// </summary>
private void OnMouseEvent(MouseEventInfo eventInfo)
{
if (!showEffect)
return;

switch (eventInfo.ButtonState)
{
case ButtonState.LeftButtonDown:
clickEffectForm.DrawMouseEffect(eventInfo);
break;
case ButtonState.RightButtonDown:
clickEffectForm.DrawMouseEffect(eventInfo);
break;
default:
clickEffectForm.ClearMouseEffect();
break;
}
}

public void Pause()
{
showEffect = false;
}

public void Resume()
{
showEffect = true;
}

public void Stop()
{
showEffect = false;
Dispose();
}

public void Dispose()
{
Dispose(true);
}

protected virtual void Dispose(bool disposing)
{
if (disposed)
{
return;
}

if (disposing)
{
mouseHook.OnMouseEvent -= OnMouseEvent;
mouseHook.Dispose();
clickEffectForm.Close();
clickEffectForm.Dispose();
}

mouseHook = null;
clickEffectForm = null;

disposed = true;
}
}
}
17 changes: 17 additions & 0 deletions ShareX.HelpersLib/Input/MouseEventInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System.Drawing;

namespace ShareX.HelpersLib
{
public class MouseEventInfo
{
public ButtonState ButtonState { get; set; }
public Point CursorPosition { get; set; }
}

public enum ButtonState
{
LeftButtonDown,
RightButtonDown,
ButtonUp
}
}
77 changes: 77 additions & 0 deletions ShareX.HelpersLib/Input/MouseHook.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using System;

namespace ShareX.HelpersLib
{
public class MouseHook : IDisposable
{
private HookProc proc;
private static IntPtr hookID = IntPtr.Zero;
public delegate void MouseEventHandler(MouseEventInfo eventInfo);
public event MouseEventHandler OnMouseEvent;

public MouseHook()
{
proc = HookCallback;
hookID = SetHook(proc); // Set up global mouse hook
}

~MouseHook()
{
Dispose();
}

private static IntPtr SetHook(HookProc proc)
{
using (var curProcess = System.Diagnostics.Process.GetCurrentProcess())
using (var curModule = curProcess.MainModule)
{
return NativeMethods.SetWindowsHookEx(NativeConstants.WH_MOUSE_LL, proc, NativeMethods.GetModuleHandle(curModule.ModuleName), 0);
}
}

private IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode >= 0)
{
switch ((WindowsMessages)wParam)
{
case WindowsMessages.LBUTTONDOWN:
OnMouseEvent(new MouseEventInfo
{
ButtonState = ButtonState.LeftButtonDown,
CursorPosition = CaptureHelpers.GetCursorPosition()
});
break;
case WindowsMessages.RBUTTONDOWN:
OnMouseEvent(new MouseEventInfo
{
ButtonState = ButtonState.RightButtonDown,
CursorPosition = CaptureHelpers.GetCursorPosition()
});
break;
case WindowsMessages.LBUTTONUP:
OnMouseEvent(new MouseEventInfo
{
ButtonState = ButtonState.ButtonUp,
CursorPosition = CaptureHelpers.GetCursorPosition()
});
break;
case WindowsMessages.RBUTTONUP:
OnMouseEvent(new MouseEventInfo
{
ButtonState = ButtonState.ButtonUp,
CursorPosition = CaptureHelpers.GetCursorPosition()
});
break;
}
}

return NativeMethods.CallNextHookEx(hookID, nCode, wParam, lParam);
}

public void Dispose()
{
NativeMethods.UnhookWindowsHookEx(hookID); // Clean up hook on exit
}
}
}
10 changes: 10 additions & 0 deletions ShareX/Forms/TaskSettingsForm.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion ShareX/Forms/TaskSettingsForm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,7 @@ public TaskSettingsForm(TaskSettings hotkeySetting, bool isDefault = false)
cbScreenRecordAutoStart.Checked = nudScreenRecorderStartDelay.Enabled = TaskSettings.CaptureSettings.ScreenRecordAutoStart;
nudScreenRecorderStartDelay.SetValue((decimal)TaskSettings.CaptureSettings.ScreenRecordStartDelay);
cbScreenRecorderShowCursor.Checked = TaskSettings.CaptureSettings.ScreenRecordShowCursor;
cbScreenRecorderShowCursorEffect.Checked = TaskSettings.CaptureSettings.ScreenRecordShowCursorEffect;
cbScreenRecordTwoPassEncoding.Checked = TaskSettings.CaptureSettings.ScreenRecordTwoPassEncoding;
cbScreenRecordTransparentRegion.Checked = TaskSettings.CaptureSettings.ScreenRecordTransparentRegion;
cbScreenRecordConfirmAbort.Checked = TaskSettings.CaptureSettings.ScreenRecordAskConfirmationOnAbort;
Expand Down Expand Up @@ -1301,7 +1302,7 @@ private void btnScreenRecorderFFmpegOptions_Click(object sender, EventArgs e)
Duration = TaskSettings.CaptureSettings.ScreenRecordFixedDuration ? TaskSettings.CaptureSettings.ScreenRecordDuration : 0,
OutputPath = "output.mp4",
CaptureArea = Screen.PrimaryScreen.Bounds,
DrawCursor = TaskSettings.CaptureSettings.ScreenRecordShowCursor
DrawCursor = TaskSettings.CaptureSettings.ScreenRecordShowCursor,
};

using (FFmpegOptionsForm form = new FFmpegOptionsForm(options))
Expand Down Expand Up @@ -1349,6 +1350,11 @@ private void cbScreenRecorderShowCursor_CheckedChanged(object sender, EventArgs
TaskSettings.CaptureSettings.ScreenRecordShowCursor = cbScreenRecorderShowCursor.Checked;
}

private void cbScreenRecorderShowCursorEffect_CheckedChanged(object sender, EventArgs e)
{
TaskSettings.CaptureSettings.ScreenRecordShowCursorEffect = cbScreenRecorderShowCursorEffect.Checked;
}

private void cbScreenRecordTwoPassEncoding_CheckedChanged(object sender, EventArgs e)
{
TaskSettings.CaptureSettings.ScreenRecordTwoPassEncoding = cbScreenRecordTwoPassEncoding.Checked;
Expand Down
Loading