Skip to content

Conversation

@mclt0568
Copy link

@mclt0568 mclt0568 commented Jan 7, 2026

Instead of relying on timeout (1000ms) to wait for taskbar to become stable (which in many case stabilisation takes more than a second), now relies MainWindow.ExplorerRestarting as a flag to determine if taskbar is stable

The ExplorerRestarting flag is flipped to true when WM_TASKBARCREATED is received, then flipped back to false when WaitForExplorerReadyAsync() returns, indicating its ready.

WaitForExplorerReadyAsync() works by probing taskbar every 200ms, checking if window exists (FindWindow()) and has a valid geometry. This happens until taskbar is ready or timed out (defaulted to 8000ms).

@github-actions github-actions bot added MainWindow / Media Flyout Changes to MainWindow.xaml or MainWindow.xaml.cs Taskbar Widget Changes to TaskbarWindow.xaml or TaskbarWindow.xaml.cs labels Jan 7, 2026
@unchihugo unchihugo requested a review from Copilot January 7, 2026 12:12
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR attempts to fix infinite Explorer crashing issues by replacing a timeout-based approach with a dynamic taskbar readiness detection mechanism. Instead of waiting a fixed 1000ms for the taskbar to stabilize, the code now uses a MainWindow.ExplorerRestarting flag that is set when WM_TASKBARCREATED is received and cleared when the taskbar is confirmed ready.

Key Changes:

  • Introduces ExplorerRestarting volatile flag to coordinate taskbar recovery across threads
  • Implements WaitForExplorerReadyAsync() to probe taskbar readiness every 200ms with an 8-second timeout
  • Refactors WndProc message handler to defer tray icon recreation until Explorer stabilizes
  • Simplifies error recovery logic in TaskbarWindow.UpdatePosition()

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 14 comments.

File Description
FluentFlyoutWPF/MainWindow.xaml.cs Adds explorer restart detection logic with ExplorerRestarting flag, implements async probing for taskbar readiness, and refactors tray icon recreation to use safer deferred approach
FluentFlyoutWPF/Windows/TaskbarWindow.xaml.cs Makes Win32 API methods public for cross-class access, adds early return in UpdatePosition() when explorer is restarting, and simplifies recovery logic by removing retry attempt tracking

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


private TaskbarWindow? taskbarWindow;

internal static volatile bool ExplorerRestarting = false;
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The volatile keyword alone does not guarantee thread-safety for flag operations that involve reading and writing. While volatile ensures visibility of changes across threads, it doesn't provide atomicity for read-modify-write operations. If ExplorerRestarting is checked and then set in a non-atomic way, race conditions could still occur. Consider using Interlocked operations (e.g., Interlocked.Exchange) or a lock to ensure proper synchronization, especially since this flag is being used to coordinate critical operations across multiple threads (UI thread in WndProc and background tasks in UpdatePosition).

Copilot uses AI. Check for mistakes.
Comment on lines +1355 to +1358
else
{
Logger.Warn("Explorer did not stabilize within timeout; keeping integration disabled");
}
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ExplorerRestarting flag is never reset to false when WaitForExplorerReadyAsync() times out (returns false). This means if Explorer fails to stabilize within 8 seconds, the flag remains true indefinitely, permanently disabling taskbar integration until the application is restarted. The flag should be reset even in the timeout case to allow recovery attempts.

Copilot uses AI. Check for mistakes.
Comment on lines 1292 to 1308
//private nint WndProc(nint hwnd, int msg, nint wParam, nint lParam, ref bool handled)
//{
// // Listen for TaskbarCreated message (when explorer.exe restarts)
// if (msg == WM_TASKBARCREATED)
// {
// //Logger.Info("TaskbarCreated message received - recreating tray icon");
// //nIcon.Visibility = Visibility.Collapsed; // remove tray icon

// //if (SettingsManager.Current.NIconHide)
// // return 0;

// //nIcon.Visibility = Visibility.Visible; // re-add tray icon
// //nIcon.Register();
// Logger.Debug("Restarted without tray icon stuff cus its buggy asf");
// }
// return 0;
//}
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Large block of commented-out code (30+ lines) should be removed rather than left in the codebase. Commented code creates maintenance burden and confusion. If this recovery logic needs to be preserved for reference, it should be documented in version control history or in a design document.

Suggested change
//private nint WndProc(nint hwnd, int msg, nint wParam, nint lParam, ref bool handled)
//{
// // Listen for TaskbarCreated message (when explorer.exe restarts)
// if (msg == WM_TASKBARCREATED)
// {
// //Logger.Info("TaskbarCreated message received - recreating tray icon");
// //nIcon.Visibility = Visibility.Collapsed; // remove tray icon
// //if (SettingsManager.Current.NIconHide)
// // return 0;
// //nIcon.Visibility = Visibility.Visible; // re-add tray icon
// //nIcon.Register();
// Logger.Debug("Restarted without tray icon stuff cus its buggy asf");
// }
// return 0;
//}

Copilot uses AI. Check for mistakes.
Comment on lines 417 to 434
// Logger.Warn("Taskbar Widget window handle is zero, attempting recovery...");

// Dispatcher.BeginInvoke(async () =>
// {
// await Task.Delay(1000); // delay before recovery to let taskbar stabilize
// try
// {
// _mainWindow?.RecreateTaskbarWindow();
// }
// catch (Exception ex)
// {
// Logger.Error(ex, "Failed to signal MainWindow to recover Taskbar Widget window");
// _recoveryAttempts++;
// }
// }, DispatcherPriority.Background);

// return;
//}
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Large block of commented-out code (28 lines) should be removed. The new implementation at lines 435-451 replaces this logic, so the old code serves no purpose and reduces code clarity.

Suggested change
// Logger.Warn("Taskbar Widget window handle is zero, attempting recovery...");
// Dispatcher.BeginInvoke(async () =>
// {
// await Task.Delay(1000); // delay before recovery to let taskbar stabilize
// try
// {
// _mainWindow?.RecreateTaskbarWindow();
// }
// catch (Exception ex)
// {
// Logger.Error(ex, "Failed to signal MainWindow to recover Taskbar Widget window");
// _recoveryAttempts++;
// }
// }, DispatcherPriority.Background);
// return;
//}

Copilot uses AI. Check for mistakes.
Logger.Error(ex, "Failed to signal MainWindow to recover Taskbar Widget window");
_recoveryAttempts++;
}
_mainWindow?.RecreateTaskbarWindow();
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The simplified recovery logic no longer includes error handling or recovery attempt tracking. The previous implementation had a try-catch block to handle exceptions during RecreateTaskbarWindow and tracked recovery attempts to prevent infinite loops. Without these safeguards, if RecreateTaskbarWindow() throws an exception, it could lead to unhandled exceptions in the dispatcher queue. Consider adding error handling around the RecreateTaskbarWindow call.

Suggested change
_mainWindow?.RecreateTaskbarWindow();
try
{
_mainWindow?.RecreateTaskbarWindow();
}
catch (System.Exception ex)
{
Logger.Error(ex, "Failed to recover Taskbar Widget window");
}

Copilot uses AI. Check for mistakes.
IntPtr taskbar = TaskbarWindow.FindWindow("Shell_TrayWnd", null);
if (taskbar != IntPtr.Zero)
{
if (TaskbarWindow.GetWindowRect(taskbar, out TaskbarWindow.RECT rect))
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace this call with a call to managed code if possible.

Copilot uses AI. Check for mistakes.
// --- Win32 APIs ---
[DllImport("user32.dll", SetLastError = true)]
private static extern IntPtr FindWindow(string lpClassName, string? lpWindowName);
public static extern IntPtr FindWindow(string lpClassName, string? lpWindowName);
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minimise the use of unmanaged code.

Copilot uses AI. Check for mistakes.
Comment on lines +49 to 50
public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);

Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minimise the use of unmanaged code.

Suggested change
public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
private static extern bool GetWindowRectNative(IntPtr hWnd, out RECT lpRect);
/// <summary>
/// Safely retrieves the bounding rectangle of the specified window as a managed <see cref="Rect"/>.
/// </summary>
/// <param name="hWnd">Handle to the window.</param>
/// <param name="rect">On success, receives the window rectangle.</param>
/// <returns><c>true</c> if the rectangle was retrieved successfully; otherwise, <c>false</c>.</returns>
public static bool TryGetWindowRect(IntPtr hWnd, out Rect rect)
{
rect = Rect.Empty;
if (hWnd == IntPtr.Zero)
return false;
if (!GetWindowRectNative(hWnd, out RECT nativeRect))
return false;
var width = nativeRect.Right - nativeRect.Left;
var height = nativeRect.Bottom - nativeRect.Top;
rect = new Rect(nativeRect.Left, nativeRect.Top, width, height);
return true;
}

Copilot uses AI. Check for mistakes.
Comment on lines 1316 to 1324
if (taskbar != IntPtr.Zero)
{
if (TaskbarWindow.GetWindowRect(taskbar, out TaskbarWindow.RECT rect))
{
if (rect.Right > rect.Left && rect.Bottom > rect.Top)
{
return true; // taskbar exists and has geometry
}
}
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 'if' statements can be combined.

Suggested change
if (taskbar != IntPtr.Zero)
{
if (TaskbarWindow.GetWindowRect(taskbar, out TaskbarWindow.RECT rect))
{
if (rect.Right > rect.Left && rect.Bottom > rect.Top)
{
return true; // taskbar exists and has geometry
}
}
if (taskbar != IntPtr.Zero
&& TaskbarWindow.GetWindowRect(taskbar, out TaskbarWindow.RECT rect)
&& rect.Right > rect.Left
&& rect.Bottom > rect.Top)
{
return true; // taskbar exists and has geometry

Copilot uses AI. Check for mistakes.
Comment on lines 1316 to 1324
if (taskbar != IntPtr.Zero)
{
if (TaskbarWindow.GetWindowRect(taskbar, out TaskbarWindow.RECT rect))
{
if (rect.Right > rect.Left && rect.Bottom > rect.Top)
{
return true; // taskbar exists and has geometry
}
}
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 'if' statements can be combined.

Suggested change
if (taskbar != IntPtr.Zero)
{
if (TaskbarWindow.GetWindowRect(taskbar, out TaskbarWindow.RECT rect))
{
if (rect.Right > rect.Left && rect.Bottom > rect.Top)
{
return true; // taskbar exists and has geometry
}
}
if (taskbar != IntPtr.Zero &&
TaskbarWindow.GetWindowRect(taskbar, out TaskbarWindow.RECT rect) &&
rect.Right > rect.Left &&
rect.Bottom > rect.Top)
{
return true; // taskbar exists and has geometry

Copilot uses AI. Check for mistakes.
@mclt0568
Copy link
Author

mclt0568 commented Jan 7, 2026

that was brutal 😿
tho please actually double check my changes doesn't break the app in other aspects, since im not extremely familiar with win API while attempting the fix..

@unchihugo
Copy link
Owner

Hi, thank you for contributing, I appreciate it!

Quickly looking through the code the implementation looks great, but I noticed that you left a lot of commented code in and removed some try catch blocks and logging. If possible, could you add them back in areas where needed?

Other than that, do you think we should keep ExplorerRestarting flag to true even if the 8 second loop has passed?

Thanks again!

@mclt0568
Copy link
Author

mclt0568 commented Jan 7, 2026

I've updated the comments and added the try-catch block back.

I think if taskbar isnt ready after 8 seconds is kinda sus, either the taskbar is never ready, maybe due to other plugins that modifies the taskbar and causing issue?

One known issue i observed is that an app called start11 could modify the taskbar window causing further instability, but this would be subjected case-by-case to any third party plugins and their implementation, and would be abit of a pain to just create a case for each one of them.

I think flipping the flag back to false after 8 seconds would mean that this PR essentially increases the 1000ms fixed wait time to 8000ms wait time, but allow wait to end earlier if the taskbar is ready; OR if never flipping it to false, then the taskbar features are never loaded again, maybe consider exit the program or showing a message telling the user that taskbar is buggy?

The easier solution would be yes, flipping it back to false, I think 8 seconds is more than enough for any heavy loading, but I think the ultimate decision of this would be how you imagine the app behave.

What do you think? I'll finalise the relevant changes in the next commit, depending on what we decide here.

@mclt0568
Copy link
Author

mclt0568 commented Jan 7, 2026

Or, keeping it as true, and maybe the next explorer restart externally (by the buggy plugin, or by the user manually) solves the buggy taskbar, then the taskbar window can reload properly, but never actively triggers a explorer restart by force loading TaskbarWindow, I think would be a better option?

In this case since it would be handled again when this app receives WM_TASKBARCREATED, so nothing needs to be changed in the current code i think

@unchihugo
Copy link
Owner

unchihugo commented Jan 7, 2026

Thanks for the quick implementation :)

I guess your points make sense! In that case we can keep it set to true, but for edge-case scenarios where the taskbar might take longer than 8 seconds to restart (like really slow computers), perhaps we should extend the timeout to a minute (maybe with exponential backoff if it's resource intensive?), what do you think?

EDIT: I also noticed that there's a duplicate import and the formatting could be fixed up just a little bit. Are you able to look into that as well? thanks!

@mclt0568
Copy link
Author

mclt0568 commented Jan 7, 2026

sorry for the messy code 😭, should be fixed now

i think exponential backoff would be a great idea, i might get it in later, its getting late today, for now its 1 minute timeout

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

MainWindow / Media Flyout Changes to MainWindow.xaml or MainWindow.xaml.cs Taskbar Widget Changes to TaskbarWindow.xaml or TaskbarWindow.xaml.cs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants