/ src / modules / cmdpal / Microsoft.CmdPal.UI / Settings / SettingsWindow.xaml.cs
SettingsWindow.xaml.cs
  1  // Copyright (c) Microsoft Corporation
  2  // The Microsoft Corporation licenses this file to you under the MIT license.
  3  // See the LICENSE file in the project root for more information.
  4  
  5  using System.Collections.ObjectModel;
  6  using CommunityToolkit.Mvvm.Messaging;
  7  using ManagedCommon;
  8  using Microsoft.CmdPal.UI.Helpers;
  9  using Microsoft.CmdPal.UI.Messages;
 10  using Microsoft.CmdPal.UI.ViewModels;
 11  using Microsoft.CmdPal.UI.ViewModels.Messages;
 12  using Microsoft.UI.Input;
 13  using Microsoft.UI.Windowing;
 14  using Microsoft.UI.Xaml;
 15  using Microsoft.UI.Xaml.Controls;
 16  using Microsoft.UI.Xaml.Input;
 17  using Microsoft.UI.Xaml.Navigation;
 18  using Windows.System;
 19  using Windows.UI.Core;
 20  using WinUIEx;
 21  using RS_ = Microsoft.CmdPal.UI.Helpers.ResourceLoaderInstance;
 22  using TitleBar = Microsoft.UI.Xaml.Controls.TitleBar;
 23  
 24  namespace Microsoft.CmdPal.UI.Settings;
 25  
 26  public sealed partial class SettingsWindow : WindowEx,
 27      IDisposable,
 28      IRecipient<NavigateToExtensionSettingsMessage>,
 29      IRecipient<QuitMessage>
 30  {
 31      private readonly LocalKeyboardListener _localKeyboardListener;
 32  
 33      private readonly NavigationViewItem? _internalNavItem;
 34  
 35      public ObservableCollection<Crumb> BreadCrumbs { get; } = [];
 36  
 37      // Gets or sets optional action invoked after NavigationView is loaded.
 38      public Action NavigationViewLoaded { get; set; } = () => { };
 39  
 40      public SettingsWindow()
 41      {
 42          this.InitializeComponent();
 43          this.ExtendsContentIntoTitleBar = true;
 44          this.SetIcon();
 45          var title = RS_.GetString("SettingsWindowTitle");
 46          this.AppWindow.Title = title;
 47          this.AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Tall;
 48          this.AppTitleBar.Title = title;
 49          PositionCentered();
 50  
 51          WeakReferenceMessenger.Default.Register<NavigateToExtensionSettingsMessage>(this);
 52          WeakReferenceMessenger.Default.Register<QuitMessage>(this);
 53  
 54          _localKeyboardListener = new LocalKeyboardListener();
 55          _localKeyboardListener.KeyPressed += LocalKeyboardListener_OnKeyPressed;
 56          _localKeyboardListener.Start();
 57          Closed += SettingsWindow_Closed;
 58          RootElement.AddHandler(UIElement.PointerPressedEvent, new PointerEventHandler(RootElement_OnPointerPressed), true);
 59  
 60          if (!BuildInfo.IsCiBuild)
 61          {
 62              _internalNavItem = new NavigationViewItem
 63              {
 64                  Content = "Internal Tools",
 65                  Icon = new FontIcon { Glyph = "\uEC7A" },
 66                  Tag = "Internal",
 67              };
 68              NavView.MenuItems.Add(_internalNavItem);
 69          }
 70          else
 71          {
 72              _internalNavItem = null;
 73          }
 74  
 75          Navigate("General");
 76      }
 77  
 78      private void SettingsWindow_Closed(object sender, WindowEventArgs args)
 79      {
 80          Dispose();
 81      }
 82  
 83      // Handles NavigationView loaded event.
 84      // Sets up initial navigation and accessibility notifications.
 85      private void NavView_Loaded(object sender, RoutedEventArgs e)
 86      {
 87          // Delay necessary to ensure NavigationView visual state can match navigation
 88          Task.Delay(500).ContinueWith(_ => this.NavigationViewLoaded?.Invoke(), TaskScheduler.FromCurrentSynchronizationContext());
 89  
 90          if (sender is NavigationView navigationView)
 91          {
 92              // Register for pane open/close changes to announce to screen readers
 93              navigationView.RegisterPropertyChangedCallback(NavigationView.IsPaneOpenProperty, AnnounceNavigationPaneStateChanged);
 94          }
 95      }
 96  
 97      // Announces navigation pane open/close state to screen readers for accessibility.
 98      private void AnnounceNavigationPaneStateChanged(DependencyObject sender, DependencyProperty dp)
 99      {
100          if (sender is NavigationView navigationView)
101          {
102              UIHelper.AnnounceActionForAccessibility(
103              ue: (UIElement)sender,
104              (sender as NavigationView)?.IsPaneOpen == true ? RS_.GetString("NavigationPaneOpened") : RS_.GetString("NavigationPaneClosed"),
105              "NavigationViewPaneIsOpenChangeNotificationId");
106          }
107      }
108  
109      private void NavView_ItemInvoked(NavigationView sender, NavigationViewItemInvokedEventArgs args)
110      {
111          var selectedItem = args.InvokedItemContainer;
112          Navigate((selectedItem.Tag as string)!);
113      }
114  
115      internal void Navigate(string page)
116      {
117          Type? pageType;
118          switch (page)
119          {
120              case "General":
121                  pageType = typeof(GeneralPage);
122                  break;
123              case "Appearance":
124                  pageType = typeof(AppearancePage);
125                  break;
126              case "Extensions":
127                  pageType = typeof(ExtensionsPage);
128                  break;
129              case "Internal":
130                  pageType = typeof(InternalPage);
131                  break;
132              case "":
133                  // intentional no-op: empty tag means no navigation
134                  pageType = null;
135                  break;
136              default:
137                  // unknown page, no-op and log
138                  pageType = null;
139                  Logger.LogError($"Unknown settings page tag '{page}'");
140                  break;
141          }
142  
143          if (pageType is not null)
144          {
145              NavFrame.Navigate(pageType);
146          }
147      }
148  
149      private void Navigate(ProviderSettingsViewModel extension)
150      {
151          NavFrame.Navigate(typeof(ExtensionPage), extension);
152      }
153  
154      private void PositionCentered()
155      {
156          var displayArea = DisplayArea.GetFromWindowId(AppWindow.Id, DisplayAreaFallback.Nearest);
157          if (displayArea is not null)
158          {
159              var centeredPosition = AppWindow.Position;
160              centeredPosition.X = (displayArea.WorkArea.Width - AppWindow.Size.Width) / 2;
161              centeredPosition.Y = (displayArea.WorkArea.Height - AppWindow.Size.Height) / 2;
162              AppWindow.Move(centeredPosition);
163          }
164      }
165  
166      public void Receive(NavigateToExtensionSettingsMessage message) => Navigate(message.ProviderSettingsVM);
167  
168      private void NavigationBreadcrumbBar_ItemClicked(BreadcrumbBar sender, BreadcrumbBarItemClickedEventArgs args)
169      {
170          if (args.Item is Crumb crumb)
171          {
172              if (crumb.Data is string data)
173              {
174                  if (!string.IsNullOrEmpty(data))
175                  {
176                      Navigate(data);
177                  }
178              }
179          }
180      }
181  
182      private void Window_Activated(object sender, Microsoft.UI.Xaml.WindowActivatedEventArgs args)
183      {
184          WeakReferenceMessenger.Default.Send<Microsoft.UI.Xaml.WindowActivatedEventArgs>(args);
185      }
186  
187      private void Window_Closed(object sender, WindowEventArgs args)
188      {
189          WeakReferenceMessenger.Default.Send<SettingsWindowClosedMessage>();
190  
191          WeakReferenceMessenger.Default.UnregisterAll(this);
192      }
193  
194      private void NavView_DisplayModeChanged(NavigationView sender, NavigationViewDisplayModeChangedEventArgs args)
195      {
196          if (args.DisplayMode is NavigationViewDisplayMode.Compact or NavigationViewDisplayMode.Minimal)
197          {
198              AppTitleBar.IsPaneToggleButtonVisible = true;
199              WorkAroundIcon.Margin = new Thickness(8, 0, 16, 0); // Required for workaround, see XAML comment
200          }
201          else
202          {
203              AppTitleBar.IsPaneToggleButtonVisible = false;
204              WorkAroundIcon.Margin = new Thickness(16, 0, 8, 0); // Required for workaround, see XAML comment
205          }
206      }
207  
208      public void Receive(QuitMessage message)
209      {
210          // This might come in on a background thread
211          DispatcherQueue.TryEnqueue(() => Close());
212      }
213  
214      private void AppTitleBar_PaneToggleRequested(TitleBar sender, object args)
215      {
216          NavView.IsPaneOpen = !NavView.IsPaneOpen;
217      }
218  
219      private void TryGoBack()
220      {
221          if (NavFrame.CanGoBack)
222          {
223              NavFrame.GoBack();
224          }
225      }
226  
227      private void TitleBar_BackRequested(TitleBar sender, object args)
228      {
229          TryGoBack();
230      }
231  
232      private void LocalKeyboardListener_OnKeyPressed(object? sender, LocalKeyboardListenerKeyPressedEventArgs e)
233      {
234          switch (e.Key)
235          {
236              case VirtualKey.GoBack:
237              case VirtualKey.XButton1:
238                  TryGoBack();
239                  break;
240  
241              case VirtualKey.Left:
242                  var altPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Menu).HasFlag(CoreVirtualKeyStates.Down);
243                  if (altPressed)
244                  {
245                      TryGoBack();
246                  }
247  
248                  break;
249          }
250      }
251  
252      private void RootElement_OnPointerPressed(object sender, PointerRoutedEventArgs e)
253      {
254          try
255          {
256              if (e.Pointer.PointerDeviceType == PointerDeviceType.Mouse)
257              {
258                  var ptrPt = e.GetCurrentPoint(RootElement);
259                  if (ptrPt.Properties.IsXButton1Pressed)
260                  {
261                      TryGoBack();
262                  }
263              }
264          }
265          catch (Exception ex)
266          {
267              Logger.LogError("Error handling mouse button press event", ex);
268          }
269      }
270  
271      public void Dispose()
272      {
273          _localKeyboardListener?.Dispose();
274      }
275  
276      private void NavFrame_OnNavigated(object sender, NavigationEventArgs e)
277      {
278          BreadCrumbs.Clear();
279  
280          if (e.SourcePageType == typeof(GeneralPage))
281          {
282              NavView.SelectedItem = GeneralPageNavItem;
283              var pageType = RS_.GetString("Settings_PageTitles_GeneralPage");
284              BreadCrumbs.Add(new(pageType, pageType));
285          }
286          else if (e.SourcePageType == typeof(AppearancePage))
287          {
288              NavView.SelectedItem = AppearancePageNavItem;
289              var pageType = RS_.GetString("Settings_PageTitles_AppearancePage");
290              BreadCrumbs.Add(new(pageType, pageType));
291          }
292          else if (e.SourcePageType == typeof(ExtensionsPage))
293          {
294              NavView.SelectedItem = ExtensionPageNavItem;
295              var pageType = RS_.GetString("Settings_PageTitles_ExtensionsPage");
296              BreadCrumbs.Add(new(pageType, pageType));
297          }
298          else if (e.SourcePageType == typeof(ExtensionPage) && e.Parameter is ProviderSettingsViewModel vm)
299          {
300              NavView.SelectedItem = ExtensionPageNavItem;
301              var extensionsPageType = RS_.GetString("Settings_PageTitles_ExtensionsPage");
302              BreadCrumbs.Add(new(extensionsPageType, extensionsPageType));
303              BreadCrumbs.Add(new(vm.DisplayName, vm));
304          }
305          else if (e.SourcePageType == typeof(InternalPage) && _internalNavItem is not null)
306          {
307              NavView.SelectedItem = _internalNavItem;
308              var pageType = "Internal";
309              BreadCrumbs.Add(new(pageType, pageType));
310          }
311          else
312          {
313              BreadCrumbs.Add(new($"[{e.SourcePageType?.Name}]", string.Empty));
314              Logger.LogError($"Unknown breadcrumb for page type '{e.SourcePageType}'");
315          }
316      }
317  }
318  
319  public readonly struct Crumb
320  {
321      public Crumb(string label, object data)
322      {
323          Label = label;
324          Data = data;
325      }
326  
327      public string Label { get; }
328  
329      public object Data { get; }
330  
331      public override string ToString() => Label;
332  }