EvilSamplesPage.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.Linq; 6 using System.Threading; 7 using System.Threading.Tasks; 8 using Microsoft.CommandPalette.Extensions; 9 using Microsoft.CommandPalette.Extensions.Toolkit; 10 using Windows.System; 11 12 namespace SamplePagesExtension; 13 14 public partial class EvilSamplesPage : ListPage 15 { 16 private readonly IListItem[] _commands = [ 17 new ListItem(new EvilSampleListPage()) 18 { 19 Title = "List Page without items", 20 Subtitle = "Throws exception on GetItems", 21 }, 22 new ListItem(new ExplodeInFiveSeconds(false)) 23 { 24 Title = "Page that will throw an exception after loading it", 25 Subtitle = "Throws exception on GetItems _after_ a ItemsChanged", 26 }, 27 new ListItem(new ExplodeInFiveSeconds(true)) 28 { 29 Title = "Page that keeps throwing exceptions", 30 Subtitle = "Will throw every 5 seconds once you open it", 31 }, 32 new ListItem(new ExplodeOnPropChange()) 33 { 34 Title = "Throw in the middle of a PropChanged", 35 Subtitle = "Will throw every 5 seconds once you open it", 36 }, 37 new ListItem(new SelfImmolateCommand()) 38 { 39 Title = "Terminate this extension", 40 Subtitle = "Will exit this extension (while it's loaded!)", 41 }, 42 new ListItem(new EvilSlowDynamicPage()) 43 { 44 Title = "Slow loading Dynamic Page", 45 Subtitle = "Takes 5 seconds to load each time you type", 46 Tags = [new Tag("GH #38190")], 47 }, 48 new ListItem(new EvilFastUpdatesPage()) 49 { 50 Title = "Fast updating Dynamic Page", 51 Subtitle = "Updates in the middle of a GetItems call", 52 Tags = [new Tag("GH #41149")], 53 }, 54 new ListItem(new NoOpCommand()) 55 { 56 Title = "I have lots of nulls", 57 Subtitle = null, 58 MoreCommands = null, 59 Tags = null, 60 Details = new Details() 61 { 62 Title = null, 63 HeroImage = null, 64 Metadata = null, 65 }, 66 }, 67 new ListItem(new NoOpCommand()) 68 { 69 Title = "I also have nulls", 70 Subtitle = null, 71 MoreCommands = null, 72 Details = new Details() 73 { 74 Title = null, 75 HeroImage = null, 76 Metadata = [new DetailsElement() { Key = "Oops all nulls", Data = new DetailsTags() { Tags = null } }], 77 }, 78 }, 79 new ListItem(new AnonymousCommand(action: () => 80 { 81 ToastStatusMessage toast = new("I should appear immediately"); 82 toast.Show(); 83 Thread.Sleep(5000); 84 }) { Result = CommandResult.KeepOpen() }) 85 { 86 Title = "I take just forever to return something", 87 Subtitle = "The toast should appear immediately.", 88 MoreCommands = null, 89 Details = new Details() 90 { 91 Body = "This is a test for GH#512. If it doesn't appear immediately, it's likely InvokeCommand is happening on the UI thread.", 92 }, 93 }, 94 95 // More edge cases than truly evil 96 new ListItem( 97 new ToastCommand("Primary command invoked", MessageState.Info) { Name = "Primary command", Icon = new IconInfo("\uF146") }) // dial 1 98 { 99 Title = "anonymous command test", 100 Subtitle = "Try pressing Ctrl+1 with me selected", 101 Icon = new IconInfo("\uE712"), // "More" dots 102 MoreCommands = [ 103 new CommandContextItem( 104 new ToastCommand("Secondary command invoked", MessageState.Warning) { Name = "Secondary command", Icon = new IconInfo("\uF147") }) // dial 2 105 { 106 Title = "I'm a second command", 107 RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number1), 108 }, 109 new CommandContextItem("nested...") 110 { 111 Title = "We can go deeper...", 112 Icon = new IconInfo("\uF148"), 113 RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number2), 114 MoreCommands = [ 115 new CommandContextItem( 116 new ToastCommand("Nested A invoked") { Name = "Do it", Icon = new IconInfo("A") }) 117 { 118 Title = "Nested A", 119 RequestedShortcut = KeyChordHelpers.FromModifiers(alt: true, vkey: VirtualKey.A), 120 }, 121 122 new CommandContextItem( 123 new ToastCommand("Nested B invoked") { Name = "Do it", Icon = new IconInfo("B") }) 124 { 125 Title = "Nested B...", 126 RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.B), 127 MoreCommands = [ 128 new CommandContextItem( 129 new ToastCommand("Nested C invoked") { Name = "Do it" }) 130 { 131 Title = "You get it", 132 RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.B), 133 } 134 ], 135 }, 136 ], 137 } 138 ], 139 }, 140 new ListItem( 141 new ToastCommand("Primary command invoked", MessageState.Info) { Name = "Primary command", Icon = new IconInfo("\uF146") }) // dial 1 142 { 143 Title = "noop command test", 144 Subtitle = "Try pressing Ctrl+1 with me selected", 145 Icon = new IconInfo("\uE712"), // "More" dots 146 MoreCommands = [ 147 new CommandContextItem( 148 new ToastCommand("Secondary command invoked", MessageState.Warning) { Name = "Secondary command", Icon = new IconInfo("\uF147") }) // dial 2 149 { 150 Title = "I'm a second command", 151 RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number1), 152 }, 153 new CommandContextItem(new NoOpCommand()) 154 { 155 Title = "We can go deeper...", 156 Icon = new IconInfo("\uF148"), 157 RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number2), 158 MoreCommands = [ 159 new CommandContextItem( 160 new ToastCommand("Nested A invoked") { Name = "Do it", Icon = new IconInfo("A") }) 161 { 162 Title = "Nested A", 163 RequestedShortcut = KeyChordHelpers.FromModifiers(alt: true, vkey: VirtualKey.A), 164 }, 165 166 new CommandContextItem( 167 new ToastCommand("Nested B invoked") { Name = "Do it", Icon = new IconInfo("B") }) 168 { 169 Title = "Nested B...", 170 RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.B), 171 MoreCommands = [ 172 new CommandContextItem( 173 new ToastCommand("Nested C invoked") { Name = "Do it" }) 174 { 175 Title = "You get it", 176 RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.B), 177 } 178 ], 179 }, 180 ], 181 } 182 ], 183 }, 184 new ListItem( 185 new ToastCommand("Primary command invoked", MessageState.Info) { Name = "Primary command", Icon = new IconInfo("\uF146") }) // dial 1 186 { 187 Title = "noop secondary command test", 188 Subtitle = "Try pressing Ctrl+1 with me selected", 189 Icon = new IconInfo("\uE712"), // "More" dots 190 MoreCommands = [ 191 new CommandContextItem(new NoOpCommand()) 192 { 193 Title = "We can go deeper...", 194 Icon = new IconInfo("\uF148"), 195 RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number2), 196 MoreCommands = [ 197 new CommandContextItem( 198 new ToastCommand("Nested A invoked") { Name = "Do it", Icon = new IconInfo("A") }) 199 { 200 Title = "Nested A", 201 RequestedShortcut = KeyChordHelpers.FromModifiers(alt: true, vkey: VirtualKey.A), 202 }, 203 204 new CommandContextItem( 205 new ToastCommand("Nested B invoked") { Name = "Do it", Icon = new IconInfo("B") }) 206 { 207 Title = "Nested B...", 208 RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.B), 209 MoreCommands = [ 210 new CommandContextItem( 211 new ToastCommand("Nested C invoked") { Name = "Do it" }) 212 { 213 Title = "You get it", 214 RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.B), 215 } 216 ], 217 }, 218 ], 219 } 220 ], 221 }, 222 new ListItem( 223 new ToastCommand("Primary command invoked", MessageState.Info) { Name = "H W\r\nE O\r\nL R\r\nL L\r\nO D", Icon = new IconInfo("\uF146") }) 224 { 225 Title = "noop third command test", 226 Icon = new IconInfo("\uE712"), // "More" dots 227 }, 228 new ListItem(new EvilDuplicateRequestedShortcut()) 229 { 230 Title = "Evil keyboard shortcuts", 231 Subtitle = "Two commands with the same shortcut and more...", 232 Icon = new IconInfo("\uE765"), 233 }, 234 ]; 235 236 public EvilSamplesPage() 237 { 238 Name = "Evil Samples"; 239 Icon = new IconInfo("👿"); // Info 240 } 241 242 public override IListItem[] GetItems() => _commands; 243 } 244 245 [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Sample code")] 246 internal sealed partial class ExplodeOnPropChange : ListPage 247 { 248 private bool _explode; 249 250 public override string Title 251 { 252 get => _explode ? Commands[9001].Title : base.Title; 253 set => base.Title = value; 254 } 255 256 private IListItem[] Commands => [ 257 new ListItem(new NoOpCommand()) 258 { 259 Title = "This page will explode in five seconds!", 260 Subtitle = "I'll change my Name, then explode", 261 }, 262 ]; 263 264 public ExplodeOnPropChange() 265 { 266 Icon = new IconInfo(string.Empty); 267 Name = "Open"; 268 } 269 270 public override IListItem[] GetItems() 271 { 272 _ = Task.Run(() => 273 { 274 Thread.Sleep(1000); 275 Title = "Ready? 3..."; 276 Thread.Sleep(1000); 277 Title = "Ready? 2..."; 278 Thread.Sleep(1000); 279 Title = "Ready? 1..."; 280 Thread.Sleep(1000); 281 _explode = true; 282 Title = "boom"; 283 }); 284 return Commands; 285 } 286 } 287 288 /// <summary> 289 /// This sample simulates a long delay in handling UpdateSearchText. I've found 290 /// that if I type "124356781234", then somewhere around the second "1234", 291 /// we'll get into a state where the character is typed, but then CmdPal snaps 292 /// back to a previous query. 293 /// 294 /// We can use this to validate that we're always sticking with the last 295 /// SearchText. My guess is that it's a bug in 296 /// Toolkit.DynamicListPage.SearchText.set 297 /// 298 /// see GH #38190 299 /// </summary> 300 [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Sample code")] 301 internal sealed partial class EvilSlowDynamicPage : DynamicListPage 302 { 303 private IListItem[] _items = []; 304 305 public EvilSlowDynamicPage() 306 { 307 Icon = new IconInfo(string.Empty); 308 Name = "Open"; 309 Title = "Evil Slow Dynamic Page"; 310 PlaceholderText = "Type to see items appear after a delay"; 311 } 312 313 public override void UpdateSearchText(string oldSearch, string newSearch) 314 { 315 DoQuery(newSearch); 316 RaiseItemsChanged(newSearch.Length); 317 } 318 319 public override IListItem[] GetItems() 320 { 321 return _items.Length > 0 ? _items : DoQuery(SearchText); 322 } 323 324 private IListItem[] DoQuery(string newSearch) 325 { 326 IsLoading = true; 327 328 // Sleep for longer for shorter search terms 329 var delay = 10000 - (newSearch.Length * 2000); 330 delay = delay < 0 ? 0 : delay; 331 if (newSearch.Length == 0) 332 { 333 delay = 0; 334 } 335 336 delay += 50; 337 338 Thread.Sleep(delay); // Simulate a long load time 339 340 var items = newSearch.ToCharArray().Select(ch => new ListItem(new NoOpCommand()) { Title = ch.ToString() }).ToArray(); 341 if (items.Length == 0) 342 { 343 items = [new ListItem(new NoOpCommand()) { Title = "Start typing in the search box" }]; 344 } 345 346 if (items.Length > 0) 347 { 348 items[0].Subtitle = "Notice how the number of items changes for this page when you type in the filter box"; 349 } 350 351 IsLoading = false; 352 353 return items; 354 } 355 } 356 357 /// <summary> 358 /// A sample for a page that updates its items in the middle of a GetItems call. 359 /// In this sample, we're returning 10000 items, which genuinely marshal slowly 360 /// (even before we start retrieving properties from them). 361 /// 362 /// While we're in the middle of the marshalling of that GetItems call, the 363 /// background thread we started will kick off another GetItems (via the 364 /// RaiseItemsChanged). 365 /// 366 /// That second GetItems will return a single item, which marshals quickly. 367 /// CmdPal _should_ only display that single green item. However, as of v0.4, 368 /// we'll display that green item, then "snap back" to the red items, when they 369 /// finish marshalling. 370 /// 371 /// See GH #41149 372 /// </summary> 373 [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Sample code")] 374 internal sealed partial class EvilFastUpdatesPage : DynamicListPage 375 { 376 private static readonly IconInfo _red = new("🔴"); // "Red" icon 377 private static readonly IconInfo _green = new("🟢"); // "Green" icon 378 379 private IListItem[] _redItems = []; 380 private IListItem[] _greenItems = []; 381 private bool _sentRed; 382 383 public EvilFastUpdatesPage() 384 { 385 Icon = new IconInfo(string.Empty); 386 Name = "Open"; 387 Title = "Evil Fast Updates Page"; 388 PlaceholderText = "Type to trigger an update"; 389 390 _redItems = Enumerable.Range(0, 10000).Select(i => new ListItem(new NoOpCommand()) 391 { 392 Icon = _red, 393 Title = $"Item {i + 1}", 394 Subtitle = "CmdPal is doing it wrong", 395 }).ToArray(); 396 _greenItems = [new ListItem(new NoOpCommand()) { Icon = _green, Title = "It works" }]; 397 } 398 399 public override void UpdateSearchText(string oldSearch, string newSearch) 400 { 401 _sentRed = false; 402 RaiseItemsChanged(); 403 } 404 405 public override IListItem[] GetItems() 406 { 407 if (!_sentRed) 408 { 409 IsLoading = true; 410 _sentRed = true; 411 412 // kick off a task to update the items after a delay 413 _ = Task.Run(() => 414 { 415 Thread.Sleep(5); 416 RaiseItemsChanged(); 417 }); 418 419 return _redItems; 420 } 421 else 422 { 423 IsLoading = false; 424 return _greenItems; 425 } 426 } 427 } 428 429 [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Sample code")] 430 internal sealed partial class EvilDuplicateRequestedShortcut : ListPage 431 { 432 private readonly IListItem[] _items = 433 [ 434 new ListItem(new NoOpCommand()) 435 { 436 Title = "I'm evil!", 437 Subtitle = "I have multiple commands sharing the same keyboard shortcut", 438 MoreCommands = [ 439 new CommandContextItem(new AnonymousCommand(() => new ToastStatusMessage("Me too executed").Show()) 440 { 441 Result = CommandResult.KeepOpen(), 442 }) 443 { 444 Title = "Me too", 445 RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number1), 446 }, 447 new CommandContextItem(new AnonymousCommand(() => new ToastStatusMessage("Me three executed").Show()) 448 { 449 Result = CommandResult.KeepOpen(), 450 }) 451 { 452 Title = "Me three", 453 RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number1), 454 }, 455 ], 456 }, 457 ]; 458 459 public override IListItem[] GetItems() => _items; 460 461 public EvilDuplicateRequestedShortcut() 462 { 463 Icon = new IconInfo(string.Empty); 464 Name = "Open"; 465 } 466 }