diff --git a/.gitignore b/.gitignore index 51c493b..a5a8099 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ bin/ obj/ -prod/ \ No newline at end of file +prod/ +.vs/ +*.sln +*.csproj.user diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d7ed74f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,79 @@ +# Switchie Changelog + + +## v1.4.3 + +Changes: +- Changed version management to have the application manifest as source so also the executable is correctly versioned + + +## v1.4.2 + +New features: +- Setting to hide application window from Taskbar +- Setting to change opacity of the application + +Bug fixes +- Filter minimized windows from list, especially important for the icons render mode +- Clicking on windows in Icons render mode now works reliably when an application has more than one window + + +## v1.4.0 + +Bug fixes: +- Get it run on recent Windows 11 version again + +Changes: +- Instance creation has now several fallbacks so it should work on several Win 10 and Win 11 versions + + +## v1.3.3 + +Changes: +- no longer allow mouse clicks to bring windows from another desktop to foreground +- Added tooltips / infos in the settings dialog for some of the settings which need an explanation + + +## v1.3.2 + +New features: +- Icons render mode supports a second row when there isn't enough space in one row +- Add parameters for desktop padding and icon padding in settings dialog + +Changes: +- fixing performance properties +- layout improvements in settings dialog +- app versioning increase now less aggressive + + +## v1.3.1 + +Changes: +- Icons render mode now also supports selection as well as drag & drop to other desktops +- the context menu now also has some nice icons based on glyhps + + +## v1.3.0 + +New features: +- new settings dialog + +Changes: +- nicer about dialog + +Others: +- removed not used properties and functions + + +## v1.2.0 + +Bug fixes: +- Update application size when number of desktops are changed + +New features: +- New alternative render mode: Show a list of application icons instead windows +- Save render mode current window position (on request) to registry and restore them from there on startup +- Windows can be brought to front when clicking on them + +Others: +- Some refactoring and cleanup diff --git a/README.md b/README.md index 9d2299f..7ada867 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ + + # Switchie Switchie is a virtual desktop pager for Windows inspired by various Linux-based virtual desktop pagers diff --git a/TODOS.md b/TODOS.md new file mode 100644 index 0000000..5165724 --- /dev/null +++ b/TODOS.md @@ -0,0 +1,11 @@ + +# Todos + +## Bugs + +- Potential crash when the render target is gone (can happen on remote desktops, probably otherwise hard to recreate) + + +## Improvements + +- there could be an option to make the application automatically half transparent when it covers another window diff --git a/src/Core/API/VirtualDesktopAPI-Win11.cs b/src/Core/API/VirtualDesktopAPI-Win11.cs index d0f09c1..538d2c3 100644 --- a/src/Core/API/VirtualDesktopAPI-Win11.cs +++ b/src/Core/API/VirtualDesktopAPI-Win11.cs @@ -154,19 +154,19 @@ public interface IApplicationViewCollection [ComImport] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - [Guid("B2F925B9-5A0F-4D2E-9F4D-2B1507593C10")] + [Guid("53F5CA0B-158F-4124-900C-057158060B27")] public interface IVirtualDesktopManagerInternal { - int GetCount(IntPtr hWndOrMon); + int GetCount(); void MoveViewToDesktop(IApplicationView view, IVirtualDesktop desktop); bool CanViewMoveDesktops(IApplicationView view); - IVirtualDesktop GetCurrentDesktop(IntPtr hWndOrMon); - void GetDesktops(IntPtr hWndOrMon, out IObjectArray desktops); + IVirtualDesktop GetCurrentDesktop(); + void GetDesktops(out IObjectArray desktops); [PreserveSig] int GetAdjacentDesktop(IVirtualDesktop from, int direction, out IVirtualDesktop desktop); - void SwitchDesktop(IntPtr hWndOrMon, IVirtualDesktop desktop); - IVirtualDesktop CreateDesktop(IntPtr hWndOrMon); - void MoveDesktop(IVirtualDesktop desktop, IntPtr hWndOrMon, int nIndex); + void SwitchDesktop(IVirtualDesktop desktop); + IVirtualDesktop CreateDesktop(); + void MoveDesktop(IVirtualDesktop desktop, int nIndex); void RemoveDesktop(IVirtualDesktop desktop, IVirtualDesktop fallback); IVirtualDesktop FindDesktop(ref Guid desktopid); void GetDesktopSwitchIncludeExcludeViews(IVirtualDesktop desktop, out IObjectArray unknown1, out IObjectArray unknown2); @@ -174,8 +174,11 @@ public interface IVirtualDesktopManagerInternal void SetDesktopWallpaper(IVirtualDesktop desktop, [MarshalAs(UnmanagedType.HString)] string path); void UpdateWallpaperPathForAllDesktops([MarshalAs(UnmanagedType.HString)] string path); void CopyDesktopState(IApplicationView pView0, IApplicationView pView1); - int GetDesktopIsPerMonitor(); - void SetDesktopIsPerMonitor(bool state); + void CreateRemoteDesktop([MarshalAs(UnmanagedType.HString)] string path, out IVirtualDesktop desktop); + void SwitchRemoteDesktop(IVirtualDesktop desktop, IntPtr switchtype); + void SwitchDesktopWithAnimation(IVirtualDesktop desktop); + void GetLastActiveDesktop(out IVirtualDesktop desktop); + void WaitForAnimationToComplete(); } [ComImport] @@ -190,16 +193,16 @@ public interface IVirtualDesktopManager [ComImport] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - [Guid("536D3495-B208-4CC9-AE26-DE8111275BF8")] + [Guid("3F07F4BE-B107-441A-AF0F-39D82529072C")] public interface IVirtualDesktop : IIVirtualDesktop { bool IsViewVisible(IApplicationView view); Guid GetId(); - IntPtr Unknown1(); [return: MarshalAs(UnmanagedType.HString)] string GetName(); [return: MarshalAs(UnmanagedType.HString)] string GetWallpaperPath(); + bool IsRemote(); } } @@ -212,17 +215,17 @@ public class WindowsVirtualDesktop : IWindowsVirtualDesktop public IIVirtualDesktop ivd { get; set; } public WindowsVirtualDesktop() { } public WindowsVirtualDesktop(IIVirtualDesktop desktop) { this.ivd = desktop; } - public void MakeVisible() => _windowsVirtualDesktopManager.VirtualDesktopManagerInternal.SwitchDesktop(IntPtr.Zero, (WindowsVirtualDesktopAPI.IVirtualDesktop)ivd); + public void MakeVisible() => _windowsVirtualDesktopManager.VirtualDesktopManagerInternal.SwitchDesktop((WindowsVirtualDesktopAPI.IVirtualDesktop)ivd); public IWindowsVirtualDesktop FromIndex(int index) => new WindowsVirtualDesktop(_windowsVirtualDesktopManager.GetDesktop(index)); public int Count { - get => _windowsVirtualDesktopManager.VirtualDesktopManagerInternal.GetCount(IntPtr.Zero); + get => _windowsVirtualDesktopManager.VirtualDesktopManagerInternal.GetCount(); } public IWindowsVirtualDesktop Current { - get => new WindowsVirtualDesktop((IIVirtualDesktop)_windowsVirtualDesktopManager.VirtualDesktopManagerInternal.GetCurrentDesktop(IntPtr.Zero)); + get => new WindowsVirtualDesktop((IIVirtualDesktop)_windowsVirtualDesktopManager.VirtualDesktopManagerInternal.GetCurrentDesktop()); } public void MoveWindow(IntPtr hWnd) @@ -277,10 +280,10 @@ public WindowsVirtualDesktopManager() public WindowsVirtualDesktopAPI.IVirtualDesktop GetDesktop(int index) { - int count = VirtualDesktopManagerInternal.GetCount(IntPtr.Zero); + int count = VirtualDesktopManagerInternal.GetCount(); if (index < 0 || index >= count) throw new ArgumentOutOfRangeException("index"); WindowsVirtualDesktopAPI.IObjectArray desktops; - VirtualDesktopManagerInternal.GetDesktops(IntPtr.Zero, out desktops); + VirtualDesktopManagerInternal.GetDesktops(out desktops); object objdesktop; desktops.GetAt(index, typeof(WindowsVirtualDesktopAPI.IVirtualDesktop).GUID, out objdesktop); Marshal.ReleaseComObject(desktops); @@ -291,8 +294,8 @@ public int GetDesktopIndex(WindowsVirtualDesktopAPI.IVirtualDesktop desktop) { int index = -1; Guid IdSearch = desktop.GetId(); - VirtualDesktopManagerInternal.GetDesktops(IntPtr.Zero, out WindowsVirtualDesktopAPI.IObjectArray desktops); - for (int i = 0; i < VirtualDesktopManagerInternal.GetCount(IntPtr.Zero); i++) + VirtualDesktopManagerInternal.GetDesktops(out WindowsVirtualDesktopAPI.IObjectArray desktops); + for (int i = 0; i < VirtualDesktopManagerInternal.GetCount(); i++) { desktops.GetAt(i, typeof(WindowsVirtualDesktopAPI.IVirtualDesktop).GUID, out object objdesktop); if (IdSearch.CompareTo(((WindowsVirtualDesktopAPI.IVirtualDesktop)objdesktop).GetId()) == 0) @@ -320,4 +323,4 @@ public void PinApplication(IntPtr hWnd) } } -} \ No newline at end of file +} diff --git a/src/Core/API/VirtualDesktopAPI.cs b/src/Core/API/VirtualDesktopAPI.cs index 97152bd..35a7417 100644 --- a/src/Core/API/VirtualDesktopAPI.cs +++ b/src/Core/API/VirtualDesktopAPI.cs @@ -1,7 +1,8 @@ using System; +using System.Collections.Generic; +using System.Diagnostics; namespace Switchie { - public class WindowsVirtualDesktop { private static IWindowsVirtualDesktop _instance; @@ -16,18 +17,122 @@ public static IWindowsVirtualDesktop GetInstance() { if (WindowsVirtualDesktop._instance == null) { - if (Program.WindowsVersion.IsWin11()) - _instance = new Switchie.VirtualDesktopAPI.Win11.WindowsVirtualDesktop(); - else if (Program.WindowsVersion.IsWin10()) - _instance = new Switchie.VirtualDesktopAPI.Win10.WindowsVirtualDesktop(); - else if (Program.WindowsVersion.IsWin10LTSC()) - _instance = new Switchie.VirtualDesktopAPI.Win10LTSC.WindowsVirtualDesktop(); - else - throw new PlatformNotSupportedException(); + _instance = CreateForCurrentOS(); } return WindowsVirtualDesktop._instance; } + private static IWindowsVirtualDesktop CreateForCurrentOS() + { + if (Program.WindowsVersion.IsWin11()) + { + IWindowsVirtualDesktop desktop; + Exception win11Error; + if (TryCreateWin11(out desktop, out win11Error)) + return desktop; + + Trace.WriteLine("[Switchie] Win11 desktop API initialization failed. Falling back. " + win11Error); + + Exception win10Error; + if (TryCreateWin10(out desktop, out win10Error)) + { + Trace.WriteLine("[Switchie] Using Win10 desktop API fallback on Win11."); + return desktop; + } + + Exception ltscError; + if (TryCreateWin10LTSC(out desktop, out ltscError)) + { + Trace.WriteLine("[Switchie] Using Win10 LTSC desktop API fallback on Win11."); + return desktop; + } + + throw BuildInitializationException("virtual desktop", win11Error, win10Error, ltscError); + } + + if (Program.WindowsVersion.IsWin10()) + { + IWindowsVirtualDesktop desktop; + Exception error; + if (TryCreateWin10(out desktop, out error)) + return desktop; + throw new InvalidOperationException("Failed to initialize Win10 virtual desktop backend.", error); + } + + if (Program.WindowsVersion.IsWin10LTSC()) + { + IWindowsVirtualDesktop desktop; + Exception error; + if (TryCreateWin10LTSC(out desktop, out error)) + return desktop; + throw new InvalidOperationException("Failed to initialize Win10 LTSC virtual desktop backend.", error); + } + + throw new PlatformNotSupportedException(); + } + + private static InvalidOperationException BuildInitializationException(string featureName, params Exception[] errors) + { + var innerExceptions = new List(); + foreach (Exception error in errors) + { + if (error != null) + innerExceptions.Add(error); + } + + return new InvalidOperationException( + "Failed to initialize " + featureName + " backend for this Windows version.", + new AggregateException(innerExceptions)); + } + + private static bool TryCreateWin11(out IWindowsVirtualDesktop desktop, out Exception error) + { + try + { + desktop = new Switchie.VirtualDesktopAPI.Win11.WindowsVirtualDesktop(); + error = null; + return true; + } + catch (Exception ex) + { + desktop = null; + error = ex; + return false; + } + } + + private static bool TryCreateWin10(out IWindowsVirtualDesktop desktop, out Exception error) + { + try + { + desktop = new Switchie.VirtualDesktopAPI.Win10.WindowsVirtualDesktop(); + error = null; + return true; + } + catch (Exception ex) + { + desktop = null; + error = ex; + return false; + } + } + + private static bool TryCreateWin10LTSC(out IWindowsVirtualDesktop desktop, out Exception error) + { + try + { + desktop = new Switchie.VirtualDesktopAPI.Win10LTSC.WindowsVirtualDesktop(); + error = null; + return true; + } + catch (Exception ex) + { + desktop = null; + error = ex; + return false; + } + } + } public class WindowsVirtualDesktopManager @@ -39,17 +144,121 @@ public static IWindowsVirtualDesktopManager GetInstance() { if (WindowsVirtualDesktopManager._instance == null) { - if (Program.WindowsVersion.IsWin11()) - _instance = new Switchie.VirtualDesktopAPI.Win11.WindowsVirtualDesktopManager(); - else if (Program.WindowsVersion.IsWin10()) - _instance = new Switchie.VirtualDesktopAPI.Win10.WindowsVirtualDesktopManager(); - else if (Program.WindowsVersion.IsWin10LTSC()) - _instance = new Switchie.VirtualDesktopAPI.Win10LTSC.WindowsVirtualDesktopManager(); - else - throw new PlatformNotSupportedException(); + _instance = CreateForCurrentOS(); } return WindowsVirtualDesktopManager._instance; } + + private static IWindowsVirtualDesktopManager CreateForCurrentOS() + { + if (Program.WindowsVersion.IsWin11()) + { + IWindowsVirtualDesktopManager manager; + Exception win11Error; + if (TryCreateWin11(out manager, out win11Error)) + return manager; + + Trace.WriteLine("[Switchie] Win11 desktop manager API initialization failed. Falling back. " + win11Error); + + Exception win10Error; + if (TryCreateWin10(out manager, out win10Error)) + { + Trace.WriteLine("[Switchie] Using Win10 desktop manager API fallback on Win11."); + return manager; + } + + Exception ltscError; + if (TryCreateWin10LTSC(out manager, out ltscError)) + { + Trace.WriteLine("[Switchie] Using Win10 LTSC desktop manager API fallback on Win11."); + return manager; + } + + throw BuildInitializationException("virtual desktop manager", win11Error, win10Error, ltscError); + } + + if (Program.WindowsVersion.IsWin10()) + { + IWindowsVirtualDesktopManager manager; + Exception error; + if (TryCreateWin10(out manager, out error)) + return manager; + throw new InvalidOperationException("Failed to initialize Win10 virtual desktop manager backend.", error); + } + + if (Program.WindowsVersion.IsWin10LTSC()) + { + IWindowsVirtualDesktopManager manager; + Exception error; + if (TryCreateWin10LTSC(out manager, out error)) + return manager; + throw new InvalidOperationException("Failed to initialize Win10 LTSC virtual desktop manager backend.", error); + } + + throw new PlatformNotSupportedException(); + } + + private static InvalidOperationException BuildInitializationException(string featureName, params Exception[] errors) + { + var innerExceptions = new List(); + foreach (Exception error in errors) + { + if (error != null) + innerExceptions.Add(error); + } + + return new InvalidOperationException( + "Failed to initialize " + featureName + " backend for this Windows version.", + new AggregateException(innerExceptions)); + } + + private static bool TryCreateWin11(out IWindowsVirtualDesktopManager manager, out Exception error) + { + try + { + manager = new Switchie.VirtualDesktopAPI.Win11.WindowsVirtualDesktopManager(); + error = null; + return true; + } + catch (Exception ex) + { + manager = null; + error = ex; + return false; + } + } + + private static bool TryCreateWin10(out IWindowsVirtualDesktopManager manager, out Exception error) + { + try + { + manager = new Switchie.VirtualDesktopAPI.Win10.WindowsVirtualDesktopManager(); + error = null; + return true; + } + catch (Exception ex) + { + manager = null; + error = ex; + return false; + } + } + + private static bool TryCreateWin10LTSC(out IWindowsVirtualDesktopManager manager, out Exception error) + { + try + { + manager = new Switchie.VirtualDesktopAPI.Win10LTSC.WindowsVirtualDesktopManager(); + error = null; + return true; + } + catch (Exception ex) + { + manager = null; + error = ex; + return false; + } + } } -} \ No newline at end of file +} diff --git a/src/Core/API/WinAPI.cs b/src/Core/API/WinAPI.cs index f91fd81..406ece4 100644 --- a/src/Core/API/WinAPI.cs +++ b/src/Core/API/WinAPI.cs @@ -15,11 +15,25 @@ public struct RECT public int Bottom; } + [StructLayout(LayoutKind.Sequential)] + public struct POINT + { + public int X; + public int Y; + } + public delegate bool EnumWindowsProc(IntPtr hWnd, int lParam); + public const int SW_HIDE = 0; + public const int SW_SHOWNOACTIVATE = 4; + public const int SW_SHOW = 5; + public const int SW_RESTORE = 9; + public const uint SWP_NOSIZE = 0x0001; public const uint SWP_NOMOVE = 0x0002; + public const uint SWP_NOACTIVATE = 0x0010; public const uint SWP_SHOWWINDOW = 0x0040; + public static readonly IntPtr HWND_TOPMOST = new IntPtr(-1); public static readonly IntPtr HWND_NOTOPMOST = new IntPtr(-2); @@ -42,24 +56,74 @@ public struct RECT public static uint GW_HWNDNEXT = 2; public static int IDI_APPLICATION = 0x7F00; - [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow(); - [DllImport("user32.dll")] public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); - [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool SetForegroundWindow(IntPtr hWnd); - [DllImport("user32.dll", SetLastError = true)] public static extern bool GetWindowRect(IntPtr hWnd, ref RECT lpRect); - [DllImport("user32.dll")] public static extern bool EnumWindows(EnumWindowsProc enumFunc, int lParam); - [DllImport("user32.dll")] public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount); - [DllImport("user32.dll")] public static extern int GetWindowTextLength(IntPtr hWnd); - [DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr hWnd); - [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] public static extern IntPtr GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount); - [DllImport("user32.dll")] public static extern IntPtr GetShellWindow(); - [DllImport("user32.dll", SetLastError = true)] public static extern IntPtr GetWindow(IntPtr hWnd, uint uCmd); - [DllImport("user32.dll", SetLastError = true)] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId); - [DllImport("user32.dll")] public static extern int SendMessage(IntPtr hWnd, int Msg, int wParam, int lParam); - [DllImport("user32.dll")] public static extern bool ReleaseCapture(); - [DllImport("user32.dll")] public static extern int PostMessage(IntPtr hWnd, int Msg, int wParam, int lParam); - [DllImport("user32.dll")] public static extern int LoadIcon(IntPtr hInstance, IntPtr lpIconName); - [DllImport("user32.dll", EntryPoint = "GetClassLong")] public static extern uint GetClassLong32(IntPtr hWnd, int nIndex); - [DllImport("user32.dll", EntryPoint = "GetClassLongPtr")] public static extern int GetClassLong64(IntPtr hWnd, int nIndex); + [DllImport("user32.dll")] + public static extern IntPtr GetForegroundWindow(); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow); + + [DllImport("user32.dll")] + public static extern IntPtr WindowFromPoint(POINT Point); + + [DllImport("user32.dll")] + public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool SetForegroundWindow(IntPtr hWnd); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool GetWindowRect(IntPtr hWnd, ref RECT lpRect); + + [DllImport("user32.dll")] + public static extern bool EnumWindows(EnumWindowsProc enumFunc, int lParam); + + [DllImport("user32.dll")] + public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount); + + [DllImport("user32.dll")] + public static extern int GetWindowTextLength(IntPtr hWnd); + + [DllImport("user32.dll")] + public static extern bool IsWindowVisible(IntPtr hWnd); + + [DllImport("user32.dll")] + public static extern bool IsIconic(IntPtr hWnd); + + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] + public static extern IntPtr GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount); + + [DllImport("user32.dll")] + public static extern IntPtr GetShellWindow(); + + [DllImport("user32.dll", SetLastError = true)] + public static extern IntPtr GetWindow(IntPtr hWnd, uint uCmd); + + [DllImport("user32.dll", SetLastError = true)] + public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId); + + [DllImport("user32.dll")] + public static extern int SendMessage(IntPtr hWnd, int Msg, int wParam, int lParam); + + [DllImport("user32.dll")] + public static extern bool ReleaseCapture(); + + [DllImport("user32.dll")] + public static extern int PostMessage(IntPtr hWnd, int Msg, int wParam, int lParam); + + [DllImport("user32.dll")] + public static extern int LoadIcon(IntPtr hInstance, IntPtr lpIconName); + + [DllImport("user32.dll", EntryPoint = "GetClassLong")] + public static extern uint GetClassLong32(IntPtr hWnd, int nIndex); + + [DllImport("user32.dll", EntryPoint = "GetClassLongPtr")] + public static extern int GetClassLong64(IntPtr hWnd, int nIndex); // 64 bit version maybe loses significant 64-bit specific information public static int GetClassLongPtr(IntPtr hWnd, int nIndex) => IntPtr.Size == 4 ? (int)GetClassLong32(hWnd, nIndex) : GetClassLong64(hWnd, nIndex); diff --git a/src/Core/API/WinEventHook.cs b/src/Core/API/WinEventHook.cs new file mode 100644 index 0000000..0665390 --- /dev/null +++ b/src/Core/API/WinEventHook.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace Switchie +{ + public static class WinEventHook + { + public const uint WINEVENT_OUTOFCONTEXT = 0x0000; + public const uint EVENT_SYSTEM_FOREGROUND = 0x0003; + public const uint EVENT_OBJECT_REORDER = 0x8004; + public const uint EVENT_SYSTEM_MINIMIZEEND = 0x0017; + public const uint EVENT_OBJECT_SHOW = 0x8002; + public const uint EVENT_OBJECT_HIDE = 0x8003; + + public delegate void WinEventDelegate( + IntPtr hWinEventHook, + uint eventType, + IntPtr hwnd, + int idObject, + int idChild, + uint dwEventThread, + uint dwmsEventTime); + + [DllImport("user32.dll")] + public static extern IntPtr SetWinEventHook( + uint eventMin, + uint eventMax, + IntPtr hmodWinEventProc, + WinEventDelegate lpfnWinEventProc, + uint idProcess, + uint idThread, + uint dwFlags); + + [DllImport("user32.dll")] + public static extern bool UnhookWinEvent(IntPtr hWinEventHook); + } +} diff --git a/src/Core/Helpers.cs b/src/Core/Helpers.cs index 921950b..ccf8874 100644 --- a/src/Core/Helpers.cs +++ b/src/Core/Helpers.cs @@ -6,9 +6,32 @@ namespace Switchie { - public class Helpers { + public static Bitmap CreateGlyphBitmap(string glyph, int size = 16) + { + var bmp = new Bitmap(size, size); + using (var g = Graphics.FromImage(bmp)) + { + g.Clear(Color.Transparent); + g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit; + + // Try common Windows fonts with broad unicode support. + using (var font = new Font("Segoe UI Symbol", size - 2, FontStyle.Regular, GraphicsUnit.Pixel)) + using (var sf = new StringFormat + { + Alignment = StringAlignment.Center, + LineAlignment = StringAlignment.Center, + FormatFlags = StringFormatFlags.NoWrap + }) + { + g.DrawString(glyph, font, Brushes.Black, new RectangleF(0, 0, size, size), sf); + } + } + + return bmp; + } + public static byte[] GetResourceFromAssembly(Type type, string name) { MemoryStream ms = new MemoryStream(); @@ -45,4 +68,4 @@ public static Size AspectRatioResize(Size sz, int finalWidth, int finalHeight) } } -} \ No newline at end of file +} diff --git a/src/Core/Types.cs b/src/Core/Types.cs index 890a2f5..a5a750c 100644 --- a/src/Core/Types.cs +++ b/src/Core/Types.cs @@ -3,24 +3,31 @@ namespace Switchie { - public class Window { public bool IsActive { get; set; } + public int VirtualDesktopIndex { get; set; } + public int ZOrder { get; set; } + public IntPtr Handle { get; set; } + public string Title { get; set; } + public string Class { get; set; } + public uint ProcessID { get; set; } + public Rectangle Dimensions { get; set; } + public Bitmap Icon { get; set; } } public class DragDropData { public int OriginDesktopIndex { get; set; } + public Window DraggedWindow { get; set; } } - } \ No newline at end of file diff --git a/src/Core/WindowManager.cs b/src/Core/WindowManager.cs index bd18489..77d7b07 100644 --- a/src/Core/WindowManager.cs +++ b/src/Core/WindowManager.cs @@ -1,56 +1,56 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Drawing; using System.Linq; using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; namespace Switchie { - public class WindowManager { - static List hWndBlacklist = new List(); - - static int GetWindowZOrder(IntPtr hWnd) - { - var zOrder = -1; - while ((hWnd = WinAPI.GetWindow(hWnd, WinAPI.GW_HWNDNEXT)) != IntPtr.Zero) zOrder++; - return zOrder; - } + static readonly List hWndBlacklist = new List(); + static readonly string[] classBlacklist = new string[] { + "Windows.UI.Core.CoreWindow" // Start menu + }; public static List GetOpenWindows() { - List rv = new List(); - IntPtr shellWindow = WinAPI.GetShellWindow(); - string[] classBlacklist = new string[] { - "Windows.UI.Core.CoreWindow" - }; + var windowList = new List(); + var shellWindow = WinAPI.GetShellWindow(); WinAPI.EnumWindows((IntPtr hWnd, int lParam) => { - if (hWndBlacklist.Contains(hWnd)) return true; + // Ignore specific windows if (hWnd == shellWindow) return true; if (!WinAPI.IsWindowVisible(hWnd)) return true; int length = WinAPI.GetWindowTextLength(hWnd); + if (length == 0) return true; + if (hWndBlacklist.Contains(hWnd)) return true; - StringBuilder builder = new StringBuilder(length); - WinAPI.GetWindowText(hWnd, builder, length + 1); + var className = new StringBuilder(256); + IntPtr nRet = WinAPI.GetClassName(hWnd, className, className.Capacity); + if (classBlacklist.Contains(className.ToString())) return true; - WinAPI.RECT rct = new WinAPI.RECT(); - WinAPI.GetWindowRect(hWnd, ref rct); + // Get required window data + var titleBuilder = new StringBuilder(length); + WinAPI.GetWindowText(hWnd, titleBuilder, length + 1); - IntPtr nRet; - StringBuilder className = new StringBuilder(256); - nRet = WinAPI.GetClassName(hWnd, className, className.Capacity); - if (classBlacklist.Contains(className.ToString())) return true; + var rect = new WinAPI.RECT(); + WinAPI.GetWindowRect(hWnd, ref rect); + // Get virtual desktop index int index = 0; WinAPI.GetWindowThreadProcessId(hWnd, out uint pid); try { index = WindowsVirtualDesktopManager.GetInstance().FromDesktop(WindowsVirtualDesktopManager.GetInstance().FromWindow((IntPtr)hWnd)); } catch { + // Note: This is where Exception thrown: 'System.Runtime.InteropServices.COMException' comes from + // All windows where this happens are getting blacklisted hWndBlacklist.Add(hWnd); return true; } @@ -60,23 +60,24 @@ public static List GetOpenWindows() if (hIcon == 0) { hIcon = WinAPI.GetClassLongPtr(hWnd, WinAPI.GCL_HICON); } if (hIcon == 0) { hIcon = WinAPI.LoadIcon(IntPtr.Zero, (IntPtr)WinAPI.IDI_APPLICATION); } - rv.Add(new Window() + windowList.Add(new Window() { Handle = hWnd, - Title = builder.ToString(), + Title = titleBuilder.ToString(), ProcessID = pid, Class = className.ToString(), ZOrder = GetWindowZOrder(hWnd), Icon = hIcon != 0 ? new Bitmap(Icon.FromHandle((IntPtr)hIcon).ToBitmap(), 16, 16) : null, IsActive = hWnd == WinAPI.GetForegroundWindow(), - Dimensions = new Rectangle(rct.Left, rct.Top, rct.Right - rct.Left, rct.Bottom - rct.Top), + Dimensions = new Rectangle(rect.Left, rect.Top, rect.Right - rect.Left, rect.Bottom - rect.Top), VirtualDesktopIndex = index }); return true; }, 0); - return rv; + windowList.Sort((x, y) => x.ProcessID.CompareTo(y.ProcessID)); + return windowList; } public static Window GetActiveWindow() @@ -85,6 +86,29 @@ public static Window GetActiveWindow() return GetOpenWindows().SingleOrDefault(x => x.Handle == hwnd); } - public static void SetAlwaysOnTop(IntPtr handle, bool value) => WinAPI.SetWindowPos(handle, value ? WinAPI.HWND_TOPMOST : WinAPI.HWND_NOTOPMOST, 0, 0, 0, 0, WinAPI.SWP_NOMOVE | WinAPI.SWP_NOSIZE | WinAPI.SWP_SHOWWINDOW); + public static void SetAlwaysOnTop(IntPtr handle, bool value) + { + WinAPI.SetWindowPos(handle, value ? WinAPI.HWND_TOPMOST : WinAPI.HWND_NOTOPMOST, + 0, 0, 0, 0, WinAPI.SWP_NOMOVE | WinAPI.SWP_NOSIZE | WinAPI.SWP_SHOWWINDOW + ); + } + + public static void RestoreWindow(IntPtr windowHandle) + { + //WinAPI.ShowWindowAsync(windowHandle, WinAPI.SW_RESTORE); + WinAPI.ShowWindowAsync(windowHandle, WinAPI.SW_SHOWNOACTIVATE); + WinAPI.SetWindowPos(windowHandle, WinAPI.HWND_TOPMOST, + 0, 0, 0, 0, + WinAPI.SWP_NOMOVE | WinAPI.SWP_NOSIZE | WinAPI.SWP_NOACTIVATE); + } + + static int GetWindowZOrder(IntPtr hWnd) + { + var zOrder = -1; + while ((hWnd = WinAPI.GetWindow(hWnd, WinAPI.GW_HWNDNEXT)) != IntPtr.Zero) zOrder++; + return zOrder; + } + + } } diff --git a/src/Core/WindowsVersion.cs b/src/Core/WindowsVersion.cs index 89b80a5..cc2d624 100644 --- a/src/Core/WindowsVersion.cs +++ b/src/Core/WindowsVersion.cs @@ -1,6 +1,5 @@ namespace Switchie { - public class WindowsVersion { public int Major { get; set; } @@ -21,9 +20,8 @@ public WindowsVersion() // Microsoft Windows [Version 10.0.22000.282] (Windows 11) // Microsoft Windows [Version 10.0.19043.1288] (Windows 10) // Microsoft Windows [Version 10.0.17763.2237] (Windows 10 LTSC) - public bool IsWin11() => Major == 10 && Minor == 0 && Build >= 22000 && Name == "21H2"; + public bool IsWin11() => Major == 10 && Minor == 0 && Build >= 22000;// && Name == "21H2"; public bool IsWin10() => Major == 10 && Minor == 0 && Build > 17763 && Build < 22000; public bool IsWin10LTSC() => Major == 10 && Minor == 0 && Build <= 17763; } - } \ No newline at end of file diff --git a/src/GUI/AppSettings.cs b/src/GUI/AppSettings.cs new file mode 100644 index 0000000..37bfced --- /dev/null +++ b/src/GUI/AppSettings.cs @@ -0,0 +1,30 @@ +using System.Drawing; + +namespace Switchie +{ + public class AppSettings + { + public int RenderMode { get; set; } = 0; + public int DesktopBorderStyle { get; set; } = 0; + public double BackgroundOpacity { get; set; } = 1.0; + public int PagerHeight { get; set; } = 40; + public int PaddingSize { get; set; } = 1; + public int IconPaddingX { get; set; } = 0; + public int IconPaddingY { get; set; } = 0; + + public Color BackgroundColor { get; set; } = Color.FromArgb(64, 64, 64); + + public Color DesktopBorderColor { get; set; } = Color.FromArgb(32, 32, 32); + public Color ActiveDesktopBorderColor { get; set; } = Color.LightBlue; + + public Color WindowColor { get; set; } = Color.FromArgb(255, Color.Gray); + public Color ActiveWindowColor { get; set; } = Color.FromArgb(255, Color.Silver); + public Color WindowBorderColor { get; set; } = Color.Silver; + public Color ActiveWindowBorderColor { get; set; } = Color.White; + + public int PrimaryUpdateDelay { get; set; } = 200; + public int SecondaryUpdateDelay { get; set; } = 500; + public bool ShowAppInTaskbar { get; set; } = true; + } +} + diff --git a/src/GUI/AppSettingsStore.cs b/src/GUI/AppSettingsStore.cs new file mode 100644 index 0000000..d652a97 --- /dev/null +++ b/src/GUI/AppSettingsStore.cs @@ -0,0 +1,96 @@ +using Microsoft.Win32; +using System; +using System.Drawing; +using System.Globalization; + +namespace Switchie +{ + public static class AppSettingsStore + { + private const string RegistryKeyPath = @"SOFTWARE\Switchie"; + + public static AppSettings Load() + { + var defaults = new AppSettings(); + using (RegistryKey key = Registry.CurrentUser.OpenSubKey(RegistryKeyPath)) + { + return key == null ? defaults : new AppSettings + { + RenderMode = ReadInt(key, "RenderMode", defaults.RenderMode), + DesktopBorderStyle = ReadInt(key, "DesktopBorderStyle", defaults.DesktopBorderStyle), + BackgroundOpacity = ReadDouble(key, "BackgroundOpacity", defaults.BackgroundOpacity), + PagerHeight = ReadInt(key, "PagerHeight", defaults.PagerHeight), + PaddingSize = ReadInt(key, "PaddingSize", defaults.PaddingSize), + IconPaddingX = ReadInt(key, "IconPaddingX", defaults.IconPaddingX), + IconPaddingY = ReadInt(key, "IconPaddingY", defaults.IconPaddingY), + + BackgroundColor = ReadColor(key, "BackgroundColor", defaults.BackgroundColor), + + DesktopBorderColor = ReadColor(key, "DesktopBorderColor", defaults.DesktopBorderColor), + ActiveDesktopBorderColor = ReadColor(key, "ActiveDesktopBorderColor", defaults.ActiveDesktopBorderColor), + + WindowColor = ReadColor(key, "WindowColor", defaults.WindowColor), + ActiveWindowColor = ReadColor(key, "ActiveWindowColor", defaults.ActiveWindowColor), + WindowBorderColor = ReadColor(key, "WindowBorderColor", defaults.WindowBorderColor), + ActiveWindowBorderColor = ReadColor(key, "ActiveWindowBorderColor", defaults.ActiveWindowBorderColor), + + PrimaryUpdateDelay = ReadInt(key, "PrimaryUpdateDelay", defaults.PrimaryUpdateDelay), + SecondaryUpdateDelay = ReadInt(key, "SecondaryUpdateDelay", defaults.SecondaryUpdateDelay), + ShowAppInTaskbar = ReadInt(key, "ShowAppInTaskbar", defaults.ShowAppInTaskbar ? 1 : 0) == 1 + }; + } + } + + public static void Save(AppSettings settings) + { + using (RegistryKey key = Registry.CurrentUser.CreateSubKey(RegistryKeyPath)) + { + if (key == null) return; + key.SetValue("RenderMode", settings.RenderMode, RegistryValueKind.DWord); + key.SetValue("DesktopBorderStyle", settings.DesktopBorderStyle, RegistryValueKind.DWord); + key.SetValue("BackgroundOpacity", settings.BackgroundOpacity.ToString(CultureInfo.InvariantCulture), RegistryValueKind.String); + key.SetValue("PagerHeight", settings.PagerHeight, RegistryValueKind.DWord); + key.SetValue("PaddingSize", settings.PaddingSize, RegistryValueKind.DWord); + key.SetValue("IconPaddingX", settings.IconPaddingX, RegistryValueKind.DWord); + key.SetValue("IconPaddingY", settings.IconPaddingY, RegistryValueKind.DWord); + + key.SetValue("BackgroundColor", settings.BackgroundColor.ToArgb(), RegistryValueKind.DWord); + + key.SetValue("DesktopBorderColor", settings.DesktopBorderColor.ToArgb(), RegistryValueKind.DWord); + key.SetValue("ActiveDesktopBorderColor", settings.ActiveDesktopBorderColor.ToArgb(), RegistryValueKind.DWord); + + key.SetValue("WindowColor", settings.WindowColor.ToArgb(), RegistryValueKind.DWord); + key.SetValue("ActiveWindowColor", settings.ActiveWindowColor.ToArgb(), RegistryValueKind.DWord); + key.SetValue("WindowBorderColor", settings.WindowBorderColor.ToArgb(), RegistryValueKind.DWord); + key.SetValue("ActiveWindowBorderColor", settings.ActiveWindowBorderColor.ToArgb(), RegistryValueKind.DWord); + + key.SetValue("PrimaryUpdateDelay", settings.PrimaryUpdateDelay, RegistryValueKind.DWord); + key.SetValue("SecondaryUpdateDelay", settings.SecondaryUpdateDelay, RegistryValueKind.DWord); + key.SetValue("ShowAppInTaskbar", settings.ShowAppInTaskbar ? 1 : 0, RegistryValueKind.DWord); + } + } + + private static int ReadInt(RegistryKey key, string valueName, int fallback) + { + object value = key.GetValue(valueName); + return value == null ? fallback : int.TryParse(value.ToString(), out int result) ? result : fallback; + } + + private static Color ReadColor(RegistryKey key, string valueName, Color fallback) + { + object value = key.GetValue(valueName); + return value == null ? fallback : int.TryParse(value.ToString(), out int argb) ? Color.FromArgb(argb) : fallback; + } + + private static double ReadDouble(RegistryKey key, string valueName, double fallback) + { + object value = key.GetValue(valueName); + return value == null + ? fallback + : double.TryParse(value.ToString(), NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out double result) + ? result + : fallback; + } + } +} + diff --git a/src/GUI/AppSettingsWindow.xaml b/src/GUI/AppSettingsWindow.xaml new file mode 100644 index 0000000..11ff0b7 --- /dev/null +++ b/src/GUI/AppSettingsWindow.xaml @@ -0,0 +1,235 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Render Mode + +