Skip to content

Commit

Permalink
Improvements (#39)
Browse files Browse the repository at this point in the history
* Initial app component dependencies.

* Remove ClipboardService manual startup initialization.

* Improved and tested "Ctrl + V" hotkey emulation.

* Simplified P/Invoke services.

* Code smell fix test.

* revert test
  • Loading branch information
Tum4ik authored Jun 13, 2023
1 parent 943f7fb commit 1337299
Show file tree
Hide file tree
Showing 16 changed files with 501 additions and 172 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.11.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.2" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.2.0">
<PackageReference Include="coverlet.collector" Version="6.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using System.Windows.Input;
using Tum4ik.JustClipboardManager.Data.Models;
using Tum4ik.JustClipboardManager.Services;
using Tum4ik.JustClipboardManager.Services.PInvoke;
using Tum4ik.JustClipboardManager.Services.PInvoke.ParameterModels;

namespace Tum4ik.JustClipboardManager.UnitTests.Services;
public class PasteServiceTests
{
private readonly Mock<IClipboardService> _clipboardServiceMock = new();
private readonly Mock<IUser32DllService> _user32DllMock = new();
private readonly PasteService _testeeService;

public PasteServiceTests()
{
_testeeService = new(_clipboardServiceMock.Object, _user32DllMock.Object);
}


[Fact]
internal void PasteData_DataIsEmpty_NothingToDo()
{
_testeeService.PasteData(nint.Zero, new List<FormattedDataObject>());
_clipboardServiceMock.VerifyNoOtherCalls();
_user32DllMock.VerifyNoOtherCalls();
}


[Fact]
internal void PasteData_DataIsPresent_DataIsInsertedIntoClipboardAndCtrlVHotkeyIsEmulated()
{
const nint TargetWindowPtr = 42;
var data = new List<FormattedDataObject>
{
new()
{
Data = Array.Empty<byte>(),
DataDotnetType = "int",
Format = "number",
FormatOrder = 0
}
};

_testeeService.PasteData(TargetWindowPtr, data);

_clipboardServiceMock.Verify(cs => cs.Paste(data), Times.Once);
_user32DllMock.Verify(u32 => u32.SetForegroundWindow(TargetWindowPtr), Times.Once);
_user32DllMock.Verify(u32 => u32.SetFocus(TargetWindowPtr), Times.Once);
_user32DllMock.Verify(u32 => u32.SendInput(4, IsCtrlVInput(), InputStructSize()), Times.Once);
}


private static INPUT[] IsCtrlVInput()
{
return Match.Create<INPUT[]>(inputs =>
{
var lengthCondition = inputs.Length == 4;
var input0Condition = inputs[0].type == INPUTTYPE.INPUT_KEYBOARD
&& inputs[0].data.ki.wVk == KeyInterop.VirtualKeyFromKey(Key.LeftCtrl)
&& inputs[0].data.ki.dwFlags == default;
var input1Condition = inputs[1].type == INPUTTYPE.INPUT_KEYBOARD
&& inputs[1].data.ki.wVk == KeyInterop.VirtualKeyFromKey(Key.V)
&& inputs[1].data.ki.dwFlags == default;
var input2Condition = inputs[2].type == INPUTTYPE.INPUT_KEYBOARD
&& inputs[2].data.ki.wVk == KeyInterop.VirtualKeyFromKey(Key.V)
&& inputs[2].data.ki.dwFlags == KEYEVENT.KEYEVENTF_KEYUP;
var input3Condition = inputs[3].type == INPUTTYPE.INPUT_KEYBOARD
&& inputs[3].data.ki.wVk == KeyInterop.VirtualKeyFromKey(Key.LeftCtrl)
&& inputs[3].data.ki.dwFlags == KEYEVENT.KEYEVENTF_KEYUP;
return lengthCondition && input0Condition && input1Condition && input2Condition && input3Condition;
});
}

private static unsafe int InputStructSize()
{
return Match.Create<int>(size => size == sizeof(INPUT));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsPublishable>false</IsPublishable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

<ItemGroup>
Expand All @@ -22,14 +23,14 @@

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.11.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.2" />
<PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.2.0">
<PackageReference Include="coverlet.collector" Version="6.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
Expand Down
14 changes: 4 additions & 10 deletions Tum4ik.JustClipboardManager/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -134,11 +134,6 @@ protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);

if (RestartAfterCrashCount < 3)
{
throw new Exception("test test");
}

var configuration = Container.Resolve<IConfiguration>();
AppCenter.Start(configuration["MicrosoftAppCenterSecret"], typeof(Crashes), typeof(Analytics));

Expand All @@ -151,7 +146,6 @@ protected override void OnStartup(StartupEventArgs e)
RemoveOldClips();
var trayIcon = Container.Resolve<TrayIcon>();
var hookService = Container.Resolve<GeneralHookService>();
var clipboardService = Container.Resolve<IClipboardService>();
}


Expand All @@ -178,7 +172,7 @@ protected override void RegisterTypes(IContainerRegistry containerRegistry)
.RegisterGeneratedWrappers()
.RegisterDatabase()
.RegisterSingleton<IDialogService, ExtendedDialogService>()
.RegisterSingleton<IUser32DllService, User32DllService>()
.RegisterSingleton<IUser32DllService, User32DllService>()
.RegisterSingleton<ISHCoreDllService, SHCoreDllService>()
.RegisterSingleton<IKernel32DllService, Kernel32DllService>()
.RegisterInstance<IAppResourcesService>(new AppResourcesService(Resources))
Expand All @@ -201,9 +195,9 @@ protected override void RegisterTypes(IContainerRegistry containerRegistry)
return new GitHubClient(new ProductHeaderValue("JustClipboardManager", infoService.InformationalVersion));
})
.Register<WshShell, WshShellWrapper>()
.Register<IShortcutService, ShortcutService>()
.RegisterShell<TrayIcon, TrayIconViewModel>()
.RegisterShell<PasteWindow, PasteWindowViewModel>();
.Register<IShortcutService, ShortcutService>()
.RegisterShell<TrayIcon, TrayIconViewModel>()
.RegisterShell<PasteWindow, PasteWindowViewModel>();

containerRegistry.RegisterDialogWindow<MainDialogWindow>(WindowNames.MainAppWindow);
containerRegistry.RegisterDialogWindow<SimpleDialogWindow>(WindowNames.SimpleDialogWindow);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
using System.Collections.Immutable;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Tum4ik.JustClipboardManager.Data.Models;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Diagnostics;
using System.Reflection;
using System.Windows;
using Microsoft.EntityFrameworkCore;
Expand All @@ -13,31 +12,31 @@
namespace Tum4ik.JustClipboardManager.Extensions;
internal static class ContainerRegistryExtensions
{
/// <summary>
/// Registers the shell element (for example <see cref="Window"/>) with its view model.
/// </summary>
/// <typeparam name="TView"></typeparam>
/// <typeparam name="TViewModel"></typeparam>
/// <param name="services"></param>
/// <param name="viewLifetime"></param>
/// <returns>The <see cref="IContainerRegistry"/>.</returns>
public static IContainerRegistry RegisterShell<TView, TViewModel>(
this IContainerRegistry services
)
where TView : FrameworkElement, new()
where TViewModel : notnull
{
services.Register<TViewModel>();
services.RegisterSingleton<TView>(cp =>
{
var viewModel = cp.Resolve<TViewModel>();
var view = new TView { DataContext = viewModel };
return view;
});
return services;
}


/// <summary>
/// Registers the shell element (for example <see cref="Window"/>) with its view model.
/// </summary>
/// <typeparam name="TView"></typeparam>
/// <typeparam name="TViewModel"></typeparam>
/// <param name="services"></param>
/// <param name="viewLifetime"></param>
/// <returns>The <see cref="IContainerRegistry"/>.</returns>
public static IContainerRegistry RegisterShell<TView, TViewModel>(
this IContainerRegistry services
)
where TView : FrameworkElement, new()
where TViewModel : notnull
{
services.Register<TViewModel>();
services.RegisterSingleton<TView>(cp =>
{
var viewModel = cp.Resolve<TViewModel>();
var view = new TView { DataContext = viewModel };
return view;
});
return services;
}


public static void RegisterSingleInstanceDialog<TView, TViewModel>(this IContainerRegistry services, string name)
where TView : FrameworkElement
where TViewModel : class, IDialogAware
Expand Down
26 changes: 10 additions & 16 deletions Tum4ik.JustClipboardManager/Services/ClipboardHookService.cs
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
using System.Runtime.InteropServices;
using Prism.Events;
using Tum4ik.JustClipboardManager.Events;
using Tum4ik.JustClipboardManager.Services.PInvoke;

namespace Tum4ik.JustClipboardManager.Services;
internal sealed class ClipboardHookService : IClipboardHookService, IDisposable
{
private readonly IEventAggregator _eventAggregator;
private readonly IUser32DllService _user32Dll;

public ClipboardHookService(IPasteWindowService pasteWindowService,
IEventAggregator eventAggregator)
IEventAggregator eventAggregator,
IUser32DllService user32Dll)
{
_eventAggregator = eventAggregator;
_user32Dll = user32Dll;

_windowHandle = pasteWindowService.WindowHandle;
_nextClipboardViewerHandle = SetClipboardViewer(_windowHandle);
_nextClipboardViewerHandle = user32Dll.SetClipboardViewer(_windowHandle);

_timer.Elapsed += (s, e) => OnClipboardChanged();
}
Expand Down Expand Up @@ -44,7 +47,7 @@ public nint HwndHook(nint hWnd, int msg, nint wParam, nint lParam, ref bool hand
_timer.Enabled = true;
}
}
SendMessage(_nextClipboardViewerHandle, msg, wParam, lParam);
_user32Dll.SendMessage(_nextClipboardViewerHandle, msg, wParam, lParam);
break;
case 0x030D: // WM_CHANGECBCHAIN
if (wParam == _nextClipboardViewerHandle)
Expand All @@ -53,7 +56,7 @@ public nint HwndHook(nint hWnd, int msg, nint wParam, nint lParam, ref bool hand
}
else
{
SendMessage(_nextClipboardViewerHandle, msg, wParam, lParam);
_user32Dll.SendMessage(_nextClipboardViewerHandle, msg, wParam, lParam);
}
break;
}
Expand All @@ -70,16 +73,7 @@ private void OnClipboardChanged()

public void Dispose()
{
ChangeClipboardChain(_windowHandle, _nextClipboardViewerHandle);
_timer.Close();
_user32Dll.ChangeClipboardChain(_windowHandle, _nextClipboardViewerHandle);
}


[DllImport("user32.dll")]
private static extern nint SetClipboardViewer(nint hWndNewViewer);

[DllImport("user32.dll")]
private static extern bool ChangeClipboardChain(nint hWndRemove, nint hWndNewNext);

[DllImport("user32.dll")]
private static extern int SendMessage(nint hWnd, int msg, nint wParam, nint lParam);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,20 @@
namespace Tum4ik.JustClipboardManager.Services.PInvoke;
internal interface IKernel32DllService
{
int GlobalAddAtom(string name);
int GlobalAddAtom(string name) => _GlobalAddAtom(name);

int GlobalDeleteAtom(int nAtom);
}


internal class Kernel32DllService : IKernel32DllService
{
public int GlobalAddAtom(string name)
{
return _GlobalAddAtom(name);
}

public int GlobalDeleteAtom(int nAtom)
{
return _GlobalDeleteAtom(nAtom);
}
int GlobalDeleteAtom(int nAtom) => _GlobalDeleteAtom(nAtom);


[DllImport("kernel32.dll", EntryPoint = "GlobalAddAtom", CharSet = CharSet.Unicode)]
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
private static extern int _GlobalAddAtom(string name);


[DllImport("kernel32.dll", EntryPoint = "GlobalDeleteAtom")]
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
private static extern int _GlobalDeleteAtom(int nAtom);
}


internal class Kernel32DllService : IKernel32DllService { }
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,7 @@ internal interface ISHCoreDllService
/// even when the screen is rotated.
/// </param>
/// <returns>True if operation succeeds, otherwise - false.</returns>
bool GetDpiForMonitor(nint hwnd, MonitorDpiType dpiType, out int dpiX, out int dpiY);
}


internal class SHCoreDllService : ISHCoreDllService
{
public bool GetDpiForMonitor(nint hwnd, MonitorDpiType dpiType, out int dpiX, out int dpiY)
bool GetDpiForMonitor(nint hwnd, MonitorDpiType dpiType, out int dpiX, out int dpiY)
{
var result = _GetDpiForMonitor(hwnd, dpiType, out dpiX, out dpiY);
return result == 0;
Expand All @@ -35,4 +29,7 @@ public bool GetDpiForMonitor(nint hwnd, MonitorDpiType dpiType, out int dpiX, ou
[DllImport("SHCore.dll", EntryPoint = "GetDpiForMonitor")]
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
private static extern uint _GetDpiForMonitor(nint hwnd, MonitorDpiType dpiType, out int dpiX, out int dpiY);
}
}


internal class SHCoreDllService : ISHCoreDllService { }
Loading

0 comments on commit 1337299

Please sign in to comment.