Files
PowerToys/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentTreeViewModel.cs
Jiří Polášek 4cb3359314 CmdPal: Plain text viewer and image viewer IContent (#43964)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

This PR introduces new types of IContent:  
- Plain text content – simple and straightforward, with options to
switch between UI and monospace fonts and toggle word wrap.
- It's super safe to display any random text content without having to
worry about escaping the pesky markdown.
- Image viewer content – a more polished way to display images:  
- When placed in the ContentPage, the component automatically resizes to
fit the viewport, ensuring the entire image is visible at once.
- Images can be opened in a built-in mini-viewer that lets you view,
pan, and zoom without leaving the Command Palette. (Doing this directly
on the page proved to be a UX and development headache.) Fully
keyboard-controllable, so there’s no need to take your hands off the
keys.

## Pictures? Pictures!

Plain text content:

<img width="960" height="604" alt="image"
src="https://github.com/user-attachments/assets/a4ec36f3-2f7f-4a2a-a646-53056c512023"
/>

Image viewer content:

<img width="939" height="605" alt="image"
src="https://github.com/user-attachments/assets/c87f5726-8cd0-4015-b2d9-f1457fa1ec96"
/>



https://github.com/user-attachments/assets/915cd9d2-e4e3-4baf-8df6-6a328a3592ba


<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #41038
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2026-03-27 18:45:41 -05:00

165 lines
5.0 KiB
C#

// Copyright (c) Microsoft Corporation
// 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.Collections.ObjectModel;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class ContentTreeViewModel(ITreeContent _tree, WeakReference<IPageContext> context) :
ContentViewModel(context)
{
public ExtensionObject<ITreeContent> Model { get; } = new(_tree);
// Remember - "observable" properties from the model (via PropChanged)
// cannot be marked [ObservableProperty]
public ContentViewModel? RootContent { get; protected set; }
public ObservableCollection<ContentViewModel> Children { get; } = [];
public bool HasChildren => Children.Count > 0;
// This is the content that's actually bound in XAML. We needed a
// collection, even if the collection is just a single item.
public ObservableCollection<ContentViewModel> Root => RootContent is not null ? [RootContent] : [];
public override void InitializeProperties()
{
var model = Model.Unsafe;
if (model is null)
{
return;
}
var root = model.RootContent;
if (root is not null)
{
RootContent = ViewModelFromContent(root, PageContext);
RootContent?.InitializeProperties();
UpdateProperty(nameof(RootContent));
UpdateProperty(nameof(Root));
}
FetchContent();
model.PropChanged += Model_PropChanged;
model.ItemsChanged += Model_ItemsChanged;
}
// Theoretically, we should unify this with the one in CommandPalettePageViewModelFactory
// and maybe just have a ContentViewModelFactory or something
public ContentViewModel? ViewModelFromContent(IContent content, WeakReference<IPageContext> context)
{
ContentViewModel? viewModel = content switch
{
IFormContent form => new ContentFormViewModel(form, context),
IMarkdownContent markdown => new ContentMarkdownViewModel(markdown, context),
ITreeContent tree => new ContentTreeViewModel(tree, context),
IPlainTextContent plainText => new ContentPlainTextViewModel(plainText, context),
IImageContent image => new ContentImageViewModel(image, context),
_ => null,
};
return viewModel;
}
// TODO: Does this need to hop to a _different_ thread, so that we don't block the extension while we're fetching?
private void Model_ItemsChanged(object sender, IItemsChangedEventArgs args) => FetchContent();
private void Model_PropChanged(object sender, IPropChangedEventArgs args)
{
try
{
var propName = args.PropertyName;
FetchProperty(propName);
}
catch (Exception ex)
{
ShowException(ex);
}
}
protected void FetchProperty(string propertyName)
{
var model = Model.Unsafe;
if (model is null)
{
return; // throw?
}
switch (propertyName)
{
case nameof(RootContent):
var root = model.RootContent;
if (root is not null)
{
RootContent = ViewModelFromContent(root, PageContext);
}
else
{
root = null;
}
UpdateProperty(nameof(Root));
break;
}
UpdateProperty(propertyName);
}
//// Run on background thread, from InitializeAsync or Model_ItemsChanged
private void FetchContent()
{
List<ContentViewModel> newContent = [];
try
{
var newItems = Model.Unsafe!.GetChildren();
foreach (var item in newItems)
{
var viewModel = ViewModelFromContent(item, PageContext);
if (viewModel is not null)
{
viewModel.InitializeProperties();
newContent.Add((ContentViewModel)viewModel);
}
}
}
catch (Exception ex)
{
ShowException(ex);
throw;
}
// Now, back to a UI thread to update the observable collection
DoOnUiThread(
() =>
{
ListHelpers.InPlaceUpdateList(Children, newContent);
});
UpdateProperty(nameof(HasChildren));
}
protected override void UnsafeCleanup()
{
base.UnsafeCleanup();
RootContent?.SafeCleanup();
foreach (var item in Children)
{
item.SafeCleanup();
}
Children.Clear();
var model = Model.Unsafe;
if (model is not null)
{
model.PropChanged -= Model_PropChanged;
model.ItemsChanged -= Model_ItemsChanged;
}
}
}