282 lines
12 KiB
C#
282 lines
12 KiB
C#
using System.Diagnostics;
|
||
using System.Runtime.InteropServices;
|
||
using System.Text;
|
||
using System.Windows;
|
||
using System.Windows.Automation;
|
||
|
||
namespace DeskCore;
|
||
|
||
/// <summary>
|
||
/// Maus-Funktionen direkt auf dem Windows-Desktop – ohne etwas in den Explorer einzuschleusen:
|
||
/// • Doppelklick auf freie Stelle: Desktop-Icons und Boxen aus-/einblenden ("Schnell verstecken")
|
||
/// • Rechte Maustaste auf freier Stelle ziehen: neue Box aufziehen (normaler Rechtsklick bleibt unverändert)
|
||
/// • Mausrad über dem Desktop: Desktop-Seite wechseln
|
||
/// </summary>
|
||
public static class DesktopToggle
|
||
{
|
||
private static IntPtr _hook;
|
||
private static LowLevelMouseProc? _proc; // Referenz halten, sonst räumt der GC den Callback weg
|
||
private static long _lastDownTick;
|
||
private static POINT _lastDownPt;
|
||
|
||
// Box aufziehen
|
||
private static bool _rPending, _rDrawing, _updatePosted;
|
||
private static POINT _rStart, _rCurrent;
|
||
private static long _lastWheelTick;
|
||
|
||
private static AppSettings S => AppSettings.Current;
|
||
|
||
public static void Apply()
|
||
{
|
||
bool needed = S.QuickHide || S.DrawFences || (S.PageWheel && S.Pages.Count > 1);
|
||
if (needed) Start();
|
||
else Stop();
|
||
}
|
||
|
||
private static void Start()
|
||
{
|
||
if (_hook != IntPtr.Zero) return;
|
||
_proc = Proc;
|
||
using var module = Process.GetCurrentProcess().MainModule!;
|
||
_hook = SetWindowsHookEx(WH_MOUSE_LL, _proc, GetModuleHandle(module.ModuleName), 0);
|
||
}
|
||
|
||
public static void Stop()
|
||
{
|
||
if (_hook == IntPtr.Zero) return;
|
||
UnhookWindowsHookEx(_hook);
|
||
_hook = IntPtr.Zero;
|
||
}
|
||
|
||
/// <summary>Muss sehr schnell sein: nur Fensterklasse und zwischengespeicherte Icon-Positionen prüfen.</summary>
|
||
private static IntPtr Proc(int nCode, IntPtr wParam, IntPtr lParam)
|
||
{
|
||
if (nCode < 0) return CallNextHookEx(_hook, nCode, wParam, lParam);
|
||
var info = Marshal.PtrToStructure<MSLLHOOKSTRUCT>(lParam);
|
||
bool injected = (info.flags & LLMHF_INJECTED) != 0;
|
||
int msg = wParam.ToInt32();
|
||
var dispatcher = Application.Current?.Dispatcher;
|
||
|
||
switch (msg)
|
||
{
|
||
case WM_LBUTTONDOWN when S.QuickHide:
|
||
{
|
||
long now = Environment.TickCount64;
|
||
bool isDouble = now - _lastDownTick <= GetDoubleClickTime() &&
|
||
Math.Abs(info.pt.x - _lastDownPt.x) <= 4 && Math.Abs(info.pt.y - _lastDownPt.y) <= 4;
|
||
if (isDouble)
|
||
{
|
||
_lastDownTick = 0;
|
||
var pt = info.pt;
|
||
var first = _lastDownPt;
|
||
// Schon im Hook (ohne COM) aussortieren: Doppelklick auf ein Icon = Programmstart
|
||
if (!DesktopIcons.HitsIcon(pt.x, pt.y, margin: 8) && !DesktopIcons.HitsIcon(first.x, first.y, margin: 8))
|
||
dispatcher?.BeginInvoke(() => OnDoubleClick(pt, first));
|
||
}
|
||
else
|
||
{
|
||
_lastDownTick = now;
|
||
_lastDownPt = info.pt;
|
||
}
|
||
break;
|
||
}
|
||
|
||
case WM_RBUTTONDOWN when !injected && S.DrawFences && IsDesktopAt(info.pt) && !DesktopIcons.HitsIcon(info.pt.x, info.pt.y):
|
||
// Rechtsklick auf freie Desktop-Fläche abfangen – bleibt es ein Klick, wird er unten nachgereicht
|
||
_rPending = true;
|
||
_rDrawing = false;
|
||
_rStart = _rCurrent = info.pt;
|
||
return new IntPtr(1);
|
||
|
||
case WM_MOUSEMOVE when _rPending:
|
||
_rCurrent = info.pt;
|
||
if (!_rDrawing && (Math.Abs(info.pt.x - _rStart.x) > 8 || Math.Abs(info.pt.y - _rStart.y) > 8))
|
||
{
|
||
_rDrawing = true;
|
||
dispatcher?.BeginInvoke(() => DrawBoxOverlay.Begin());
|
||
}
|
||
if (_rDrawing && !_updatePosted)
|
||
{
|
||
_updatePosted = true;
|
||
dispatcher?.BeginInvoke(() => { _updatePosted = false; DrawBoxOverlay.Update(PixelRect(_rStart, _rCurrent)); });
|
||
}
|
||
break;
|
||
|
||
case WM_RBUTTONUP when _rPending && !injected:
|
||
{
|
||
_rPending = false;
|
||
var rect = PixelRect(_rStart, info.pt);
|
||
if (_rDrawing) dispatcher?.BeginInvoke(() => DrawBoxOverlay.Finish(rect));
|
||
else dispatcher?.BeginInvoke(ReplayRightClick); // normales Desktop-Kontextmenü
|
||
return new IntPtr(1);
|
||
}
|
||
|
||
case WM_MOUSEWHEEL when S.PageWheel && S.Pages.Count > 1 && !injected:
|
||
{
|
||
if ((GetKeyState(VK_CONTROL) & 0x8000) != 0) break; // Strg+Rad = Icon-Größe (Windows)
|
||
long now = Environment.TickCount64;
|
||
if (now - _lastWheelTick < 350 || !IsDesktopAt(info.pt)) break;
|
||
_lastWheelTick = now;
|
||
int delta = (short)((info.mouseData >> 16) & 0xFFFF);
|
||
dispatcher?.BeginInvoke(() => FenceManager.NextPage(delta < 0 ? 1 : -1));
|
||
break;
|
||
}
|
||
}
|
||
return CallNextHookEx(_hook, nCode, wParam, lParam);
|
||
}
|
||
|
||
private static Rect PixelRect(POINT a, POINT b) =>
|
||
new(Math.Min(a.x, b.x), Math.Min(a.y, b.y), Math.Abs(a.x - b.x), Math.Abs(a.y - b.y));
|
||
|
||
/// <summary>Liegt unter dem Punkt direkt der Desktop (kein Fenster darüber)?</summary>
|
||
private static bool IsDesktopAt(POINT pt)
|
||
{
|
||
var cls = ClassName(WindowFromPoint(pt));
|
||
return cls is "WorkerW" or "Progman" or "SHELLDLL_DefView" ||
|
||
(cls == "SysListView32" && ClassName(GetParent(WindowFromPoint(pt))) == "SHELLDLL_DefView");
|
||
}
|
||
|
||
/// <summary>Abgefangenen Rechtsklick an Windows weitergeben (wird als "eingespeist" markiert und nicht erneut abgefangen).</summary>
|
||
private static void ReplayRightClick()
|
||
{
|
||
var inputs = new[]
|
||
{
|
||
new INPUT { type = INPUT_MOUSE, mi = new MOUSEINPUT { dwFlags = MOUSEEVENTF_RIGHTDOWN } },
|
||
new INPUT { type = INPUT_MOUSE, mi = new MOUSEINPUT { dwFlags = MOUSEEVENTF_RIGHTUP } },
|
||
};
|
||
SendInput((uint)inputs.Length, inputs, Marshal.SizeOf<INPUT>());
|
||
}
|
||
|
||
private static void OnDoubleClick(POINT pt, POINT first)
|
||
{
|
||
if (!IsEmptyDesktopSpot(pt) || !IsEmptyDesktopSpot(first)) return;
|
||
// Positionen frisch lesen – der Zwischenspeicher kann ein paar Sekunden alt sein (Icon gerade verschoben)
|
||
if (IconsVisible())
|
||
{
|
||
try { DesktopIcons.RefreshCache(); } catch { return; }
|
||
if (DesktopIcons.HitsIcon(pt.x, pt.y, margin: 8) || DesktopIcons.HitsIcon(first.x, first.y, margin: 8)) return;
|
||
}
|
||
Toggle();
|
||
}
|
||
|
||
/// <summary>Liegt der Punkt auf dem Desktop – und zwar nicht auf einem Icon?</summary>
|
||
private static bool IsEmptyDesktopSpot(POINT pt)
|
||
{
|
||
var hwnd = WindowFromPoint(pt);
|
||
var cls = ClassName(hwnd);
|
||
if (cls is "WorkerW" or "Progman" or "SHELLDLL_DefView") return true; // Icons gerade ausgeblendet
|
||
if (cls != "SysListView32" || ClassName(GetParent(hwnd)) != "SHELLDLL_DefView") return false;
|
||
// Auf ein Icon geklickt? Dann ist es ein normaler Programmstart, nichts tun.
|
||
try
|
||
{
|
||
var el = AutomationElement.FromPoint(new Point(pt.x, pt.y));
|
||
for (var e = el; e is not null; e = TreeWalker.RawViewWalker.GetParent(e))
|
||
{
|
||
var type = e.Current.ControlType;
|
||
if (type == ControlType.ListItem) return false;
|
||
if (type == ControlType.List) break;
|
||
}
|
||
return true;
|
||
}
|
||
catch { return false; }
|
||
}
|
||
|
||
/// <summary>Desktop-Icons und Boxen umschalten (auch per Befehl --toggle-desktop).</summary>
|
||
public static void Toggle()
|
||
{
|
||
var view = DesktopView();
|
||
var list = view == IntPtr.Zero ? IntPtr.Zero : FindWindowEx(view, IntPtr.Zero, "SysListView32", null);
|
||
bool iconsVisible = list != IntPtr.Zero && IsWindowVisible(list);
|
||
if (view != IntPtr.Zero) SendMessage(view, WM_COMMAND, new IntPtr(0x7402), IntPtr.Zero); // "Desktopsymbole anzeigen" umschalten
|
||
FenceManager.SetHidden(iconsVisible);
|
||
DesktopTint.Refresh();
|
||
}
|
||
|
||
/// <summary>Beim Beenden: ausgeblendete Desktop-Icons wieder zeigen, damit niemand vor einem leeren Desktop sitzt.</summary>
|
||
public static void RestoreOnExit()
|
||
{
|
||
if (!FenceManager.Hidden) return;
|
||
var view = DesktopView();
|
||
var list = view == IntPtr.Zero ? IntPtr.Zero : FindWindowEx(view, IntPtr.Zero, "SysListView32", null);
|
||
if (view != IntPtr.Zero && list != IntPtr.Zero && !IsWindowVisible(list))
|
||
SendMessage(view, WM_COMMAND, new IntPtr(0x7402), IntPtr.Zero);
|
||
}
|
||
|
||
/// <summary>Das Fenster mit den Desktop-Icons (liegt je nach Zustand unter Progman oder einem WorkerW).</summary>
|
||
public static IntPtr DesktopView()
|
||
{
|
||
var progman = FindWindow("Progman", null);
|
||
var view = FindWindowEx(progman, IntPtr.Zero, "SHELLDLL_DefView", null);
|
||
if (view != IntPtr.Zero) return view;
|
||
IntPtr worker = IntPtr.Zero;
|
||
while ((worker = FindWindowEx(IntPtr.Zero, worker, "WorkerW", null)) != IntPtr.Zero)
|
||
{
|
||
view = FindWindowEx(worker, IntPtr.Zero, "SHELLDLL_DefView", null);
|
||
if (view != IntPtr.Zero) return view;
|
||
}
|
||
return IntPtr.Zero;
|
||
}
|
||
|
||
/// <summary>Sind die Desktop-Icons gerade sichtbar?</summary>
|
||
public static bool IconsVisible()
|
||
{
|
||
var view = DesktopView();
|
||
var list = view == IntPtr.Zero ? IntPtr.Zero : FindWindowEx(view, IntPtr.Zero, "SysListView32", null);
|
||
return list != IntPtr.Zero && IsWindowVisible(list);
|
||
}
|
||
|
||
private static string ClassName(IntPtr hwnd)
|
||
{
|
||
if (hwnd == IntPtr.Zero) return "";
|
||
var sb = new StringBuilder(64);
|
||
GetClassName(hwnd, sb, sb.Capacity);
|
||
return sb.ToString();
|
||
}
|
||
|
||
private const int WH_MOUSE_LL = 14, WM_COMMAND = 0x0111, VK_CONTROL = 0x11;
|
||
private const int WM_MOUSEMOVE = 0x0200, WM_LBUTTONDOWN = 0x0201, WM_RBUTTONDOWN = 0x0204, WM_RBUTTONUP = 0x0205, WM_MOUSEWHEEL = 0x020A;
|
||
private const uint LLMHF_INJECTED = 0x1, INPUT_MOUSE = 0, MOUSEEVENTF_RIGHTDOWN = 0x0008, MOUSEEVENTF_RIGHTUP = 0x0010;
|
||
|
||
private delegate IntPtr LowLevelMouseProc(int nCode, IntPtr wParam, IntPtr lParam);
|
||
|
||
[StructLayout(LayoutKind.Sequential)] private struct POINT { public int x, y; }
|
||
|
||
[StructLayout(LayoutKind.Sequential)]
|
||
private struct MSLLHOOKSTRUCT
|
||
{
|
||
public POINT pt;
|
||
public uint mouseData, flags, time;
|
||
public IntPtr dwExtraInfo;
|
||
}
|
||
|
||
[StructLayout(LayoutKind.Sequential)]
|
||
private struct MOUSEINPUT
|
||
{
|
||
public int dx, dy;
|
||
public uint mouseData, dwFlags, time;
|
||
public IntPtr dwExtraInfo;
|
||
}
|
||
|
||
[StructLayout(LayoutKind.Explicit)]
|
||
private struct INPUT
|
||
{
|
||
[FieldOffset(0)] public uint type;
|
||
[FieldOffset(8)] public MOUSEINPUT mi; // 64-Bit: Union beginnt bei Offset 8
|
||
}
|
||
|
||
[DllImport("user32.dll", SetLastError = true)] private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelMouseProc lpfn, IntPtr hMod, uint dwThreadId);
|
||
[DllImport("user32.dll")] private static extern bool UnhookWindowsHookEx(IntPtr hhk);
|
||
[DllImport("user32.dll")] private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
|
||
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)] private static extern IntPtr GetModuleHandle(string lpModuleName);
|
||
[DllImport("user32.dll")] private static extern uint GetDoubleClickTime();
|
||
[DllImport("user32.dll")] private static extern short GetKeyState(int vk);
|
||
[DllImport("user32.dll")] private static extern uint SendInput(uint n, INPUT[] inputs, int size);
|
||
[DllImport("user32.dll")] private static extern IntPtr WindowFromPoint(POINT p);
|
||
[DllImport("user32.dll")] private static extern IntPtr GetParent(IntPtr hWnd);
|
||
[DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern int GetClassName(IntPtr hWnd, StringBuilder name, int max);
|
||
[DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern IntPtr FindWindow(string? cls, string? title);
|
||
[DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern IntPtr FindWindowEx(IntPtr parent, IntPtr after, string? cls, string? title);
|
||
[DllImport("user32.dll")] private static extern bool IsWindowVisible(IntPtr hWnd);
|
||
[DllImport("user32.dll")] private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
|
||
}
|