ListItemViewModel.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.Diagnostics.CodeAnalysis; 6 using Microsoft.CmdPal.Core.ViewModels.Commands; 7 using Microsoft.CmdPal.Core.ViewModels.Models; 8 using Microsoft.CommandPalette.Extensions; 9 using Microsoft.CommandPalette.Extensions.Toolkit; 10 11 namespace Microsoft.CmdPal.Core.ViewModels; 12 13 public partial class ListItemViewModel : CommandItemViewModel 14 { 15 public new ExtensionObject<IListItem> Model { get; } 16 17 public List<TagViewModel>? Tags { get; set; } 18 19 // Remember - "observable" properties from the model (via PropChanged) 20 // cannot be marked [ObservableProperty] 21 public bool HasTags => (Tags?.Count ?? 0) > 0; 22 23 public string TextToSuggest { get; private set; } = string.Empty; 24 25 public string Section { get; private set; } = string.Empty; 26 27 public bool IsSectionOrSeparator { get; private set; } 28 29 public DetailsViewModel? Details { get; private set; } 30 31 [MemberNotNullWhen(true, nameof(Details))] 32 public bool HasDetails => Details is not null; 33 34 public string AccessibleName { get; private set; } = string.Empty; 35 36 public bool ShowTitle { get; private set; } 37 38 public bool ShowSubtitle { get; private set; } 39 40 public bool LayoutShowsTitle 41 { 42 get; 43 set 44 { 45 if (SetProperty(ref field, value)) 46 { 47 UpdateShowsTitle(); 48 } 49 } 50 } 51 52 public bool LayoutShowsSubtitle 53 { 54 get; 55 set 56 { 57 if (SetProperty(ref field, value)) 58 { 59 UpdateShowsSubtitle(); 60 } 61 } 62 } 63 64 public ListItemViewModel(IListItem model, WeakReference<IPageContext> context) 65 : base(new(model), context) 66 { 67 Model = new ExtensionObject<IListItem>(model); 68 } 69 70 public override void InitializeProperties() 71 { 72 if (IsInitialized) 73 { 74 return; 75 } 76 77 // This sets IsInitialized = true 78 base.InitializeProperties(); 79 80 var li = Model.Unsafe; 81 if (li is null) 82 { 83 return; // throw? 84 } 85 86 UpdateTags(li.Tags); 87 Section = li.Section ?? string.Empty; 88 IsSectionOrSeparator = IsSeparator(li); 89 UpdateProperty(nameof(Section), nameof(IsSectionOrSeparator)); 90 91 UpdateAccessibleName(); 92 } 93 94 private bool IsSeparator(IListItem item) 95 { 96 return item.Command is null; 97 } 98 99 public override void SlowInitializeProperties() 100 { 101 base.SlowInitializeProperties(); 102 var model = Model.Unsafe; 103 if (model is null) 104 { 105 return; 106 } 107 108 var extensionDetails = model.Details; 109 if (extensionDetails is not null) 110 { 111 Details = new(extensionDetails, PageContext); 112 Details.InitializeProperties(); 113 UpdateProperty(nameof(Details), nameof(HasDetails)); 114 } 115 116 AddShowDetailsCommands(); 117 118 TextToSuggest = model.TextToSuggest; 119 UpdateProperty(nameof(TextToSuggest)); 120 } 121 122 protected override void FetchProperty(string propertyName) 123 { 124 base.FetchProperty(propertyName); 125 126 var model = this.Model.Unsafe; 127 if (model is null) 128 { 129 return; // throw? 130 } 131 132 switch (propertyName) 133 { 134 case nameof(model.Tags): 135 UpdateTags(model.Tags); 136 break; 137 case nameof(model.TextToSuggest): 138 TextToSuggest = model.TextToSuggest ?? string.Empty; 139 UpdateProperty(nameof(TextToSuggest)); 140 break; 141 case nameof(model.Section): 142 Section = model.Section ?? string.Empty; 143 IsSectionOrSeparator = IsSeparator(model); 144 UpdateProperty(nameof(Section), nameof(IsSectionOrSeparator)); 145 break; 146 case nameof(model.Command): 147 IsSectionOrSeparator = IsSeparator(model); 148 UpdateProperty(nameof(IsSectionOrSeparator)); 149 break; 150 case nameof(Details): 151 var extensionDetails = model.Details; 152 Details = extensionDetails is not null ? new(extensionDetails, PageContext) : null; 153 Details?.InitializeProperties(); 154 UpdateProperty(nameof(Details), nameof(HasDetails)); 155 UpdateShowDetailsCommand(); 156 break; 157 case nameof(model.MoreCommands): 158 UpdateProperty(nameof(MoreCommands)); 159 AddShowDetailsCommands(); 160 break; 161 case nameof(model.Title): 162 UpdateProperty(nameof(Title)); 163 UpdateShowsTitle(); 164 UpdateAccessibleName(); 165 break; 166 case nameof(model.Subtitle): 167 UpdateProperty(nameof(Subtitle)); 168 UpdateShowsSubtitle(); 169 UpdateAccessibleName(); 170 break; 171 default: 172 UpdateProperty(propertyName); 173 break; 174 } 175 } 176 177 // TODO: Do we want filters to match descriptions and other properties? Tags, etc... Yes? 178 // TODO: Do we want to save off the score here so we can sort by it in our ListViewModel? 179 public override string ToString() => $"{Name} ListItemViewModel"; 180 181 public override bool Equals(object? obj) => obj is ListItemViewModel vm && vm.Model.Equals(this.Model); 182 183 public override int GetHashCode() => Model.GetHashCode(); 184 185 private void AddShowDetailsCommands() 186 { 187 // If the parent page has ShowDetails = false and we have details, 188 // then we should add a show details action in the context menu. 189 if (HasDetails && 190 PageContext.TryGetTarget(out var pageContext) && 191 pageContext is ListViewModel listViewModel && 192 !listViewModel.ShowDetails) 193 { 194 // Check if "Show Details" action already exists to prevent duplicates 195 if (!MoreCommands.Any(cmd => cmd is CommandContextItemViewModel contextItemViewModel && 196 contextItemViewModel.Command.Id == ShowDetailsCommand.ShowDetailsCommandId)) 197 { 198 // Create the view model for the show details command 199 var showDetailsCommand = new ShowDetailsCommand(Details); 200 var showDetailsContextItem = new CommandContextItem(showDetailsCommand); 201 var showDetailsContextItemViewModel = new CommandContextItemViewModel(showDetailsContextItem, PageContext); 202 showDetailsContextItemViewModel.SlowInitializeProperties(); 203 MoreCommands.Add(showDetailsContextItemViewModel); 204 } 205 206 UpdateProperty(nameof(MoreCommands), nameof(AllCommands)); 207 } 208 } 209 210 // This method is called when the details change to make sure we 211 // have the latest details in the show details command. 212 private void UpdateShowDetailsCommand() 213 { 214 // If the parent page has ShowDetails = false and we have details, 215 // then we should add a show details action in the context menu. 216 if (HasDetails && 217 PageContext.TryGetTarget(out var pageContext) && 218 pageContext is ListViewModel listViewModel && 219 !listViewModel.ShowDetails) 220 { 221 var existingCommand = MoreCommands.FirstOrDefault(cmd => 222 cmd is CommandContextItemViewModel contextItemViewModel && 223 contextItemViewModel.Command.Id == ShowDetailsCommand.ShowDetailsCommandId); 224 225 // If the command already exists, remove it to update with the new details 226 if (existingCommand is not null) 227 { 228 MoreCommands.Remove(existingCommand); 229 } 230 231 // Create the view model for the show details command 232 var showDetailsCommand = new ShowDetailsCommand(Details); 233 var showDetailsContextItem = new CommandContextItem(showDetailsCommand); 234 var showDetailsContextItemViewModel = new CommandContextItemViewModel(showDetailsContextItem, PageContext); 235 showDetailsContextItemViewModel.SlowInitializeProperties(); 236 MoreCommands.Add(showDetailsContextItemViewModel); 237 238 UpdateProperty(nameof(MoreCommands), nameof(AllCommands)); 239 } 240 } 241 242 private void UpdateTags(ITag[]? newTagsFromModel) 243 { 244 var newTags = newTagsFromModel?.Select(t => 245 { 246 var vm = new TagViewModel(t, PageContext); 247 vm.InitializeProperties(); 248 return vm; 249 }) 250 .ToList() ?? []; 251 252 DoOnUiThread( 253 () => 254 { 255 // Tags being an ObservableCollection instead of a List lead to 256 // many COM exception issues. 257 Tags = [.. newTags]; 258 259 // We're already in UI thread, so just raise the events 260 OnPropertyChanged(nameof(Tags)); 261 OnPropertyChanged(nameof(HasTags)); 262 }); 263 } 264 265 private void UpdateShowsTitle() 266 { 267 var oldShowTitle = ShowTitle; 268 ShowTitle = LayoutShowsTitle; 269 if (oldShowTitle != ShowTitle) 270 { 271 UpdateProperty(nameof(ShowTitle)); 272 } 273 } 274 275 private void UpdateShowsSubtitle() 276 { 277 var oldShowSubtitle = ShowSubtitle; 278 ShowSubtitle = LayoutShowsSubtitle && !string.IsNullOrWhiteSpace(Subtitle); 279 if (oldShowSubtitle != ShowSubtitle) 280 { 281 UpdateProperty(nameof(ShowSubtitle)); 282 } 283 } 284 285 protected override void UnsafeCleanup() 286 { 287 base.UnsafeCleanup(); 288 289 // Tags don't have event handlers or anything to cleanup 290 Tags?.ForEach(t => t.SafeCleanup()); 291 Details?.SafeCleanup(); 292 293 var model = Model.Unsafe; 294 if (model is not null) 295 { 296 // We don't need to revoke the PropChanged event handler here, 297 // because we are just overriding CommandItem's FetchProperty and 298 // piggy-backing off their PropChanged 299 } 300 } 301 302 protected void UpdateAccessibleName() 303 { 304 AccessibleName = Title + ", " + Subtitle; 305 UpdateProperty(nameof(AccessibleName)); 306 } 307 }