Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using ShortcutGuide.Models;

Expand All @@ -32,16 +33,27 @@ public static void UpdatePinnedShortcuts(string appName, ShortcutEntry shortcutE
list.Add(shortcutEntry);
}

// Persist on a best-effort basis. The in-memory pinned list is the source of truth
// for the rest of the session; failing to write should not crash the overlay
// (Pin/Unpin runs from a synchronous UI handler).
Save();
PinnedShortcutsChanged?.Invoke(null, appName);
}

public static void Save()
{
string serialized = JsonSerializer.Serialize(App.PinnedShortcuts);

string pinnedPath = SettingsUtils.Default.GetSettingsFilePath(ShortcutGuideSettings.ModuleName, "Pinned.json");
File.WriteAllText(pinnedPath, serialized);
try
{
string serialized = JsonSerializer.Serialize(App.PinnedShortcuts);
string pinnedPath = SettingsUtils.Default.GetSettingsFilePath(ShortcutGuideSettings.ModuleName, "Pinned.json");
File.WriteAllText(pinnedPath, serialized);
}
catch (Exception ex) when (ex is IOException
or UnauthorizedAccessException
or JsonException)
{
Logger.LogError("Failed to persist Shortcut Guide pinned shortcuts; keeping in-memory state.", ex);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Telemetry;
using Microsoft.UI.Xaml;
Expand All @@ -31,21 +34,39 @@ public partial class App
public App()
{
this.InitializeComponent();

// Register process-wide exception handlers so a stray exception (e.g. an IO failure
// during a fire-and-forget UI handler, or a background Task fault) gets logged
// instead of taking the overlay down with an unhandled access violation in coreclr.
// Without these the runtime tears the process down before our local catches can run.
this.UnhandledException += App_UnhandledException;
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
}

protected override void OnLaunched(LaunchActivatedEventArgs args)
{
this.LoadData();
MainWindow = new MainWindow();
TaskBarWindow = new TaskbarWindow();
MainWindow.Activate();
MainWindow.Closed += (_, _) =>
try
{
PowerToysTelemetry.Log.WriteEvent(new ShortcutGuideSessionEvent(
MainWindow.SessionDurationMs,
MainWindow.CloseType));
TaskBarWindow.Close();
};
this.LoadData();
MainWindow = new MainWindow();
TaskBarWindow = new TaskbarWindow();
MainWindow.Activate();
MainWindow.Closed += (_, _) =>
{
PowerToysTelemetry.Log.WriteEvent(new ShortcutGuideSessionEvent(
MainWindow.SessionDurationMs,
MainWindow.CloseType));
TaskBarWindow.Close();
};
}
catch (Exception ex)
{
// Any failure in launch is fatal for this short-lived overlay; log and exit
// cleanly rather than letting WinUI surface a generic crash dialog.
Logger.LogError("Failed to launch Shortcut Guide.", ex);
Environment.Exit(1);
}
}

private void LoadData()
Expand All @@ -63,18 +84,53 @@ private void LoadData()
PinnedShortcuts = loaded;
}
}
catch (JsonException)
catch (Exception ex) when (ex is JsonException
or IOException
or UnauthorizedAccessException)
{
// Fall back to the empty default if the file is corrupt.
// Fall back to the empty default if the file is corrupt or unreadable.
Logger.LogWarning($"Failed to load pinned shortcuts from '{pinnedPath}'. Falling back to empty list. Reason: {ex.Message}");
}
}

ShortcutGuideSettings = SettingsRepository<ShortcutGuideSettings>.GetInstance(settingsUtils).SettingsConfig;
ShortcutGuideProperties = ShortcutGuideSettings.Properties;

try
{
#pragma warning disable CA1869 // Cache and reuse 'JsonSerializerOptions' instances
settingsUtils.SaveSettings(JsonSerializer.Serialize(App.ShortcutGuideSettings, new JsonSerializerOptions { WriteIndented = true }), "Shortcut Guide");
settingsUtils.SaveSettings(JsonSerializer.Serialize(App.ShortcutGuideSettings, new JsonSerializerOptions { WriteIndented = true }), "Shortcut Guide");
#pragma warning restore CA1869 // Cache and reuse 'JsonSerializerOptions' instances
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
// Persisting the round-tripped settings is best-effort; the in-memory copy is still valid.
Logger.LogWarning($"Failed to persist Shortcut Guide settings on launch. Reason: {ex.Message}");
}
}

private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
{
// Exceptions raised on the UI thread land here. Mark handled so the runtime
// does not terminate the process; the overlay can usually continue.
Logger.LogError("Unhandled UI exception in Shortcut Guide.", e.Exception);
e.Handled = true;
}

private static void CurrentDomain_UnhandledException(object sender, System.UnhandledExceptionEventArgs e)
{
// Background-thread exceptions reach here as a last resort; we cannot prevent
// termination when IsTerminating is true, but at least we leave a log trail.
if (e.ExceptionObject is Exception ex)
{
Logger.LogError($"Unhandled background exception in Shortcut Guide (IsTerminating={e.IsTerminating}).", ex);
}
}

private static void TaskScheduler_UnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e)
{
Logger.LogError("Unobserved Task exception in Shortcut Guide.", e.Exception);
e.SetObserved();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -237,37 +237,54 @@ private static PathIcon CreatePathIcon(string pathData)

private void SetWindowPosition()
{
if (!this._hasMovedToRightMonitor)
try
{
NativeMethods.GetCursorPos(out NativeMethods.POINT lpPoint);
AppWindow.Move(new NativeMethods.POINT { Y = lpPoint.Y - ((int)Height / 2), X = lpPoint.X - ((int)Width / 2) });
this._hasMovedToRightMonitor = true;
}
if (!this._hasMovedToRightMonitor)
{
NativeMethods.GetCursorPos(out NativeMethods.POINT lpPoint);
AppWindow.Move(new NativeMethods.POINT { Y = lpPoint.Y - ((int)Height / 2), X = lpPoint.X - ((int)Width / 2) });
this._hasMovedToRightMonitor = true;
}

var hwnd = WindowNative.GetWindowHandle(this);
float dpi = DpiHelper.GetDPIScaleForWindow(hwnd);
Rect monitorRect = DisplayHelper.GetWorkAreaForDisplayWithWindow(hwnd);
var hwnd = WindowNative.GetWindowHandle(this);
float dpi = DpiHelper.GetDPIScaleForWindow(hwnd);
Rect monitorRect = DisplayHelper.GetWorkAreaForDisplayWithWindow(hwnd);

var windowPosition = (ShortcutGuideWindowPosition)App.ShortcutGuideProperties.WindowPosition.Value;
var taskbarWindow = App.TaskBarWindow.AppWindow;
bool taskbarOnLeft = taskbarWindow.IsVisible && taskbarWindow.Position.X < AppWindow.Position.X + Width && windowPosition == ShortcutGuideWindowPosition.Left;
bool taskbarOnRight = taskbarWindow.IsVisible && taskbarWindow.Position.X + taskbarWindow.Size.Width > AppWindow.Position.X && windowPosition == ShortcutGuideWindowPosition.Right;
var windowPosition = (ShortcutGuideWindowPosition)App.ShortcutGuideProperties.WindowPosition.Value;

double newHeight = monitorRect.Height / dpi;
if (taskbarOnLeft || taskbarOnRight)
{
newHeight -= taskbarWindow.Size.Height;
}
// App.TaskBarWindow / its AppWindow can briefly be null during the reentrant
// Hide → Activate → BringToFront chain triggered from SelectionChanged. When the
// taskbar window is not currently observable, skip the overlap adjustment instead
// of crashing the overlay (issue #48448).
var taskbarWindow = App.TaskBarWindow?.AppWindow;
bool taskbarOnLeft = false;
bool taskbarOnRight = false;
if (taskbarWindow is not null)
{
taskbarOnLeft = taskbarWindow.IsVisible && taskbarWindow.Position.X < AppWindow.Position.X + Width && windowPosition == ShortcutGuideWindowPosition.Left;
taskbarOnRight = taskbarWindow.IsVisible && taskbarWindow.Position.X + taskbarWindow.Size.Width > AppWindow.Position.X && windowPosition == ShortcutGuideWindowPosition.Right;
}

double newHeight = monitorRect.Height / dpi;
if (taskbarWindow is not null && (taskbarOnLeft || taskbarOnRight))
{
newHeight -= taskbarWindow.Size.Height;
}

MaxHeight = newHeight;
MinHeight = newHeight;
Height = newHeight;
MaxHeight = newHeight;
MinHeight = newHeight;
Height = newHeight;

int xPosition = windowPosition == ShortcutGuideWindowPosition.Right
? (int)(monitorRect.X + monitorRect.Width) - (int)(Width * dpi)
: (int)monitorRect.X;
int xPosition = windowPosition == ShortcutGuideWindowPosition.Right
? (int)(monitorRect.X + monitorRect.Width) - (int)(Width * dpi)
: (int)monitorRect.X;

this.MoveAndResize(xPosition, (int)monitorRect.Y, Width, Height);
this.MoveAndResize(xPosition, (int)monitorRect.Y, Width, Height);
}
catch (Exception ex)
{
Logger.LogError("Failed to set Shortcut Guide window position; keeping previous layout.", ex);
}
}

/// <summary>
Expand All @@ -282,25 +299,35 @@ private void WindowSelector_SelectionChanged(NavigationView sender, NavigationVi
return;
}

this._selectedAppName = selectedItem.Name;
App.CurrentAppName = this._selectedAppName;
this._shortcutFile = ManifestInterpreter.GetShortcutsOfApplication(this._selectedAppName);

App.TaskBarWindow.Hide();
if (this._shortcutFile is ShortcutFile file)
try
{
// Show the taskbar button window only when the selected app exposes the <TASKBAR1-9> section.
if (file.Shortcuts is not null && file.Shortcuts.Any(c => c.SectionName?.StartsWith("<TASKBAR1-9>", StringComparison.Ordinal) == true))
this._selectedAppName = selectedItem.Name;
App.CurrentAppName = this._selectedAppName;
this._shortcutFile = ManifestInterpreter.GetShortcutsOfApplication(this._selectedAppName);

App.TaskBarWindow?.Hide();
if (this._shortcutFile is ShortcutFile file)
{
this._taskBarWindowActivated = true;
App.TaskBarWindow.Activate();
}
// Show the taskbar button window only when the selected app exposes the <TASKBAR1-9> section.
if (file.Shortcuts is not null && file.Shortcuts.Any(c => c.SectionName?.StartsWith("<TASKBAR1-9>", StringComparison.Ordinal) == true))
{
this._taskBarWindowActivated = true;
App.TaskBarWindow?.Activate();
}

// Reposition before navigating so the taskbar window does not clip into the main window.
this.SetWindowPosition();
this.ContentFrame.Navigate(
typeof(ShortcutsPage),
new ShortcutPageNavParam { ShortcutFile = file, AppName = this._selectedAppName });
// Reposition before navigating so the taskbar window does not clip into the main window.
this.SetWindowPosition();
this.ContentFrame.Navigate(
typeof(ShortcutsPage),
new ShortcutPageNavParam { ShortcutFile = file, AppName = this._selectedAppName });
}
}
catch (Exception ex)
{
// Guard against exceptions during section navigation so the overlay does not close on the user.
// InitializeNavItemsAsync's catch interprets any exception bubbling out of the initial
// SelectedItem assignment as a fatal init failure and closes the window (issue #48448).
Logger.LogError($"Failed to handle Shortcut Guide section selection '{selectedItem.Name}'.", ex);
}
}

Expand Down
Loading
Loading