From 7f0402affb23d85cc7dcfcd40b63de1c690f068e Mon Sep 17 00:00:00 2001 From: Robin S Date: Thu, 19 Mar 2026 21:39:07 +0100 Subject: [PATCH 01/16] version with desktop change issues --- .gitignore | 5 +- src/Core/API/VirtualDesktopAPI.cs | 1 - src/Core/API/WinAPI.cs | 87 ++++++-- src/Core/API/WinEventHook.cs | 41 ++++ src/Core/Helpers.cs | 1 - src/Core/Types.cs | 11 +- src/Core/WindowManager.cs | 53 +++-- src/Core/WindowsVersion.cs | 2 - src/GUI/MainForm.cs | 334 ++++++++++++++++++++++++++---- src/GUI/Utilities.cs | 48 +++++ src/GUI/VirtualDesktop.cs | 90 ++++++-- src/GUI/VirtualDesktopScreen.cs | 127 ++++++++---- 12 files changed, 665 insertions(+), 135 deletions(-) create mode 100644 src/Core/API/WinEventHook.cs create mode 100644 src/GUI/Utilities.cs 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/src/Core/API/VirtualDesktopAPI.cs b/src/Core/API/VirtualDesktopAPI.cs index 97152bd..fb9e7ae 100644 --- a/src/Core/API/VirtualDesktopAPI.cs +++ b/src/Core/API/VirtualDesktopAPI.cs @@ -1,7 +1,6 @@ using System; namespace Switchie { - public class WindowsVirtualDesktop { private static IWindowsVirtualDesktop _instance; diff --git a/src/Core/API/WinAPI.cs b/src/Core/API/WinAPI.cs index f91fd81..9654bf7 100644 --- a/src/Core/API/WinAPI.cs +++ b/src/Core/API/WinAPI.cs @@ -17,9 +17,16 @@ public struct RECT 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 +49,68 @@ 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 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); // 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..0cf91c7 100644 --- a/src/Core/Helpers.cs +++ b/src/Core/Helpers.cs @@ -6,7 +6,6 @@ namespace Switchie { - public class Helpers { public static byte[] GetResourceFromAssembly(Type type, string name) 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..079abe1 100644 --- a/src/Core/WindowManager.cs +++ b/src/Core/WindowManager.cs @@ -1,15 +1,18 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Drawing; using System.Linq; using System.Text; namespace Switchie { - public class WindowManager { - static List hWndBlacklist = new List(); + static readonly List hWndBlacklist = new List(); + static readonly string[] classBlacklist = new string[] { + "Windows.UI.Core.CoreWindow" // Start menu + }; static int GetWindowZOrder(IntPtr hWnd) { @@ -20,37 +23,39 @@ static int GetWindowZOrder(IntPtr hWnd) 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 +65,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 +91,11 @@ 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 + ); + } } } diff --git a/src/Core/WindowsVersion.cs b/src/Core/WindowsVersion.cs index 89b80a5..65eff13 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; } @@ -25,5 +24,4 @@ public WindowsVersion() 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/MainForm.cs b/src/GUI/MainForm.cs index 509f5da..802112c 100644 --- a/src/GUI/MainForm.cs +++ b/src/GUI/MainForm.cs @@ -1,9 +1,12 @@ -using System; +using Microsoft.Win32; +using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Drawing; using System.IO; using System.Linq; +using System.Text; using System.Threading.Tasks; using System.Windows.Forms; @@ -16,74 +19,221 @@ public class MainForm : Form private int _activeDesktopIndex = 0; private bool _forceAlwaysOnTop = false; private string _windowsHash = string.Empty; + private List _virtualDesktops = new List(); + private int updateDelay = 200; + public int BorderSize { get; set; } = 1; public int PagerHeight { get; set; } = 40; public bool IsDraggingWindow { get; set; } public int VirtualDesktopSpacing { get; set; } = 4; - public Color DesktopColor { get; set; } = Color.FromArgb(64, 64, 64); - public Color WindowColor { get; set; } = Color.Gray; + + public Color BackgroundColor { get; set; } = Color.FromArgb(64, 64, 64); + + public Color DesktopColor { get; set; } = Color.FromArgb(32, 32, 32); // Background inbetween desktops + + public Color WindowColor { get; set; } = Color.FromArgb(255, Color.Gray); + public Color WindowBorderColor { get; set; } = Color.Silver; - public Color ActiveWindowColor { get; set; } = Color.Silver; + + public Color ActiveWindowColor { get; set; } = Color.FromArgb(255, Color.Silver); + public Color ActiveWindowBorderColor { get; set; } = Color.White; - public Color ActiveDesktopBorderColor { get; set; } = Color.White; + + public Color ActiveDesktopBorderColor { get; set; } = Color.LightBlue; + public ConcurrentBag Windows = new ConcurrentBag(); + private WinEventHook.WinEventDelegate _proc; + private IntPtr _hook; + + public enum RenderMode + { + Thumbnails, + Icons + } + + public RenderMode WindowRenderMode { get; set; } = RenderMode.Thumbnails; + public MainForm() { SuspendLayout(); + Name = "frmMain"; + DoubleBuffered = true; AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(64)))), ((int)(((byte)(64)))), ((int)(((byte)(64))))); + + StartPosition = FormStartPosition.Manual; ClientSize = new System.Drawing.Size(1, 1); - ControlBox = false; - AllowDrop = true; MinimumSize = new System.Drawing.Size(1, 1); - StartPosition = FormStartPosition.Manual; - FormBorderStyle = System.Windows.Forms.FormBorderStyle.None; + + ControlBox = false; MaximizeBox = false; MinimizeBox = false; - Name = "frmMain"; TopMost = true; + AllowDrop = true; + + BackColor = BackgroundColor; + FormBorderStyle = System.Windows.Forms.FormBorderStyle.None; Icon = new System.Drawing.Icon(new MemoryStream(Helpers.GetResourceFromAssembly(typeof(Program), "Switchie.Resources.icon.ico"))); - Enumerable.Range(0, WindowsVirtualDesktop.GetInstance().Count).ToList().ForEach(x => - { - VirtualDesktop desktop = new VirtualDesktop(x, this, new Point(_virtualDesktops.Sum(y => y.Size.Width), 0)); - MouseUp += desktop.OnMouseUp; - MouseDown += desktop.OnMouseDown; - MouseMove += desktop.OnMouseMove; - DragOver += desktop.OnDragOver; - DragDrop += desktop.OnDragDrop; - _virtualDesktops.Add(desktop); - }); + + // Collect all virtual desktops and add mouse event listeners to them + _virtualDesktops = GetVirtualDesktopsAndAddMouseHandlers(); + + // Pager size depending on current amount of virtual desktops + // -> TODO: This doesn't get updated when amount of desktops has changed Size = new Size(_virtualDesktops.Sum(x => x.Size.Width), PagerHeight); MinimumSize = Size; MaximumSize = Size; ClientSize = Size; - Location = new System.Drawing.Point((Screen.PrimaryScreen.Bounds.Width / 2) - (Size.Width / 2), Screen.PrimaryScreen.WorkingArea.Bottom - Size.Height); + + var storedLocation = Utilties.GetLocationFromRegistry(); + if (storedLocation.HasValue) + { + Location = storedLocation.Value; + } + else + { + // Default: Centered and above Taskbar + // -> Preffered: 98, Screen.PrimaryScreen.WorkingArea.Bottom + // -> There should be some defaults options in the settings + Location = new System.Drawing.Point( + (Screen.PrimaryScreen.Bounds.Width / 2) - (Size.Width / 2), + Screen.PrimaryScreen.WorkingArea.Bottom - Size.Height + ); + } + ResumeLayout(false); Shown += OnShown; MouseUp += OnMouseUp; MouseDown += OnMouseDown; MouseMove += OnMouseMove; + + _proc = WinEventCallback; + /*_hook = WinEventHook.SetWinEventHook( + WinEventHook.EVENT_SYSTEM_FOREGROUND, + WinEventHook.EVENT_SYSTEM_FOREGROUND, + IntPtr.Zero, _proc, 0, 0, + WinEventHook.WINEVENT_OUTOFCONTEXT);*/ + } + + private void WinEventCallback( + IntPtr hWinEventHook, + uint eventType, + IntPtr hwnd, + int idObject, + int idChild, + uint dwEventThread, + uint dwmsEventTime) + { + var className = new StringBuilder(256); + IntPtr nRet = WinAPI.GetClassName(hwnd, className, className.Capacity); + Debug.WriteLine("Hook called for " + className); + + // Optional: eigenes Fenster ignorieren + if (hwnd == Handle) + return; + + /* + if (className.ToString() == "Shell_TrayWnd") + { + Task.Delay(50).ContinueWith(_ => + { + if (!IsDisposed) + { + BeginInvoke((Action)(() => RestoreWindow())); + } + }); + }*/ + } + + private void RestoreWindow() + { + WinAPI.ShowWindowAsync(Handle, WinAPI.SW_RESTORE); + WinAPI.ShowWindowAsync(Handle, WinAPI.SW_SHOWNOACTIVATE); + WinAPI.SetWindowPos(Handle, WinAPI.HWND_TOPMOST, + 0, 0, 0, 0, + WinAPI.SWP_NOMOVE | WinAPI.SWP_NOSIZE | WinAPI.SWP_NOACTIVATE); + } + + private void ResetVirtualDesktop() + { + //SuspendLayout(); + + // Remove mouse listeners for all existing desktops first before removing them + _virtualDesktops.ForEach((desktop) => + { + MouseUp -= desktop.OnMouseUp; + MouseDown -= desktop.OnMouseDown; + MouseMove -= desktop.OnMouseMove; + DragOver -= desktop.OnDragOver; + DragDrop -= desktop.OnDragDrop; + }); + + WindowsVirtualDesktop.Restart(); + WindowsVirtualDesktopManager.Restart(); + + _virtualDesktops.Clear(); + _virtualDesktops = GetVirtualDesktopsAndAddMouseHandlers(); + + // TODO: Change recognized but no pager size difference? + Debug.WriteLine(_virtualDesktops.Count); + + Size = new Size(_virtualDesktops.Sum(x => x.Size.Width), PagerHeight * 2); + MinimumSize = Size; + MaximumSize = Size; + ClientSize = Size; + + //base.Size = Size; + //Invalidate(); + //ResumeLayout(false); + } + + private List GetVirtualDesktopsAndAddMouseHandlers() + { + var virtualDesktops = new List(); + Enumerable.Range(0, WindowsVirtualDesktop.GetInstance().Count).ToList().ForEach(x => + { + VirtualDesktop desktop = new VirtualDesktop(x, this, new Point(_virtualDesktops.Sum(y => y.Size.Width), 0)); + MouseUp += desktop.OnMouseUp; + MouseDown += desktop.OnMouseDown; + MouseMove += desktop.OnMouseMove; + DragOver += desktop.OnDragOver; + DragDrop += desktop.OnDragDrop; + virtualDesktops.Add(desktop); + }); + return virtualDesktops; } private void OnShown(object sender, EventArgs e) { + // There are two asyncly started loops running as the main loop of the application. Why two? Task.Run(async () => { + // Update application state and always bring the Window to front while (!Program.ApplicationClosing.IsCancellationRequested) { Invoke(new Action(() => { try { - if (_forceAlwaysOnTop) - WindowManager.SetAlwaysOnTop(Handle, _forceAlwaysOnTop); + if (_forceAlwaysOnTop) WindowManager.SetAlwaysOnTop(Handle, _forceAlwaysOnTop); + + // Change Detection via Hash (calculated on basic parameters of all opened windows) + // -> Works now finally as expected because all windows are sorted first to remain in the same Windows = new ConcurrentBag(WindowManager.GetOpenWindows()); - var hash = $"{_activeDesktopIndex}{Windows.Sum(x => Math.Abs(x.Dimensions.X))}{Windows.Sum(x => Math.Abs(x.Dimensions.Y))}{Windows.Sum(x => x.Dimensions.Width)}{Windows.Sum(x => x.Dimensions.Height)}{string.Join("", Windows.Select(x => x.IsActive ? 1 : 0))}{string.Join("", Windows.Select(x => x.VirtualDesktopIndex))}"; + var hash = + $"{_activeDesktopIndex}" + + $"{Windows.Sum(x => Math.Abs(x.Dimensions.X))}" + + $"{Windows.Sum(x => Math.Abs(x.Dimensions.Y))}" + + $"{Windows.Sum(x => x.Dimensions.Width)}" + + $"{Windows.Sum(x => x.Dimensions.Height)}" + + $"{string.Join("", Windows.Select(x => x.IsActive ? 1 : 0))}" + + $"{string.Join("", Windows.Select(x => WinAPI.GetForegroundWindow() == x.Handle ? 1 : 0))}" + + $"{string.Join("", Windows.Select(x => x.VirtualDesktopIndex))}"; + if (hash != _windowsHash) { _windowsHash = hash; @@ -92,19 +242,28 @@ private void OnShown(object sender, EventArgs e) } catch { } })); - await Task.Delay(1); + + // Refresh rate for thumbnails by default 1, but 100 is also fine, otherwise the + // hash calculation and string concatenations happens every ms + await Task.Delay(updateDelay); // TODO: Refresh Rate einstellbar machen } }); + Task.Run(async () => { + // App windows can be pinned (in active windows overview) so they are presented on all virtual desktops + // -> But why forcing here it? while (!Program.ApplicationClosing.IsCancellationRequested) { Invoke(new Action(() => { if (!_isAppPinned) { + // TODO: Research what it does + Debug.WriteLine("App Not Pinned"); try { + // Divided by Zero error here WindowsVirtualDesktopManager.GetInstance().PinApplication(Handle); _isAppPinned = true; } @@ -114,11 +273,11 @@ private void OnShown(object sender, EventArgs e) { _activeDesktopIndex = WindowsVirtualDesktopManager.GetInstance().FromDesktop(WindowsVirtualDesktop.GetInstance().Current); Windows = new ConcurrentBag(WindowManager.GetOpenWindows()); - Invalidate(); + //Invalidate(); } catch { } })); - await Task.Delay(50); + await Task.Delay(500); // was 50, 500 also ok when Invalidate() wieder aktiv gesetzt wird } }); } @@ -129,12 +288,7 @@ private void OnMouseUp(object sender, MouseEventArgs e) Cursor = Cursors.Default; if ((e.Button & MouseButtons.Right) == MouseButtons.Right) { - _forceAlwaysOnTop = false; - ContextMenuStrip menu = new ContextMenuStrip(); - Helpers.AddMenuItem(this, menu, new ToolStripMenuItem() { Text = "About" }, () => { MessageBox.Show($"Switchie{Environment.NewLine}v1.1.5{Environment.NewLine}{Environment.NewLine}Made by darkguy2008", "About"); _forceAlwaysOnTop = true; }); - Helpers.AddMenuItem(this, menu, new ToolStripMenuItem() { Text = "Exit" }, () => { Environment.Exit(1); }); - menu.Opened += (ss, ee) => _forceAlwaysOnTop = false; - menu.Show(this, PointToClient(Cursor.Position)); + ShowContextMenu(); } } @@ -156,12 +310,122 @@ private void OnMouseMove(object sender, MouseEventArgs e) } } + private void ShowContextMenu() + { + _forceAlwaysOnTop = false; + ContextMenuStrip menu = new ContextMenuStrip(); + + ToolStripDropDown dropDown = new ToolStripDropDown(); + ToolStripDropDownButton dropDownButton = new ToolStripDropDownButton + { + Text = "Position", + AutoToolTip = false, + DropDown = dropDown, + DropDownDirection = ToolStripDropDownDirection.Right + }; + + ToolStripButton buttonRestore = new ToolStripButton("Restore", null, (s, ev) => + { + var storedLocation = Utilties.GetLocationFromRegistry(); + if (storedLocation != null) + { + Location = storedLocation.Value; + } + }); + + ToolStripButton buttonSave = new ToolStripButton("Save", null, (s, ev) => + { + Utilties.SaveLocationToRegistry(Location); + }); + + dropDown.Items.AddRange(new ToolStripItem[] { buttonRestore, buttonSave }); + menu.Items.Add(dropDownButton); + + Helpers.AddMenuItem(this, menu, + new ToolStripMenuItem() + { + Text = "Reset", + ToolTipText = "Reinitialize Virtual Desktops" + }, + () => + { + // Reinitialize Virtual Desktops on redraw failure... but doesn't work? + ResetVirtualDesktop(); + + // !: Wenn es nicht in der Taskbar ist, dann erscheint es auch nicht automatisch bei Fensterwechsel! + // -> Aber dennoch im primären Screen, von daher könnte man es möglicherweise auch nach Desktopwechsel forcieren + //base.ShowInTaskbar = false; + //base.Visible = true; + + // Hide ist nicht das Problem + /* + base.Hide(); + await Task.Delay(1000); + base.Show(); + */ + }); + + Helpers.AddMenuItem(this, menu, + new ToolStripMenuItem() + { + Text = "About" + }, + () => + { + MessageBox.Show( + $"Switchie{Environment.NewLine}v1.1.5{Environment.NewLine}{Environment.NewLine}Made by darkguy2008", "About" + ); + _forceAlwaysOnTop = true; + }); + + Helpers.AddMenuItem(this, menu, + new ToolStripMenuItem() + { + Text = "Toggle Render Mode" + }, + () => + { + WindowRenderMode = WindowRenderMode == RenderMode.Icons ? RenderMode.Thumbnails : RenderMode.Icons; + Invalidate(); + }); + + Helpers.AddMenuItem(this, menu, + new ToolStripMenuItem() + { + Text = "Exit" + }, + () => + { + Environment.Exit(1); + }); + + menu.Opened += (ss, ee) => _forceAlwaysOnTop = false; + menu.Show(this, PointToClient(Cursor.Position)); + } + protected override void OnPaint(PaintEventArgs e) { + //Debug.WriteLine("P"); + + // OK aber flackrig, an erster Stelle noch am besten, nach ForEach am schlechtesten + // -> führt im Output jedoch zu massiv: Exception thrown: 'System.Runtime.InteropServices.COMException' in Switchie.exe + // -> wen wundert's, OnPaint wird ja auch ständig aufgerufen, ich brauche es jedoch nur einmal! + //if (ShowInTaskbar) ShowInTaskbar = false; + base.OnPaint(e); - try { _virtualDesktops.ForEach(x => x.OnPaint(e)); } + + try + { + _virtualDesktops.ForEach(x => x.OnPaint(e)); + //base.Visible = true; + //base.Show(); + //Opacity = 100; // So wird wenigstens dieser Zoom Effekt unterdrückt + + } catch { + // Prüfen, ob hier bei Rendertarget Verlust ein Problem auftritt + // -> Vielleicht könnte man das per hide forcieren WindowsVirtualDesktop.Restart(); WindowsVirtualDesktopManager.Restart(); } diff --git a/src/GUI/Utilities.cs b/src/GUI/Utilities.cs new file mode 100644 index 0000000..08f3cde --- /dev/null +++ b/src/GUI/Utilities.cs @@ -0,0 +1,48 @@ +using Microsoft.Win32; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Switchie +{ + public class Utilties + { + private readonly static string registryKey = @"SOFTWARE\Switchie"; + + #region Registry Storage + public static System.Drawing.Point? GetLocationFromRegistry() + { + RegistryKey key = Registry.CurrentUser.OpenSubKey(registryKey); + if (key != null) + { + return new System.Drawing.Point( + int.Parse(key.GetValue("PagerLocationX").ToString()), + int.Parse(key.GetValue("PagerLocationY").ToString()) + ); + } + return null; + } + + public static void SaveLocationToRegistry(System.Drawing.Point location) + { + RegistryKey key = Registry.CurrentUser.CreateSubKey(registryKey); + key.SetValue("PagerLocationX", location.X); + key.SetValue("PagerLocationY", location.Y); + key.Close(); + } + + public bool getMode() + { + return true; + } + + public void setMode() + { + + } + + #endregion + } +} diff --git a/src/GUI/VirtualDesktop.cs b/src/GUI/VirtualDesktop.cs index bcd1d12..fa6f245 100644 --- a/src/GUI/VirtualDesktop.cs +++ b/src/GUI/VirtualDesktop.cs @@ -1,30 +1,45 @@ using System.Collections.Generic; +using System.Diagnostics; using System.Drawing; using System.Linq; using System.Windows.Forms; namespace Switchie { - + // Represents a Virtual Desktop Thumbnail in the Pager public class VirtualDesktop { public MainForm Form { get; set; } + public Size Size { get; set; } + public Point Location { get; set; } + public int VirtualDesktopIndex { get; set; } - public bool IsCurrentActiveDesktop { get => WindowsVirtualDesktopManager.GetInstance().FromDesktop(WindowsVirtualDesktop.GetInstance().Current) == VirtualDesktopIndex; } + + public bool IsCurrentActiveDesktop + { + get => WindowsVirtualDesktopManager.GetInstance().FromDesktop(WindowsVirtualDesktop.GetInstance().Current) == VirtualDesktopIndex; + } private DragDropData _dragDropData; private Rectangle dragBoxFromMouseDown; private List _screens = new List(); + private bool IsInsideBounds(Point p) => IsInsideBounds(p.X, p.Y); - private bool IsInsideBounds(int x, int y) => x >= Location.X && x < (Location.X + Size.Width) && y >= Location.Y && y < (Location.Y + Size.Height); + + private bool IsInsideBounds(int x, int y) => + x >= Location.X && x < (Location.X + Size.Width) && y >= Location.Y && y < (Location.Y + Size.Height); public VirtualDesktop(int virtualDesktopIndex, MainForm form, Point location) { Form = form; Location = location; VirtualDesktopIndex = virtualDesktopIndex; + + _screens.Clear(); + + // Ein Virtual Desktop beinhaltet ein komplettes Set an Screens, sofern mehrere physikalische Screens vorhanden sind foreach (var screen in Screen.AllScreens.OrderBy(x => x.Bounds.Left).ThenBy(x => x.Bounds.Top)) _screens.Add(new VirtualDesktopScreen() { @@ -37,15 +52,23 @@ public VirtualDesktop(int virtualDesktopIndex, MainForm form, Point location) Size = new Size(_screens.Sum(x => x.Size.Width), Form.PagerHeight); } - public Window GetWindowUnderCursor(Point mousePosition) + private Window GetWindowUnderCursor(Point mousePosition) { - var coord = Form.PointToClient(mousePosition); - var windows = Form.Windows.Where(x => x.VirtualDesktopIndex == VirtualDesktopIndex).OrderByDescending(x => x.ZOrder); - foreach (var w in windows) - if (_screens.Any(x => x.WindowAreas.ContainsKey(w.Handle) && x.WindowAreas[w.Handle].Contains(mousePosition))) - if (_screens.Select(x => x.AttachedScreen.DeviceName).Contains(Screen.FromHandle(w.Handle).DeviceName)) - return w; - return null; + if (Form.WindowRenderMode == MainForm.RenderMode.Thumbnails) + { + var coord = Form.PointToClient(mousePosition); + var windows = Form.Windows.Where(x => x.VirtualDesktopIndex == VirtualDesktopIndex).OrderByDescending(x => x.ZOrder); + foreach (var w in windows) + if (_screens.Any(x => x.WindowAreas.ContainsKey(w.Handle) && x.WindowAreas[w.Handle].Contains(mousePosition))) + if (_screens.Select(x => x.AttachedScreen.DeviceName).Contains(Screen.FromHandle(w.Handle).DeviceName)) + return w; + return null; + } + else + { + //var windows = Form.Windows.Where(x => x.VirtualDesktopIndex == VirtualDesktopIndex).OrderByDescending(x => x.ZOrder); + return null; + } } public void OnPaint(PaintEventArgs e) @@ -54,9 +77,18 @@ public void OnPaint(PaintEventArgs e) Graphics g = e.Graphics; Color desktopBorderColor = IsCurrentActiveDesktop ? Form.ActiveDesktopBorderColor : Form.DesktopColor; + + /* g.DrawRectangle( - new Pen(new SolidBrush(desktopBorderColor), Form.BorderSize), - new Rectangle(Location.X, Location.Y, Size.Width - (Form.BorderSize), Size.Height - (Form.BorderSize))); + new Pen(new SolidBrush(desktopBorderColor), Form.BorderSize), + new Rectangle(Location.X, Location.Y, Size.Width - (Form.BorderSize), Size.Height - (Form.BorderSize)) + );*/ + + // Just underline the active desktop + g.FillRectangle( + new SolidBrush(desktopBorderColor), + new Rectangle(Location.X, Size.Height-1, Size.Width, Size.Height) + ); } public void OnMouseUp(object sender, MouseEventArgs e) @@ -64,19 +96,49 @@ public void OnMouseUp(object sender, MouseEventArgs e) if (!IsInsideBounds(e.X, e.Y)) return; if ((e.Button & MouseButtons.Left) == MouseButtons.Left) { + Debug.WriteLine("Change to Desktop: " + VirtualDesktopIndex); + + // Test, damit Fenster nach Wechsel wieder erscheint, der Effekt ist aber st�rend + //Form.Opacity = 0; // Unterdr�cken des Zoom Effekts bei Ein/Ausblenden der Form wegen ShowInTaskbar + //Form.ShowInTaskbar = true; + WindowsVirtualDesktop.GetInstance().FromIndex(VirtualDesktopIndex).MakeVisible(); + + // Set Focus to clicked window, which is usually the topmost + // -> funktioniert logischerweise aber so nicht mit meinem alternativen Modus + /*var w = GetWindowUnderCursor(e.Location); + if (w != null) + { + Debug.WriteLine(w.Title); + WinAPI.SetForegroundWindow(w.Handle); + }*/ + Form.Invalidate(); + //Form.ShowInTaskbar = false; + //Form.Opacity = 100; } } public void OnMouseDown(object sender, MouseEventArgs e) { - if (!IsInsideBounds(e.X, e.Y)) return; + Debug.WriteLine(e.X + " " + e.Y); + + //if (!IsInsideBounds(e.X, e.Y)) return; if ((e.Button & MouseButtons.Left) == MouseButtons.Left) { var w = GetWindowUnderCursor(e.Location); if (w != null) { + + + // Sobald das aktiviert wird, habe ich den Scroll-Effekt beim Wechsel! + // -> wahrscheinlich weil hierdurch der Wechsel zuerst ausgel�st wird und nicht durch die App selbst! + //WinAPI.SetForegroundWindow(w.Handle); + + //SetFocus(w.Handle); + //w.Handle + //w. + _dragDropData = new DragDropData() { OriginDesktopIndex = VirtualDesktopIndex, diff --git a/src/GUI/VirtualDesktopScreen.cs b/src/GUI/VirtualDesktopScreen.cs index d54b185..15ad197 100644 --- a/src/GUI/VirtualDesktopScreen.cs +++ b/src/GUI/VirtualDesktopScreen.cs @@ -1,12 +1,13 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Drawing; using System.Linq; +using System.Runtime.InteropServices; using System.Windows.Forms; namespace Switchie { - public class VirtualDesktopScreen { public Size Size { get; set; } @@ -22,54 +23,100 @@ public class VirtualDesktopScreen public void OnPaint(PaintEventArgs e) { Graphics g = e.Graphics; - var windows = Form.Windows.Where(x => x.VirtualDesktopIndex == VirtualDesktop.VirtualDesktopIndex).OrderBy(x => x.ZOrder).ToArray(); + Window[] windows; + windows = Form.Windows.Where(x => x.VirtualDesktopIndex == VirtualDesktop.VirtualDesktopIndex).OrderBy(x => x.ZOrder).ToArray(); + + /* + if (Form.WindowRenderMode == MainForm.RenderMode.Thumbnails) + { + + } + else + { + windows = Form.Windows.Where(x => x.VirtualDesktopIndex == VirtualDesktop.VirtualDesktopIndex).OrderByDescending(x => x.ZOrder).ToArray(); + }*/ + + int hSpace = Size.Width / windows.Length; + int vSpace = Size.Height / windows.Length; + + // Renders the thumbnails of all windows for all desktops from back to front + // -> TODO: Different rendering modes, like when all windows are maximized (on smaller screens typical) + /* + WINDOWPLACEMENT placement = new WINDOWPLACEMENT(); + placement.length = Marshal.SizeOf(placement); + GetWindowPlacement(w.Handle, ref placement); + */ + + int wnum = windows.Length - 1; foreach (var w in windows) { + Color fillColor = w.IsActive ? Form.ActiveWindowColor : Form.WindowColor; + Color borderColor = w.IsActive ? Form.ActiveWindowBorderColor : Form.WindowBorderColor; + if (Screen.FromHandle(w.Handle).DeviceName == AttachedScreen.DeviceName) { - Color fillColor = w.IsActive ? Form.ActiveWindowColor : Form.WindowColor; - Color borderColor = w.IsActive ? Form.ActiveWindowBorderColor : Form.WindowBorderColor; - - var x = w.Dimensions.X; - var y = w.Dimensions.Y; - x -= AttachedScreen.Bounds.Left; - y -= AttachedScreen.Bounds.Top; - var area = new Rectangle(x, y, w.Dimensions.Width - Form.BorderSize, w.Dimensions.Height - Form.BorderSize); - - // Scale rectangles down to the thumbnail's desired size - var ar = Helpers.AspectRatioResize(new Size(AttachedScreen.Bounds.Width, AttachedScreen.Bounds.Height), 0, Form.PagerHeight); - float percentageWidth = (float)ar.Width * 100 / AttachedScreen.Bounds.Width; - float percentageHeight = (float)ar.Height * 100 / AttachedScreen.Bounds.Height; - - area.X = (int)(area.X * (percentageWidth / 100)); - area.Y = (int)(area.Y * (percentageHeight / 100)); - area.Width = (int)(area.Width * (percentageWidth / 100)); - area.Height = (int)(area.Height * (percentageWidth / 100)); - - area.X += Location.X; - area.Y += Location.Y; - WindowAreas[w.Handle] = area; - - // Window rectangle - g.FillRectangle(new SolidBrush(fillColor), new Rectangle(area.X, area.Y, area.Width - (Form.BorderSize), area.Height - (Form.BorderSize))); - - // Window icon - var oldBounds = e.Graphics.ClipBounds; - e.Graphics.Clip = new Region(area); - g.DrawImage(w.Icon, new Point( - (area.X + area.Width / 2) - w.Icon.Width / 2, - (area.Y + area.Height / 2) - w.Icon.Height / 2 - )); - e.Graphics.Clip = new Region(oldBounds); - - // Window border - g.DrawRectangle(new Pen(new SolidBrush(borderColor), Form.BorderSize), new Rectangle(area.X, area.Y, area.Width - (Form.BorderSize), area.Height - (Form.BorderSize))); + if (Form.WindowRenderMode == MainForm.RenderMode.Icons) + { + // TODO: Die Window-Select Methode arbeitet noch nicht mit diesem Modus! + + // Window rectangle fill + /* + g.FillRectangle( + new SolidBrush(fillColor), + new Rectangle(Location.X + wnum * hSpace, 0, hSpace, Size.Height));*/ + + g.DrawImage(w.Icon, new Point( + Location.X + wnum * hSpace + hSpace / 2 - w.Icon.Width / 2, + (Size.Height / 2) - w.Icon.Height / 2 + )); + } + else + { + var x = w.Dimensions.X; + var y = w.Dimensions.Y; + x -= AttachedScreen.Bounds.Left; + y -= AttachedScreen.Bounds.Top; + var area = new Rectangle(x, y, w.Dimensions.Width - Form.BorderSize, w.Dimensions.Height - Form.BorderSize); + + // Scale rectangles down to the thumbnail's desired size + var ar = Helpers.AspectRatioResize(new Size(AttachedScreen.Bounds.Width, AttachedScreen.Bounds.Height), 0, Form.PagerHeight); + float percentageWidth = (float)ar.Width * 100 / AttachedScreen.Bounds.Width; + float percentageHeight = (float)ar.Height * 100 / AttachedScreen.Bounds.Height; + + area.X = (int)(area.X * (percentageWidth / 100)); + area.Y = (int)(area.Y * (percentageHeight / 100)); + area.Width = (int)(area.Width * (percentageWidth / 100)); + area.Height = (int)(area.Height * (percentageWidth / 100)); + + area.X += Location.X; + area.Y += Location.Y; + WindowAreas[w.Handle] = area; + + // Window rectangle fill + g.FillRectangle( + new SolidBrush(fillColor), + new Rectangle(area.X, area.Y, area.Width - (Form.BorderSize), area.Height - (Form.BorderSize))); + + // Window icon + var oldBounds = e.Graphics.ClipBounds; + e.Graphics.Clip = new Region(area); + g.DrawImage(w.Icon, new Point( + (area.X + area.Width / 2) - w.Icon.Width / 2, + (area.Y + area.Height / 2) - w.Icon.Height / 2 + )); + e.Graphics.Clip = new Region(oldBounds); + + // Window border + g.DrawRectangle( + new Pen(new SolidBrush(borderColor), Form.BorderSize), + new Rectangle(area.X, area.Y, area.Width - (Form.BorderSize), area.Height - (Form.BorderSize))); + } + wnum--; } else continue; } } } - } \ No newline at end of file From 1a75b7a53b9988486452410ce8ff3390aa7ec285 Mon Sep 17 00:00:00 2001 From: Robin S Date: Fri, 20 Mar 2026 01:07:24 +0100 Subject: [PATCH 02/16] Some new features and a bug fix, little refactoring, see changelog --- src/CHANGELOG.md | 14 +++ src/Core/API/WinAPI.cs | 10 ++ src/Core/WindowManager.cs | 27 +++-- src/GUI/MainForm.cs | 204 ++++++++++++++------------------ src/GUI/RegistryAccess.cs | 51 ++++++++ src/GUI/Utilities.cs | 48 -------- src/GUI/VirtualDesktop.cs | 59 ++++----- src/GUI/VirtualDesktopScreen.cs | 4 +- 8 files changed, 206 insertions(+), 211 deletions(-) create mode 100644 src/CHANGELOG.md create mode 100644 src/GUI/RegistryAccess.cs delete mode 100644 src/GUI/Utilities.cs diff --git a/src/CHANGELOG.md b/src/CHANGELOG.md new file mode 100644 index 0000000..884077a --- /dev/null +++ b/src/CHANGELOG.md @@ -0,0 +1,14 @@ +# Switchie Changelog + +## 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/src/Core/API/WinAPI.cs b/src/Core/API/WinAPI.cs index 9654bf7..60abe97 100644 --- a/src/Core/API/WinAPI.cs +++ b/src/Core/API/WinAPI.cs @@ -15,6 +15,13 @@ 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; @@ -60,6 +67,9 @@ public struct RECT [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); diff --git a/src/Core/WindowManager.cs b/src/Core/WindowManager.cs index 079abe1..77d7b07 100644 --- a/src/Core/WindowManager.cs +++ b/src/Core/WindowManager.cs @@ -4,6 +4,8 @@ using System.Drawing; using System.Linq; using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; namespace Switchie { @@ -14,13 +16,6 @@ public class WindowManager "Windows.UI.Core.CoreWindow" // Start menu }; - static int GetWindowZOrder(IntPtr hWnd) - { - var zOrder = -1; - while ((hWnd = WinAPI.GetWindow(hWnd, WinAPI.GW_HWNDNEXT)) != IntPtr.Zero) zOrder++; - return zOrder; - } - public static List GetOpenWindows() { var windowList = new List(); @@ -97,5 +92,23 @@ public static void SetAlwaysOnTop(IntPtr handle, bool value) 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/GUI/MainForm.cs b/src/GUI/MainForm.cs index 802112c..94eba8e 100644 --- a/src/GUI/MainForm.cs +++ b/src/GUI/MainForm.cs @@ -12,41 +12,42 @@ namespace Switchie { + // Main Application Form public class MainForm : Form { - private Point dragOffset; + private string version = "1.2.0"; + private bool _isAppPinned = false; private int _activeDesktopIndex = 0; + private int currentDesktopCount = 0; private bool _forceAlwaysOnTop = false; private string _windowsHash = string.Empty; - private List _virtualDesktops = new List(); + private readonly List _virtualDesktops = new List(); + + private Point dragOffset; + public bool IsDraggingWindow { get; set; } - private int updateDelay = 200; + private readonly int primaryUpdateDelay = 200; + private readonly int secondaryUpdateDelay = 500; public int BorderSize { get; set; } = 1; public int PagerHeight { get; set; } = 40; - public bool IsDraggingWindow { get; set; } public int VirtualDesktopSpacing { get; set; } = 4; - public Color BackgroundColor { get; set; } = Color.FromArgb(64, 64, 64); - public Color DesktopColor { get; set; } = Color.FromArgb(32, 32, 32); // Background inbetween desktops - + public Color BackgroundColor { get; set; } = Color.FromArgb(64, 64, 64); public Color WindowColor { get; set; } = Color.FromArgb(255, Color.Gray); - public Color WindowBorderColor { get; set; } = Color.Silver; public Color ActiveWindowColor { get; set; } = Color.FromArgb(255, Color.Silver); - public Color ActiveWindowBorderColor { get; set; } = Color.White; - public Color ActiveDesktopBorderColor { get; set; } = Color.LightBlue; public ConcurrentBag Windows = new ConcurrentBag(); - private WinEventHook.WinEventDelegate _proc; - private IntPtr _hook; + private readonly WinEventHook.WinEventDelegate _proc; + private readonly IntPtr _hook; public enum RenderMode { @@ -66,9 +67,6 @@ public MainForm() AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; StartPosition = FormStartPosition.Manual; - ClientSize = new System.Drawing.Size(1, 1); - MinimumSize = new System.Drawing.Size(1, 1); - ControlBox = false; MaximizeBox = false; MinimizeBox = false; @@ -80,30 +78,18 @@ public MainForm() Icon = new System.Drawing.Icon(new MemoryStream(Helpers.GetResourceFromAssembly(typeof(Program), "Switchie.Resources.icon.ico"))); // Collect all virtual desktops and add mouse event listeners to them - _virtualDesktops = GetVirtualDesktopsAndAddMouseHandlers(); + GetVirtualDesktopsAndAddMouseHandlers(_virtualDesktops); // Pager size depending on current amount of virtual desktops - // -> TODO: This doesn't get updated when amount of desktops has changed Size = new Size(_virtualDesktops.Sum(x => x.Size.Width), PagerHeight); MinimumSize = Size; MaximumSize = Size; ClientSize = Size; - var storedLocation = Utilties.GetLocationFromRegistry(); - if (storedLocation.HasValue) - { - Location = storedLocation.Value; - } - else - { - // Default: Centered and above Taskbar - // -> Preffered: 98, Screen.PrimaryScreen.WorkingArea.Bottom - // -> There should be some defaults options in the settings - Location = new System.Drawing.Point( - (Screen.PrimaryScreen.Bounds.Width / 2) - (Size.Width / 2), - Screen.PrimaryScreen.WorkingArea.Bottom - Size.Height - ); - } + WindowRenderMode = (RenderMode)RegistryAccess.getRenderMode(); + + var storedLocation = RegistryAccess.RestoreLocation(); + Location = storedLocation ?? getDefaultLocation(); ResumeLayout(false); Shown += OnShown; @@ -111,7 +97,11 @@ public MainForm() MouseDown += OnMouseDown; MouseMove += OnMouseMove; - _proc = WinEventCallback; + // Start global Windows Event Hook to bring app window to front again when covered by the taskbar before + // -> only useful whenn overlapping with taskbar is required + // -> currently not used because of overlapping detection + //_proc = WinEventCallback; + /*_hook = WinEventHook.SetWinEventHook( WinEventHook.EVENT_SYSTEM_FOREGROUND, WinEventHook.EVENT_SYSTEM_FOREGROUND, @@ -119,49 +109,49 @@ public MainForm() WinEventHook.WINEVENT_OUTOFCONTEXT);*/ } - private void WinEventCallback( - IntPtr hWinEventHook, - uint eventType, - IntPtr hwnd, - int idObject, - int idChild, - uint dwEventThread, - uint dwmsEventTime) + private void WinEventCallback(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime) { var className = new StringBuilder(256); IntPtr nRet = WinAPI.GetClassName(hwnd, className, className.Capacity); Debug.WriteLine("Hook called for " + className); - // Optional: eigenes Fenster ignorieren + // Ingore own window if (hwnd == Handle) return; - /* + // Currently we pay only attention to Shell TrayWnd events if (className.ToString() == "Shell_TrayWnd") { Task.Delay(50).ContinueWith(_ => { if (!IsDisposed) { - BeginInvoke((Action)(() => RestoreWindow())); + BeginInvoke((Action)(() => WindowManager.RestoreWindow(Handle))); } }); - }*/ + } } - private void RestoreWindow() + // Default app window location: Centered and above Taskbar + private Point getDefaultLocation() => new Point((Screen.PrimaryScreen.Bounds.Width / 2) - (Size.Width / 2), Screen.PrimaryScreen.WorkingArea.Bottom - Size.Height); + + // App Window covering detection: Currently only by its center point + private bool isCovered() { - WinAPI.ShowWindowAsync(Handle, WinAPI.SW_RESTORE); - WinAPI.ShowWindowAsync(Handle, WinAPI.SW_SHOWNOACTIVATE); - WinAPI.SetWindowPos(Handle, WinAPI.HWND_TOPMOST, - 0, 0, 0, 0, - WinAPI.SWP_NOMOVE | WinAPI.SWP_NOSIZE | WinAPI.SWP_NOACTIVATE); + var rect = this.Bounds; + var point = new WinAPI.POINT + { + X = rect.Left + rect.Width / 2, + Y = rect.Top + rect.Height / 2 + }; + var hwndAtPoint = WinAPI.WindowFromPoint(point); + return hwndAtPoint != this.Handle; } + private bool hasDesktopCountChanged() => currentDesktopCount != WindowsVirtualDesktop.GetInstance().Count; + private void ResetVirtualDesktop() { - //SuspendLayout(); - // Remove mouse listeners for all existing desktops first before removing them _virtualDesktops.ForEach((desktop) => { @@ -176,27 +166,22 @@ private void ResetVirtualDesktop() WindowsVirtualDesktopManager.Restart(); _virtualDesktops.Clear(); - _virtualDesktops = GetVirtualDesktopsAndAddMouseHandlers(); + GetVirtualDesktopsAndAddMouseHandlers(_virtualDesktops); - // TODO: Change recognized but no pager size difference? - Debug.WriteLine(_virtualDesktops.Count); - - Size = new Size(_virtualDesktops.Sum(x => x.Size.Width), PagerHeight * 2); - MinimumSize = Size; - MaximumSize = Size; - ClientSize = Size; - - //base.Size = Size; - //Invalidate(); - //ResumeLayout(false); + SuspendLayout(); + var newSize = new Size(_virtualDesktops.Sum(d => d.Size.Width), PagerHeight); + ClientSize = newSize; + MinimumSize = newSize; + MaximumSize = newSize; + ResumeLayout(); } - private List GetVirtualDesktopsAndAddMouseHandlers() + private void GetVirtualDesktopsAndAddMouseHandlers(List virtualDesktops) { - var virtualDesktops = new List(); - Enumerable.Range(0, WindowsVirtualDesktop.GetInstance().Count).ToList().ForEach(x => + currentDesktopCount = WindowsVirtualDesktop.GetInstance().Count; + Enumerable.Range(0, currentDesktopCount).ToList().ForEach(d => { - VirtualDesktop desktop = new VirtualDesktop(x, this, new Point(_virtualDesktops.Sum(y => y.Size.Width), 0)); + VirtualDesktop desktop = new VirtualDesktop(d, this, new Point(_virtualDesktops.Sum(i => i.Size.Width), 0)); MouseUp += desktop.OnMouseUp; MouseDown += desktop.OnMouseDown; MouseMove += desktop.OnMouseMove; @@ -204,25 +189,23 @@ private List GetVirtualDesktopsAndAddMouseHandlers() DragDrop += desktop.OnDragDrop; virtualDesktops.Add(desktop); }); - return virtualDesktops; } private void OnShown(object sender, EventArgs e) { - // There are two asyncly started loops running as the main loop of the application. Why two? + // Primary application loop to check global window changes so the miniatures get updated Task.Run(async () => { - // Update application state and always bring the Window to front while (!Program.ApplicationClosing.IsCancellationRequested) { Invoke(new Action(() => { try { - if (_forceAlwaysOnTop) WindowManager.SetAlwaysOnTop(Handle, _forceAlwaysOnTop); + //if (_forceAlwaysOnTop) WindowManager.SetAlwaysOnTop(Handle, _forceAlwaysOnTop); // Change Detection via Hash (calculated on basic parameters of all opened windows) - // -> Works now finally as expected because all windows are sorted first to remain in the same + // -> Works now finally as expected because all windows are sorted first to remain in the same state Windows = new ConcurrentBag(WindowManager.GetOpenWindows()); var hash = $"{_activeDesktopIndex}" + @@ -245,25 +228,35 @@ private void OnShown(object sender, EventArgs e) // Refresh rate for thumbnails by default 1, but 100 is also fine, otherwise the // hash calculation and string concatenations happens every ms - await Task.Delay(updateDelay); // TODO: Refresh Rate einstellbar machen + await Task.Delay(primaryUpdateDelay); } }); + // Secondary application loop for pinning the app, bringing it to front when hidden by some other window and detect desktop count changes + // -> only pinned apps are visible on all virtual desktops, which is a requirement for this app Task.Run(async () => { - // App windows can be pinned (in active windows overview) so they are presented on all virtual desktops - // -> But why forcing here it? while (!Program.ApplicationClosing.IsCancellationRequested) { Invoke(new Action(() => { + // Check if window is covered shoulf be fine with every 500ms + if (isCovered()) + { + WindowManager.RestoreWindow(Handle); + } + + if (hasDesktopCountChanged()) + { + ResetVirtualDesktop(); + } + + // This should only be the case at start up if (!_isAppPinned) { - // TODO: Research what it does - Debug.WriteLine("App Not Pinned"); try { - // Divided by Zero error here + // Note: The internal Divided by Zero error happens here WindowsVirtualDesktopManager.GetInstance().PinApplication(Handle); _isAppPinned = true; } @@ -277,7 +270,7 @@ private void OnShown(object sender, EventArgs e) } catch { } })); - await Task.Delay(500); // was 50, 500 also ok when Invalidate() wieder aktiv gesetzt wird + await Task.Delay(secondaryUpdateDelay); } }); } @@ -315,6 +308,7 @@ private void ShowContextMenu() _forceAlwaysOnTop = false; ContextMenuStrip menu = new ContextMenuStrip(); + // --- Position related --- ToolStripDropDown dropDown = new ToolStripDropDown(); ToolStripDropDownButton dropDownButton = new ToolStripDropDownButton { @@ -324,23 +318,28 @@ private void ShowContextMenu() DropDownDirection = ToolStripDropDownDirection.Right }; - ToolStripButton buttonRestore = new ToolStripButton("Restore", null, (s, ev) => + ToolStripButton buttonRestorePos = new ToolStripButton("Restore", null, (s, ev) => { - var storedLocation = Utilties.GetLocationFromRegistry(); + var storedLocation = RegistryAccess.RestoreLocation(); if (storedLocation != null) { Location = storedLocation.Value; } }); - ToolStripButton buttonSave = new ToolStripButton("Save", null, (s, ev) => + ToolStripButton buttonSavePos = new ToolStripButton("Save", null, (s, ev) => { - Utilties.SaveLocationToRegistry(Location); + RegistryAccess.SaveLocation(Location); }); - dropDown.Items.AddRange(new ToolStripItem[] { buttonRestore, buttonSave }); + ToolStripButton buttonDefaultPos = new ToolStripButton("Default", null, (s, ev) => + { + Location = getDefaultLocation(); + }); + dropDown.Items.AddRange(new ToolStripItem[] { buttonRestorePos, buttonSavePos, buttonDefaultPos }); menu.Items.Add(dropDownButton); + // --- Aditional menu entries --- Helpers.AddMenuItem(this, menu, new ToolStripMenuItem() { @@ -349,20 +348,7 @@ private void ShowContextMenu() }, () => { - // Reinitialize Virtual Desktops on redraw failure... but doesn't work? ResetVirtualDesktop(); - - // !: Wenn es nicht in der Taskbar ist, dann erscheint es auch nicht automatisch bei Fensterwechsel! - // -> Aber dennoch im primären Screen, von daher könnte man es möglicherweise auch nach Desktopwechsel forcieren - //base.ShowInTaskbar = false; - //base.Visible = true; - - // Hide ist nicht das Problem - /* - base.Hide(); - await Task.Delay(1000); - base.Show(); - */ }); Helpers.AddMenuItem(this, menu, @@ -372,9 +358,7 @@ private void ShowContextMenu() }, () => { - MessageBox.Show( - $"Switchie{Environment.NewLine}v1.1.5{Environment.NewLine}{Environment.NewLine}Made by darkguy2008", "About" - ); + MessageBox.Show($"Switchie{Environment.NewLine}v{version}{Environment.NewLine}{Environment.NewLine}Made by darkguy2008", "About"); _forceAlwaysOnTop = true; }); @@ -386,6 +370,7 @@ private void ShowContextMenu() () => { WindowRenderMode = WindowRenderMode == RenderMode.Icons ? RenderMode.Thumbnails : RenderMode.Icons; + RegistryAccess.saveRenderMode((int)WindowRenderMode); Invalidate(); }); @@ -405,27 +390,14 @@ private void ShowContextMenu() protected override void OnPaint(PaintEventArgs e) { - //Debug.WriteLine("P"); - - // OK aber flackrig, an erster Stelle noch am besten, nach ForEach am schlechtesten - // -> führt im Output jedoch zu massiv: Exception thrown: 'System.Runtime.InteropServices.COMException' in Switchie.exe - // -> wen wundert's, OnPaint wird ja auch ständig aufgerufen, ich brauche es jedoch nur einmal! - //if (ShowInTaskbar) ShowInTaskbar = false; - base.OnPaint(e); - try { _virtualDesktops.ForEach(x => x.OnPaint(e)); - //base.Visible = true; - //base.Show(); - //Opacity = 100; // So wird wenigstens dieser Zoom Effekt unterdrückt - } catch { - // Prüfen, ob hier bei Rendertarget Verlust ein Problem auftritt - // -> Vielleicht könnte man das per hide forcieren + // TODO: Check, if we run into issues when render target is gone WindowsVirtualDesktop.Restart(); WindowsVirtualDesktopManager.Restart(); } diff --git a/src/GUI/RegistryAccess.cs b/src/GUI/RegistryAccess.cs new file mode 100644 index 0000000..8924f49 --- /dev/null +++ b/src/GUI/RegistryAccess.cs @@ -0,0 +1,51 @@ +using Microsoft.Win32; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Switchie +{ + public class RegistryAccess + { + private readonly static string registryKey = @"SOFTWARE\Switchie"; + + public static System.Drawing.Point? RestoreLocation() + { + RegistryKey key = Registry.CurrentUser.OpenSubKey(registryKey); + if (key == null) return null; + var x = key.GetValue("PagerLocationX"); + var y = key.GetValue("PagerLocationY"); + if (x == null || y == null) return null; + return new System.Drawing.Point( + int.Parse(x.ToString()), + int.Parse(y.ToString()) + ); + } + + public static void SaveLocation(System.Drawing.Point location) + { + RegistryKey key = Registry.CurrentUser.CreateSubKey(registryKey); + key.SetValue("PagerLocationX", location.X); + key.SetValue("PagerLocationY", location.Y); + key.Close(); + } + + public static int getRenderMode() + { + RegistryKey key = Registry.CurrentUser.OpenSubKey(registryKey); + if (key == null) return 0; + var renderMode = key.GetValue("RenderMode"); + if (renderMode == null) return 0; + return int.Parse(key.GetValue("RenderMode").ToString()); + } + + public static void saveRenderMode(int renderMode) + { + RegistryKey key = Registry.CurrentUser.CreateSubKey(registryKey); + key.SetValue("RenderMode", renderMode); + key.Close(); + } + } +} diff --git a/src/GUI/Utilities.cs b/src/GUI/Utilities.cs deleted file mode 100644 index 08f3cde..0000000 --- a/src/GUI/Utilities.cs +++ /dev/null @@ -1,48 +0,0 @@ -using Microsoft.Win32; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Switchie -{ - public class Utilties - { - private readonly static string registryKey = @"SOFTWARE\Switchie"; - - #region Registry Storage - public static System.Drawing.Point? GetLocationFromRegistry() - { - RegistryKey key = Registry.CurrentUser.OpenSubKey(registryKey); - if (key != null) - { - return new System.Drawing.Point( - int.Parse(key.GetValue("PagerLocationX").ToString()), - int.Parse(key.GetValue("PagerLocationY").ToString()) - ); - } - return null; - } - - public static void SaveLocationToRegistry(System.Drawing.Point location) - { - RegistryKey key = Registry.CurrentUser.CreateSubKey(registryKey); - key.SetValue("PagerLocationX", location.X); - key.SetValue("PagerLocationY", location.Y); - key.Close(); - } - - public bool getMode() - { - return true; - } - - public void setMode() - { - - } - - #endregion - } -} diff --git a/src/GUI/VirtualDesktop.cs b/src/GUI/VirtualDesktop.cs index fa6f245..d777dc1 100644 --- a/src/GUI/VirtualDesktop.cs +++ b/src/GUI/VirtualDesktop.cs @@ -2,6 +2,7 @@ using System.Diagnostics; using System.Drawing; using System.Linq; +using System.Threading.Tasks; using System.Windows.Forms; namespace Switchie @@ -39,7 +40,7 @@ public VirtualDesktop(int virtualDesktopIndex, MainForm form, Point location) _screens.Clear(); - // Ein Virtual Desktop beinhaltet ein komplettes Set an Screens, sofern mehrere physikalische Screens vorhanden sind + // Note: A virtual desktop includes a complete set of screens, if available foreach (var screen in Screen.AllScreens.OrderBy(x => x.Bounds.Left).ThenBy(x => x.Bounds.Top)) _screens.Add(new VirtualDesktopScreen() { @@ -78,13 +79,9 @@ public void OnPaint(PaintEventArgs e) Graphics g = e.Graphics; Color desktopBorderColor = IsCurrentActiveDesktop ? Form.ActiveDesktopBorderColor : Form.DesktopColor; - /* - g.DrawRectangle( - new Pen(new SolidBrush(desktopBorderColor), Form.BorderSize), - new Rectangle(Location.X, Location.Y, Size.Width - (Form.BorderSize), Size.Height - (Form.BorderSize)) - );*/ - // Just underline the active desktop + // -> TODO: Doesn't work if there is no window on the desktop + g.FillRectangle( new SolidBrush(desktopBorderColor), new Rectangle(Location.X, Size.Height-1, Size.Width, Size.Height) @@ -96,48 +93,34 @@ public void OnMouseUp(object sender, MouseEventArgs e) if (!IsInsideBounds(e.X, e.Y)) return; if ((e.Button & MouseButtons.Left) == MouseButtons.Left) { - Debug.WriteLine("Change to Desktop: " + VirtualDesktopIndex); - - // Test, damit Fenster nach Wechsel wieder erscheint, der Effekt ist aber st�rend - //Form.Opacity = 0; // Unterdr�cken des Zoom Effekts bei Ein/Ausblenden der Form wegen ShowInTaskbar - //Form.ShowInTaskbar = true; - WindowsVirtualDesktop.GetInstance().FromIndex(VirtualDesktopIndex).MakeVisible(); - - // Set Focus to clicked window, which is usually the topmost - // -> funktioniert logischerweise aber so nicht mit meinem alternativen Modus - /*var w = GetWindowUnderCursor(e.Location); - if (w != null) - { - Debug.WriteLine(w.Title); - WinAPI.SetForegroundWindow(w.Handle); - }*/ - Form.Invalidate(); - //Form.ShowInTaskbar = false; - //Form.Opacity = 100; } } public void OnMouseDown(object sender, MouseEventArgs e) { - Debug.WriteLine(e.X + " " + e.Y); - - //if (!IsInsideBounds(e.X, e.Y)) return; + if (!IsInsideBounds(e.X, e.Y)) return; if ((e.Button & MouseButtons.Left) == MouseButtons.Left) { var w = GetWindowUnderCursor(e.Location); if (w != null) { - - - // Sobald das aktiviert wird, habe ich den Scroll-Effekt beim Wechsel! - // -> wahrscheinlich weil hierdurch der Wechsel zuerst ausgel�st wird und nicht durch die App selbst! - //WinAPI.SetForegroundWindow(w.Handle); - - //SetFocus(w.Handle); - //w.Handle - //w. + // Bring a window into front by clicking on its miniature + // -> doesn't work yet for the alternative mode, probably because of GetWindowUnderCursor() + if (IsCurrentActiveDesktop) + { + WinAPI.SetForegroundWindow(w.Handle); + } + else + { + // If we come from another desktop we delay the activation so the desktop change can be applied before + // (otherwise we would scroll to the target desktop first) + Task.Delay(300).ContinueWith(_ => + { + WinAPI.SetForegroundWindow(w.Handle); + }); + } _dragDropData = new DragDropData() { @@ -186,4 +169,4 @@ public void OnDragOver(object sender, DragEventArgs e) } } -} \ No newline at end of file +} diff --git a/src/GUI/VirtualDesktopScreen.cs b/src/GUI/VirtualDesktopScreen.cs index 15ad197..7006e30 100644 --- a/src/GUI/VirtualDesktopScreen.cs +++ b/src/GUI/VirtualDesktopScreen.cs @@ -58,7 +58,7 @@ public void OnPaint(PaintEventArgs e) { if (Form.WindowRenderMode == MainForm.RenderMode.Icons) { - // TODO: Die Window-Select Methode arbeitet noch nicht mit diesem Modus! + // TODO: Window selection doesn't work in this mode yet // Window rectangle fill /* @@ -119,4 +119,4 @@ public void OnPaint(PaintEventArgs e) } } } -} \ No newline at end of file +} From b58bee541aa1fef83fe976349220840aa33c70b3 Mon Sep 17 00:00:00 2001 From: Robin S Date: Fri, 20 Mar 2026 01:20:38 +0100 Subject: [PATCH 03/16] Moved changelog to right folder --- src/CHANGELOG.md => CHANGELOG.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/CHANGELOG.md => CHANGELOG.md (100%) diff --git a/src/CHANGELOG.md b/CHANGELOG.md similarity index 100% rename from src/CHANGELOG.md rename to CHANGELOG.md From a8df23365c44174e2a44971f188a92f16f7cdbc8 Mon Sep 17 00:00:00 2001 From: Robin S Date: Fri, 20 Mar 2026 02:08:57 +0100 Subject: [PATCH 04/16] added flag to hide app from taskbar for manual pinning --- src/GUI/MainForm.cs | 19 ++++++++++++------- src/GUI/VirtualDesktopScreen.cs | 4 ++-- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/GUI/MainForm.cs b/src/GUI/MainForm.cs index 94eba8e..91d0e0d 100644 --- a/src/GUI/MainForm.cs +++ b/src/GUI/MainForm.cs @@ -17,9 +17,12 @@ public class MainForm : Form { private string version = "1.2.0"; + // This works only when the pinning is manually done by the user after application has started (because auto pinning will always fail) + private bool _showAppInTaskbar = true; + private bool _isAppPinned = false; private int _activeDesktopIndex = 0; - private int currentDesktopCount = 0; + private int _currentDesktopCount = 0; private bool _forceAlwaysOnTop = false; private string _windowsHash = string.Empty; @@ -77,6 +80,8 @@ public MainForm() FormBorderStyle = System.Windows.Forms.FormBorderStyle.None; Icon = new System.Drawing.Icon(new MemoryStream(Helpers.GetResourceFromAssembly(typeof(Program), "Switchie.Resources.icon.ico"))); + ShowInTaskbar = _showAppInTaskbar; + // Collect all virtual desktops and add mouse event listeners to them GetVirtualDesktopsAndAddMouseHandlers(_virtualDesktops); @@ -148,7 +153,7 @@ private bool isCovered() return hwndAtPoint != this.Handle; } - private bool hasDesktopCountChanged() => currentDesktopCount != WindowsVirtualDesktop.GetInstance().Count; + private bool hasDesktopCountChanged() => _currentDesktopCount != WindowsVirtualDesktop.GetInstance().Count; private void ResetVirtualDesktop() { @@ -178,8 +183,8 @@ private void ResetVirtualDesktop() private void GetVirtualDesktopsAndAddMouseHandlers(List virtualDesktops) { - currentDesktopCount = WindowsVirtualDesktop.GetInstance().Count; - Enumerable.Range(0, currentDesktopCount).ToList().ForEach(d => + _currentDesktopCount = WindowsVirtualDesktop.GetInstance().Count; + Enumerable.Range(0, _currentDesktopCount).ToList().ForEach(d => { VirtualDesktop desktop = new VirtualDesktop(d, this, new Point(_virtualDesktops.Sum(i => i.Size.Width), 0)); MouseUp += desktop.OnMouseUp; @@ -251,9 +256,9 @@ private void OnShown(object sender, EventArgs e) ResetVirtualDesktop(); } - // This should only be the case at start up - if (!_isAppPinned) - { + // This should only be the case at start up, right + if (_showAppInTaskbar && !_isAppPinned) { + try { // Note: The internal Divided by Zero error happens here diff --git a/src/GUI/VirtualDesktopScreen.cs b/src/GUI/VirtualDesktopScreen.cs index 7006e30..af889e1 100644 --- a/src/GUI/VirtualDesktopScreen.cs +++ b/src/GUI/VirtualDesktopScreen.cs @@ -58,8 +58,8 @@ public void OnPaint(PaintEventArgs e) { if (Form.WindowRenderMode == MainForm.RenderMode.Icons) { - // TODO: Window selection doesn't work in this mode yet - + // TODO: Window selection doesn't work in this mode + // TODO: There should to make it looking like in the original (user preferences) // Window rectangle fill /* g.FillRectangle( From 1169618694dc32bf8d69a7e69b0288cedccc3b01 Mon Sep 17 00:00:00 2001 From: Robbson Date: Fri, 20 Mar 2026 13:54:13 +0100 Subject: [PATCH 05/16] added settings dialog; updated about dialog; some code cleanup --- TODOS.md | 7 + src/GUI/AppSettings.cs | 19 ++ src/GUI/AppSettingsStore.cs | 57 ++++++ src/GUI/AppSettingsWindow.xaml | 112 +++++++++++ src/GUI/AppSettingsWindow.xaml.cs | 236 ++++++++++++++++++++++++ src/GUI/MainForm.cs | 296 +++++++++++++++++++++--------- src/GUI/VirtualDesktop.cs | 18 +- src/GUI/VirtualDesktopScreen.cs | 2 +- src/Switchie.csproj | 5 +- 9 files changed, 650 insertions(+), 102 deletions(-) create mode 100644 TODOS.md create mode 100644 src/GUI/AppSettings.cs create mode 100644 src/GUI/AppSettingsStore.cs create mode 100644 src/GUI/AppSettingsWindow.xaml create mode 100644 src/GUI/AppSettingsWindow.xaml.cs diff --git a/TODOS.md b/TODOS.md new file mode 100644 index 0000000..98ceb04 --- /dev/null +++ b/TODOS.md @@ -0,0 +1,7 @@ + +# Todos + +- Get it to work on more recent Windows 11 versions +- Icon View Mode should support multiple lines as well as drag & drop to other desktops +- there should be a way to make the application temporarily transparent or when hovered +- Fix render issue when there is an empty virtual desktop in the middle diff --git a/src/GUI/AppSettings.cs b/src/GUI/AppSettings.cs new file mode 100644 index 0000000..6f9329a --- /dev/null +++ b/src/GUI/AppSettings.cs @@ -0,0 +1,19 @@ +using System.Drawing; + +namespace Switchie +{ + public class AppSettings + { + public int RenderMode { get; set; } = 0; + + public int PagerHeight { get; set; } = 40; + + public Color DesktopColor { get; set; } = Color.FromArgb(32, 32, 32); + public Color BackgroundColor { get; set; } = Color.FromArgb(64, 64, 64); + public Color ActiveDesktopBorderColor { get; set; } = Color.LightBlue; + + public int PrimaryUpdateDelay { get; set; } = 200; + public int SecondaryUpdateDelay { get; set; } = 500; + } +} + diff --git a/src/GUI/AppSettingsStore.cs b/src/GUI/AppSettingsStore.cs new file mode 100644 index 0000000..5da1733 --- /dev/null +++ b/src/GUI/AppSettingsStore.cs @@ -0,0 +1,57 @@ +using Microsoft.Win32; +using System; +using System.Drawing; + +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), + PagerHeight = ReadInt(key, "PagerHeight", defaults.PagerHeight), + PrimaryUpdateDelay = ReadInt(key, "PrimaryUpdateDelay", defaults.PrimaryUpdateDelay), + SecondaryUpdateDelay = ReadInt(key, "SecondaryUpdateDelay", defaults.SecondaryUpdateDelay), + DesktopColor = ReadColor(key, "DesktopColor", defaults.DesktopColor), + BackgroundColor = ReadColor(key, "BackgroundColor", defaults.BackgroundColor), + ActiveDesktopBorderColor = ReadColor(key, "ActiveDesktopBorderColor", defaults.ActiveDesktopBorderColor) + }; + } + } + + 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("PagerHeight", settings.PagerHeight, RegistryValueKind.DWord); + key.SetValue("PrimaryUpdateDelay", settings.PrimaryUpdateDelay, RegistryValueKind.DWord); + key.SetValue("SecondaryUpdateDelay", settings.SecondaryUpdateDelay, RegistryValueKind.DWord); + key.SetValue("DesktopColor", settings.DesktopColor.ToArgb(), RegistryValueKind.DWord); + key.SetValue("BackgroundColor", settings.BackgroundColor.ToArgb(), RegistryValueKind.DWord); + key.SetValue("ActiveDesktopBorderColor", settings.ActiveDesktopBorderColor.ToArgb(), 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; + } + } +} + diff --git a/src/GUI/AppSettingsWindow.xaml b/src/GUI/AppSettingsWindow.xaml new file mode 100644 index 0000000..540195a --- /dev/null +++ b/src/GUI/AppSettingsWindow.xaml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Render Mode + +