mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-07-09 12:00:14 +02:00
<!-- 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
165 lines
5.0 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|