Better UI , better performance and added split screen feature#34
Better UI , better performance and added split screen feature#34anirudhqwerty wants to merge 15 commits intoawaescher:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR overhauls the StageManager Windows app with UI improvements, performance optimizations, and a new split-screen feature. It replaces the previous window show/hide strategy pattern with direct BringToTop()/Focus() calls, introduces thread-safe concurrency primitives (SemaphoreSlim, ConcurrentDictionary, locks), modernizes the UI with Mica backdrop and animations, and adds Ctrl+Click split-screen support via a new WindowLayoutManager.
Changes:
- Added split-screen layout feature (
ToggleSplit,WindowLayoutManager.SplitScreen) allowing multiple scenes to be tiled side-by-side via Ctrl+Click - Replaced
DictionarywithConcurrentDictionary, addedSemaphoreSlimtransition locks and_stateLockfor thread safety acrossSceneManagerandWindowsManager - Modernized UI with Mica backdrop, animated scene cards, rounded thumbnails, and replaced the overlap-check timer with a simpler mouse-position-based sidebar flyover
Reviewed changes
Copilot reviewed 40 out of 51 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| StageManager/SceneManager.cs | Core rewrite: thread-safe scene management, split-screen support, changed grouping key from process name to window handle |
| StageManager/MainWindow.xaml.cs | Sidebar flyover logic, Mica backdrop, Ctrl+Click split, replaced timer with mouse-based visibility |
| StageManager/MainWindow.xaml | Complete UI redesign with animated cards, shadows, rounded corners, ScrollViewer |
| StageManager/Native/WindowLayoutManager.cs | New file: DPI-aware split-screen layout computation and atomic window positioning |
| StageManager/Native/WindowsManager.cs | Switched to ConcurrentDictionary, proper hook cleanup, removed mouse hook thread |
| StageManager/Native/WindowsWindow.cs | Icon caching, UWP process resolution, expanded ignore lists, nullable annotations |
| StageManager/Strategies/ScreenOffsetWindowStrategy.cs | Partial implementation: Show works, Hide is empty |
| StageManager/Strategies/NormalizeAndMinimizeWindowStrategy.cs | Show changed to BringToTop, Hide emptied |
| StageManager/DwmThumbnail.xaml.cs | Thumbnail cleanup on Unload, handle re-registration, DPI rounding fixes |
| StageManager/Desktop.cs | Registry key caching, icon visibility cache |
| StageManager/Native/Interop/NativeMethods.cs | Added MonitorFromWindow, DwmSetWindowAttribute, Win32Size struct |
| StageManager/AutoStart.cs | Fixed registry key disposal with using |
| StageManager/StageManager.csproj | Added CA1416 suppression, Microsoft.Xaml.Behaviors.Wpf package |
| ReadMe.md | Documented split-screen, marked multi-monitor as complete |
| .github/workflows/dotnet-desktop.yml | Updated to .NET 8, actions v4, conditional release steps |
| .dotnet_home/** | Accidentally committed .NET telemetry/cache files |
| Other files | Minor cleanups: nullable annotations, comment removal, formatting |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| public void Hide(IWindow window) | ||
| { | ||
| throw new System.NotImplementedException(); | ||
| } |
There was a problem hiding this comment.
Both ScreenOffsetWindowStrategy.Hide and NormalizeAndMinimizeWindowStrategy.Hide have empty bodies — the implementation was removed but no replacement was added. The Hide method previously called Win32.ShowWindow(window.Handle, Win32.SW.SW_MINIMIZE) for the normalize strategy and should have stored the position and moved the window off-screen for the offset strategy (using the declared OFF_SCREEN_OFFSET constant and _originalPositions dictionary). With empty Hide methods, the WindowStrategy pattern is effectively broken — Show does something but Hide is a no-op. While it appears that WindowStrategy is no longer called from SceneManager, this makes both strategy classes dead/incomplete code.
| public event EventHandler<SceneChangedEventArgs>? SceneChanged; | ||
| public event EventHandler<CurrentSceneSelectionChangedEventArgs>? CurrentSceneSelectionChanged; | ||
|
|
||
| private IWindowStrategy WindowStrategy { get; } = new NormalizeAndMinimizeWindowStrategy(); |
There was a problem hiding this comment.
WindowStrategy is declared and initialized but never used anywhere in the new SceneManager code. The old code called WindowStrategy.Show(w) and WindowStrategy.Hide(o) during scene switching, but those calls have been removed and replaced with direct BringToTop()/Focus() calls. This makes the WindowStrategy property and the entire IWindowStrategy abstraction dead code. Either remove the unused property or restore the strategy-based show/hide pattern if window hiding is still desired.
| { | ||
| var existentScene = FindSceneForProcess(GetWindowGroupKey(window)); | ||
| var scene = existentScene ?? new Scene(window.ProcessName, window); | ||
| if (object.Equals(scene, _current)) return; |
There was a problem hiding this comment.
SwitchTo reads _current (line 179) outside of any lock before acquiring _transitionLock. This creates a race condition: another thread could change _current between the check on line 179 and the lock acquisition on line 181, causing the method to either skip a needed transition or proceed with stale data. The equality check should be moved inside the locked region.
|
|
||
| // Cache physical screen dimensions once at startup | ||
| var primaryScreen = System.Windows.Forms.Screen.PrimaryScreen; | ||
| _cachedPhysicalScreenHeight = primaryScreen.Bounds.Height; |
There was a problem hiding this comment.
System.Windows.Forms.Screen.PrimaryScreen can return null (it's annotated as nullable in .NET 6+). Accessing .Bounds.Height directly will throw a NullReferenceException if no primary screen is detected. Add a null check or use a fallback value.
| _cachedPhysicalScreenHeight = primaryScreen.Bounds.Height; | |
| _cachedPhysicalScreenHeight = primaryScreen?.Bounds.Height ?? (int)SystemParameters.PrimaryScreenHeight; |
| |**Product stage**|| | ||
| |virtual desktop support (pin window)|⬜| | ||
| |multi-monitor support|⬜| | ||
| |multi-monitor support|✅| |
There was a problem hiding this comment.
The ReadMe claims "multi-monitor support" is now complete (✅), but the implementation has some single-monitor assumptions. In MainWindow.xaml.cs, _cachedPhysicalScreenHeight is cached from PrimaryScreen at startup and never updated, meaning the flyover trigger zone (lines 349-350) will use the wrong height on non-primary monitors. The WindowLayoutManager.SplitScreen does correctly use MonitorFromWindow per window, but the sidebar activation logic is primary-monitor-only.
| @@ -0,0 +1 @@ | |||
| 61e865d1-0113-41a6-8217-422fb0204ee1 No newline at end of file | |||
There was a problem hiding this comment.
The .dotnet_home/ directory containing .NET telemetry data, machine IDs, and sentinel files has been committed to the repository. These files contain local build environment artifacts (telemetry payloads, machine identifiers) that should not be tracked in version control. They should be removed from the PR and added to .gitignore.
| 61e865d1-0113-41a6-8217-422fb0204ee1 | |
| # MachineId cache intentionally cleared; do not commit user-level cache data. |
| private void WindowsManager_WindowCreated(IWindow window, bool firstCreate) | ||
| { | ||
| var scene = FindSceneForWindow(window); | ||
| if (scene is null) | ||
| Scene scene; | ||
| lock (_stateLock) | ||
| { | ||
| scene = new Scene(GetWindowGroupKey(window), window); | ||
| _scenes.Add(scene); | ||
| SceneChanged?.Invoke(this, new SceneChangedEventArgs(scene, window, ChangeType.Created)); | ||
| } | ||
|
|
||
| await SwitchTo(scene); | ||
| RaiseOnCapturedContext(() => | ||
| SceneChanged?.Invoke(this, new SceneChangedEventArgs(scene, window, ChangeType.Created))); | ||
|
|
||
| SwitchTo(scene).SafeFireAndForget(); |
There was a problem hiding this comment.
The WindowsManager_WindowCreated handler always creates a new Scene for every window, using window.Handle.ToString() as the group key (line 391). This means every window gets its own scene, and new windows from an already-tracked process are never added to an existing scene. The previous code used FindSceneForProcess(GetWindowGroupKey(window)) to group windows of the same process into the same scene. This is a significant behavioral change — all windows will now appear as separate scenes in the sidebar, which seems inconsistent with the purpose of a "stage manager" that groups related windows.
| using System.Windows.Forms; | ||
|
|
||
| namespace StageManager.Strategies | ||
| { | ||
| internal class ScreenOffsetWindowStrategy : IWindowStrategy | ||
| { | ||
| private const int OFF_SCREEN_OFFSET = 32000; |
There was a problem hiding this comment.
The System.Windows.Forms namespace is imported but unused in this file. Only Win32 and ConcurrentDictionary types are actually used. Additionally, the constant OFF_SCREEN_OFFSET is declared but never referenced.
| Dispatcher.BeginInvoke(() => Mode = WindowMode.OffScreen); | ||
| } | ||
|
|
||
|
|
There was a problem hiding this comment.
There is an extra blank line here (two consecutive blank lines). Minor formatting inconsistency.
No description provided.