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
This commit is contained in:
Jiří Polášek
2026-03-28 00:45:41 +01:00
committed by GitHub
parent 943c2a1ff5
commit 4cb3359314
37 changed files with 1752 additions and 76 deletions

View File

@@ -20,6 +20,8 @@ public partial class CommandPaletteContentPageViewModel : ContentPageViewModel
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;

View File

@@ -0,0 +1,94 @@
// 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 Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class ContentImageViewModel : ContentViewModel
{
public ExtensionObject<IImageContent> Model { get; }
public IconInfoViewModel Image { get; protected set; } = new(null);
public double MaxWidth { get; protected set; } = double.PositiveInfinity;
public double MaxHeight { get; protected set; } = double.PositiveInfinity;
public ContentImageViewModel(IImageContent content, WeakReference<IPageContext> context)
: base(context)
{
Model = new ExtensionObject<IImageContent>(content);
}
public override void InitializeProperties()
{
var model = Model.Unsafe;
if (model is null)
{
return;
}
Image = new IconInfoViewModel(model.Image);
Image.InitializeProperties();
MaxWidth = model.MaxWidth <= 0 ? double.PositiveInfinity : model.MaxWidth;
MaxHeight = model.MaxHeight <= 0 ? double.PositiveInfinity : model.MaxHeight;
UpdateProperty(nameof(Image), nameof(MaxWidth), nameof(MaxHeight));
model.PropChanged += Model_PropChanged;
}
private void Model_PropChanged(object sender, IPropChangedEventArgs args)
{
try
{
var propName = args.PropertyName;
FetchProperty(propName);
}
catch (Exception ex)
{
ShowException(ex);
}
}
private void FetchProperty(string propertyName)
{
var model = Model.Unsafe;
if (model is null)
{
return; // throw?
}
switch (propertyName)
{
case nameof(Image):
Image = new IconInfoViewModel(model.Image);
Image.InitializeProperties();
UpdateProperty(propertyName);
break;
case nameof(IImageContent.MaxWidth):
MaxWidth = model.MaxWidth <= 0 ? double.PositiveInfinity : model.MaxWidth;
UpdateProperty(propertyName);
break;
case nameof(IImageContent.MaxHeight):
MaxHeight = model.MaxHeight <= 0 ? double.PositiveInfinity : model.MaxHeight;
UpdateProperty(propertyName);
break;
}
}
protected override void UnsafeCleanup()
{
base.UnsafeCleanup();
var model = Model.Unsafe;
if (model is not null)
{
model.PropChanged -= Model_PropChanged;
}
}
}

View File

@@ -0,0 +1,114 @@
// 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 Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class ContentPlainTextViewModel : ContentViewModel
{
private ExtensionObject<IPlainTextContent> Model { get; }
public string? Text { get; protected set; }
public bool WordWrapEnabled { get; protected set; }
public bool UseMonospace { get; protected set; }
public ContentPlainTextViewModel(IPlainTextContent content, WeakReference<IPageContext> context)
: base(context)
{
Model = new ExtensionObject<IPlainTextContent>(content);
}
public override void InitializeProperties()
{
var model = Model.Unsafe;
if (model is null)
{
return;
}
Text = model.Text;
WordWrapEnabled = model.WrapWords;
UseMonospace = model.FontFamily == CommandPalette.Extensions.FontFamily.Monospace;
UpdateProperty(nameof(Text), nameof(WordWrapEnabled), nameof(UseMonospace));
model.PropChanged += Model_PropChanged;
}
private void Model_PropChanged(object sender, IPropChangedEventArgs args)
{
try
{
var propName = args.PropertyName;
FetchProperty(propName);
}
catch (Exception ex)
{
ShowException(ex);
}
}
private void FetchProperty(string propertyName)
{
var model = Model.Unsafe;
if (model is null)
{
return; // throw?
}
switch (propertyName)
{
case nameof(IPlainTextContent.FontFamily):
// RPC:
var incomingUseMonospace = model.FontFamily == CommandPalette.Extensions.FontFamily.Monospace;
// local:
if (incomingUseMonospace != UseMonospace)
{
UseMonospace = incomingUseMonospace;
UpdateProperty(nameof(UseMonospace));
}
break;
case nameof(IPlainTextContent.WrapWords):
// RPC:
var incomingWrap = model.WrapWords;
// local:
if (WordWrapEnabled != incomingWrap)
{
WordWrapEnabled = model.WrapWords;
UpdateProperty(nameof(WordWrapEnabled));
}
break;
case nameof(IPlainTextContent.Text):
// RPC:
var incomingText = model.Text;
// local:
if (incomingText != Text)
{
Text = incomingText;
UpdateProperty(nameof(Text));
}
break;
}
}
protected override void UnsafeCleanup()
{
base.UnsafeCleanup();
var model = Model.Unsafe;
if (model is not null)
{
model.PropChanged -= Model_PropChanged;
}
}
}

View File

@@ -58,6 +58,8 @@ public partial class ContentTreeViewModel(ITreeContent _tree, WeakReference<IPag
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;

View File

@@ -77,7 +77,7 @@ public partial class App : Application, IDisposable
Services = ConfigureServices(appInfoService);
IconCacheProvider.Initialize(Services);
IconProvider.Initialize(Services);
this.InitializeComponent();

View File

@@ -41,7 +41,7 @@
Margin="4,0,0,0"
HorizontalAlignment="Left"
SourceKey="{x:Bind Icon, Mode=OneWay}"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested20}" />
SourceRequested="{x:Bind help:IconProvider.SourceRequested20}" />
<TextBlock
x:Name="TitleTextBlock"
Grid.Column="1"
@@ -90,7 +90,7 @@
HorizontalAlignment="Left"
Foreground="{ThemeResource SystemFillColorCriticalBrush}"
SourceKey="{x:Bind Icon, Mode=OneWay}"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested20}" />
SourceRequested="{x:Bind help:IconProvider.SourceRequested20}" />
<TextBlock
x:Name="TitleTextBlock"
Grid.Column="1"

View File

@@ -48,7 +48,7 @@
Height="16"
AutomationProperties.AccessibilityView="Raw"
SourceKey="{x:Bind Icon, Mode=OneWay}"
SourceRequested="{x:Bind helpers:IconCacheProvider.SourceRequested20}" />
SourceRequested="{x:Bind helpers:IconProvider.SourceRequested20}" />
</cpcontrols:ContentIcon.Content>
</cpcontrols:ContentIcon>
</controls:SettingsCard.HeaderIcon>

View File

@@ -35,7 +35,7 @@
HorizontalAlignment="Left"
VerticalAlignment="Center"
SourceKey="{x:Bind Icon}"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested20}" />
SourceRequested="{x:Bind help:IconProvider.SourceRequested20}" />
<TextBlock
Grid.Column="1"
VerticalAlignment="Center"
@@ -72,7 +72,7 @@
Margin="0,0,8,0"
VerticalAlignment="Center"
AutomationProperties.AccessibilityView="Raw"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested20}"
SourceRequested="{x:Bind help:IconProvider.SourceRequested20}"
Visibility="Collapsed" />
<TextBlock x:Name="SelectedFilterText" VerticalAlignment="Center" />
</StackPanel>

View File

@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="Microsoft.CmdPal.UI.Controls.ImageViewer"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Microsoft.CmdPal.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:help="using:Microsoft.CmdPal.UI.Helpers"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="Transparent">
<controls:IconBox
x:Name="ZoomImage"
HorizontalAlignment="Center"
VerticalAlignment="Center"
IsTabStop="False"
RenderTransformOrigin="0.5,0.5"
SourceRequested="{x:Bind help:IconProvider.SourceRequestedOriginal}">
<controls:IconBox.RenderTransform>
<TransformGroup>
<ScaleTransform x:Name="ScaleTransform" ScaleX="1" ScaleY="1" />
<TranslateTransform x:Name="TranslateTransform" X="0" Y="0" />
</TransformGroup>
</controls:IconBox.RenderTransform>
</controls:IconBox>
<Border
x:Name="ToolbarHost"
Margin="0,0,0,12"
Padding="8"
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
Background="{ThemeResource SystemControlAcrylicWindowBrush}"
BorderBrush="{ThemeResource ControlStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="8">
<StackPanel
x:Name="Toolbar"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="Transparent"
Orientation="Horizontal"
Spacing="8">
<Button
x:Uid="ImageViewer_ZoomInButton"
Padding="8,6"
Click="OnZoomInClick">
<SymbolIcon Symbol="ZoomIn" />
</Button>
<Button
x:Uid="ImageViewer_ZoomOutButton"
Padding="8,6"
Click="OnZoomOutClick">
<SymbolIcon Symbol="ZoomOut" />
</Button>
<Button
x:Uid="ImageViewer_ZoomToFitButton"
Padding="8,6"
Click="OnZoomToFitClick">
<FontIcon Glyph="&#xE9A6;" />
</Button>
</StackPanel>
</Border>
</Grid>
</UserControl>

View File

@@ -0,0 +1,335 @@
// 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 Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Windows.Foundation;
using Windows.System;
namespace Microsoft.CmdPal.UI.Controls;
public sealed partial class ImageViewer : UserControl
{
public event EventHandler? CancelRequested;
private const double MinScale = 0.25;
private const double MaxScale = 8.0;
private const double MinVisiblePadding = 24.0;
private const double KeyboardPanStep = 24.0;
private Grid? _host;
private Point _lastPanPoint;
private bool _isPanning;
private double _scale = 1.0;
public ImageViewer()
{
InitializeComponent();
_host = Content as Grid;
IsTabStop = true;
KeyDown += OnKeyDown;
Loaded += OnLoaded;
Unloaded += OnUnloaded;
PointerPressed += OnPointerPressed;
PointerMoved += OnPointerMoved;
PointerReleased += OnPointerReleased;
PointerWheelChanged += OnPointerWheelChanged;
DoubleTapped += OnDoubleTapped;
SizeChanged += OnSizeChanged;
}
public void Initialize(object? sourceKey)
{
ZoomImage.SourceKey = sourceKey;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
ResetView();
CenterImage();
}
private void OnUnloaded(object sender, RoutedEventArgs e)
{
}
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
ClampTranslation();
}
private void ResetView()
{
_scale = 1.0;
ScaleTransform.ScaleX = _scale;
ScaleTransform.ScaleY = _scale;
TranslateTransform.X = 0.0;
TranslateTransform.Y = 0.0;
ClampTranslation();
}
private void CenterImage()
{
TranslateTransform.X = 0.0;
TranslateTransform.Y = 0.0;
ClampTranslation();
}
private void OnZoomInClick(object sender, RoutedEventArgs e)
{
ZoomRelative(1.1);
}
private void OnZoomOutClick(object sender, RoutedEventArgs e)
{
ZoomRelative(0.9);
}
private void OnZoomToFitClick(object sender, RoutedEventArgs e)
{
ResetView();
CenterImage();
}
private void OnKeyDown(object sender, KeyRoutedEventArgs e)
{
switch (e.Key)
{
case VirtualKey.Add:
ZoomRelative(1.1);
e.Handled = true;
break;
case VirtualKey.Subtract:
ZoomRelative(0.9);
e.Handled = true;
break;
case VirtualKey.Number0:
case VirtualKey.NumberPad0:
ResetView();
CenterImage();
e.Handled = true;
break;
case VirtualKey.R:
CenterImage();
e.Handled = true;
break;
case VirtualKey.Escape:
CancelRequested?.Invoke(this, EventArgs.Empty);
e.Handled = true;
break;
case VirtualKey.Left:
case VirtualKey.A:
TranslateTransform.X += KeyboardPanStep;
ClampTranslation();
e.Handled = true;
break;
case VirtualKey.Right:
case VirtualKey.D:
TranslateTransform.X -= KeyboardPanStep;
ClampTranslation();
e.Handled = true;
break;
case VirtualKey.Up:
case VirtualKey.W:
TranslateTransform.Y += KeyboardPanStep;
ClampTranslation();
e.Handled = true;
break;
case VirtualKey.Down:
case VirtualKey.S:
TranslateTransform.Y -= KeyboardPanStep;
ClampTranslation();
e.Handled = true;
break;
}
}
/// <summary>
/// Zoom relative to viewport center (used by keyboard shortcuts and toolbar buttons).
/// </summary>
private void ZoomRelative(double factor)
{
var target = _scale * factor;
var center = new Point((_host?.ActualWidth ?? ActualWidth) / 2.0, (_host?.ActualHeight ?? ActualHeight) / 2.0);
SetScale(target, center);
}
private void OnDoubleTapped(object sender, DoubleTappedRoutedEventArgs e)
{
if (_scale < 1.5)
{
SetScale(2.0, e.GetPosition(this));
}
else
{
ResetView();
CenterImage();
}
}
private void OnPointerWheelChanged(object sender, PointerRoutedEventArgs e)
{
var point = e.GetCurrentPoint(this);
var delta = point.Properties.MouseWheelDelta;
if (delta == 0)
{
return;
}
var zoomIn = delta > 0;
var factor = zoomIn ? 1.1 : 0.9;
var newScale = _scale * factor;
SetScale(newScale, point.Position);
e.Handled = true;
}
/// <summary>
/// Applies zoom so the image point under <paramref name="pivot"/> stays fixed.
/// </summary>
/// <remarks>
/// The image element uses <c>RenderTransformOrigin="0.5,0.5"</c> and is centered
/// in the viewport via layout alignment. This means the effective transform origin
/// in viewport coordinates is the viewport center.
///
/// The full mapping from image-local to viewport space is:
/// screen = viewportCenter + (imgLocal - imgCenter) × scale + translate
///
/// Because the image is layout-centered, the viewport center acts as the origin
/// for the transform group, giving us:
/// screen = origin + relativeOffset × scale + translate
/// </remarks>
private void SetScale(double targetScale, Point pivot)
{
targetScale = Math.Clamp(targetScale, MinScale, MaxScale);
var prevScale = _scale;
if (targetScale == prevScale)
{
return;
}
// The effective transform origin is the viewport center
// (RenderTransformOrigin="0.5,0.5" + centered layout).
var vw = _host?.ActualWidth ?? ActualWidth;
var vh = _host?.ActualHeight ?? ActualHeight;
var originX = vw / 2.0;
var originY = vh / 2.0;
// Convert pivot to image-relative-to-origin space using the old transform:
// pivot = origin + rel × oldScale + oldTranslate
// rel = (pivot - origin - oldTranslate) / oldScale
var relX = (pivot.X - originX - TranslateTransform.X) / prevScale;
var relY = (pivot.Y - originY - TranslateTransform.Y) / prevScale;
_scale = targetScale;
ScaleTransform.ScaleX = _scale;
ScaleTransform.ScaleY = _scale;
// Solve for new translate so the same image point stays under the pivot:
// pivot = origin + rel × newScale + newTranslate
// newTranslate = pivot - origin - rel × newScale
TranslateTransform.X = pivot.X - originX - (relX * _scale);
TranslateTransform.Y = pivot.Y - originY - (relY * _scale);
ClampTranslation();
}
private void OnPointerPressed(object sender, PointerRoutedEventArgs e)
{
var point = e.GetCurrentPoint(this);
if (point.Properties.IsLeftButtonPressed)
{
_isPanning = true;
_lastPanPoint = point.Position;
CapturePointer(e.Pointer);
}
}
private void OnPointerMoved(object sender, PointerRoutedEventArgs e)
{
if (!_isPanning)
{
return;
}
var point = e.GetCurrentPoint(this);
var pos = point.Position;
var dx = pos.X - _lastPanPoint.X;
var dy = pos.Y - _lastPanPoint.Y;
_lastPanPoint = pos;
TranslateTransform.X += dx;
TranslateTransform.Y += dy;
ClampTranslation();
}
private void OnPointerReleased(object sender, PointerRoutedEventArgs e)
{
if (_isPanning)
{
_isPanning = false;
ReleasePointerCapture(e.Pointer);
}
}
private void ClampTranslation()
{
var iw = ZoomImage.ActualWidth * ScaleTransform.ScaleX;
var ih = ZoomImage.ActualHeight * ScaleTransform.ScaleY;
var vw = _host?.ActualWidth ?? ActualWidth;
var vh = _host?.ActualHeight ?? ActualHeight;
if (iw <= 0 || ih <= 0 || vw <= 0 || vh <= 0)
{
return;
}
double maxOffsetX;
double maxOffsetY;
if (iw <= vw)
{
maxOffsetX = 0;
TranslateTransform.X = 0;
}
else
{
maxOffsetX = Math.Max(0, ((iw - vw) / 2) + MinVisiblePadding);
}
if (ih <= vh)
{
maxOffsetY = 0;
TranslateTransform.Y = 0;
}
else
{
maxOffsetY = Math.Max(0, ((ih - vh) / 2) + MinVisiblePadding);
}
if (TranslateTransform.X > maxOffsetX)
{
TranslateTransform.X = maxOffsetX;
}
if (TranslateTransform.X < -maxOffsetX)
{
TranslateTransform.X = -maxOffsetX;
}
if (TranslateTransform.Y > maxOffsetY)
{
TranslateTransform.Y = maxOffsetY;
}
if (TranslateTransform.Y < -maxOffsetY)
{
TranslateTransform.Y = -maxOffsetY;
}
}
}

View File

@@ -72,7 +72,7 @@ public partial class Tag : Control
if (GetTemplateChild(TagIconBox) is IconBox iconBox)
{
iconBox.SourceRequested += IconCacheProvider.SourceRequested20;
iconBox.SourceRequested += IconProvider.SourceRequested20;
iconBox.Visibility = HasIcon ? Visibility.Visible : Visibility.Collapsed;
}
}

View File

@@ -18,6 +18,10 @@ public partial class ContentTemplateSelector : DataTemplateSelector
public DataTemplate? TreeTemplate { get; set; }
public DataTemplate? PlainTextTemplate { get; set; }
public DataTemplate? ImageTemplate { get; set; }
protected override DataTemplate? SelectTemplateCore(object item)
{
return item is ContentViewModel element
@@ -26,6 +30,8 @@ public partial class ContentTemplateSelector : DataTemplateSelector
ContentFormViewModel => FormTemplate,
ContentMarkdownViewModel => MarkdownTemplate,
ContentTreeViewModel => TreeTemplate,
ContentImageViewModel => ImageTemplate,
ContentPlainTextViewModel => PlainTextTemplate,
_ => null,
}
: null;

View File

@@ -53,7 +53,7 @@
AutomationProperties.AccessibilityView="Raw"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
SourceKey="{x:Bind Icon, Mode=OneWay}"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested16}" />
SourceRequested="{x:Bind help:IconProvider.SourceRequested16}" />
</local:DockItemControl.Icon>
</local:DockItemControl>
</DataTemplate>
@@ -168,7 +168,7 @@
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
SourceKey="{x:Bind IconViewModel, Mode=OneWay}"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested20}" />
SourceRequested="{x:Bind help:IconProvider.SourceRequested20}" />
<TextBlock
Grid.Column="1"
VerticalAlignment="Center"

View File

@@ -10,6 +10,8 @@
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:extViews="using:Microsoft.CmdPal.UI.ExtViews.Controls"
xmlns:help="using:Microsoft.CmdPal.UI.Helpers"
xmlns:local="using:Microsoft.CmdPal.UI"
xmlns:markdownImageProviders="using:Microsoft.CmdPal.UI.Helpers.MarkdownImageProviders"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -38,12 +40,17 @@
<cmdpalUI:ContentTemplateSelector
x:Key="ContentTemplateSelector"
FormTemplate="{StaticResource FormContentTemplate}"
ImageTemplate="{StaticResource ImageContentTemplate}"
MarkdownTemplate="{StaticResource MarkdownContentTemplate}"
PlainTextTemplate="{StaticResource PlainTextContentTemplate}"
TreeTemplate="{StaticResource TreeContentTemplate}" />
<cmdpalUI:ContentTemplateSelector
x:Key="NestedContentTemplateSelector"
FormTemplate="{StaticResource NestedFormContentTemplate}"
ImageTemplate="{StaticResource ImageContentTemplate}"
MarkdownTemplate="{StaticResource NestedMarkdownContentTemplate}"
PlainTextTemplate="{StaticResource PlainTextContentTemplate}"
TreeTemplate="{StaticResource TreeContentTemplate}" />
<DataTemplate x:Key="FormContentTemplate" x:DataType="viewModels:ContentFormViewModel">
@@ -62,6 +69,21 @@
</Grid>
</DataTemplate>
<DataTemplate x:Key="PlainTextContentTemplate" x:DataType="viewModels:ContentPlainTextViewModel">
<Grid Margin="0,4,4,4" Padding="12,8,8,8">
<extViews:PlainTextContentViewer
Text="{x:Bind Text, Mode=OneWay}"
UseMonospace="{x:Bind UseMonospace, Mode=OneWay}"
WordWrapEnabled="{x:Bind WordWrapEnabled, Mode=OneWay}" />
</Grid>
</DataTemplate>
<DataTemplate x:Key="ImageContentTemplate" x:DataType="viewModels:ContentImageViewModel">
<Grid Margin="0,4,4,4" Padding="12,8,8,8">
<extViews:ImageContentViewer DataContext="{x:Bind}" />
</Grid>
</DataTemplate>
<DataTemplate x:Key="NestedFormContentTemplate" x:DataType="viewModels:ContentFormViewModel">
<Grid>
<cmdPalControls:ContentFormControl ViewModel="{x:Bind}" />

View File

@@ -0,0 +1,83 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="Microsoft.CmdPal.UI.ExtViews.Controls.ImageContentViewer"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Microsoft.CmdPal.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:help="using:Microsoft.CmdPal.UI.Helpers"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid Padding="12,8,8,8">
<Border
x:Name="ImageBorder"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
BorderThickness="1">
<Grid>
<Border
x:Name="ImageCornerBorder"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="Transparent"
CornerRadius="8">
<Viewbox
x:Name="FitViewbox"
MaxWidth="{x:Bind ViewModel.MaxWidth, Mode=OneWay}"
MaxHeight="{x:Bind ViewModel.MaxHeight, Mode=OneWay}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
SizeChanged="FitViewbox_SizeChanged"
Stretch="Uniform">
<controls:IconBox
x:Name="Image"
AutomationProperties.AccessibilityView="Raw"
SourceKey="{x:Bind ViewModel.Image, Mode=OneWay}"
SourceRequested="{x:Bind help:IconProvider.SourceRequestedOriginal}" />
</Viewbox>
</Border>
<Canvas
x:Name="OverlayCanvas"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="{x:Null}">
<Button
x:Name="OpenZoomOverlayButton"
x:Uid="ImageContentViewer_OpenZoomOverlayButton"
Padding="8,4"
Click="OpenZoomOverlay_Click"
IsHitTestVisible="True"
Loaded="OpenZoomOverlayButton_Loaded"
SizeChanged="OpenZoomOverlayButton_SizeChanged"
Style="{StaticResource ButtonRevealStyle}">
<StackPanel Orientation="Horizontal" Spacing="6">
<SymbolIcon Symbol="Zoom" />
<TextBlock x:Uid="ImageContentViewer_ZoomAndScroll" />
</StackPanel>
</Button>
</Canvas>
</Grid>
</Border>
<Grid.ContextFlyout>
<MenuFlyout>
<MenuFlyoutItem
x:Uid="ImageContentViewer_OpenZoomViewer"
Click="OpenZoomOverlay_Click"
Icon="Zoom"
Text="Open image viewer" />
<MenuFlyoutSeparator />
<MenuFlyoutItem
x:Uid="ImageContentViewer_CopyImage"
Click="CopyImage_Click"
Icon="Copy"
Text="Copy image" />
<MenuFlyoutItem
x:Uid="ImageContentViewer_CopyImageUri"
Click="CopyImageUri_Click"
Icon="Copy"
Text="Copy image link" />
</MenuFlyout>
</Grid.ContextFlyout>
</Grid>
</UserControl>

View File

@@ -0,0 +1,416 @@
// 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.Runtime.CompilerServices;
using System.Runtime.InteropServices.WindowsRuntime;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.UI.Controls;
using Microsoft.CmdPal.UI.Helpers;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging;
using Windows.ApplicationModel.DataTransfer;
using Windows.Foundation;
using Windows.Graphics.Imaging;
using Windows.Storage.Streams;
using Windows.System;
using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue;
using DispatcherQueueTimer = Microsoft.UI.Dispatching.DispatcherQueueTimer;
using RS_ = Microsoft.CmdPal.UI.Helpers.ResourceLoaderInstance;
namespace Microsoft.CmdPal.UI.ExtViews.Controls;
public sealed partial class ImageContentViewer : UserControl
{
private const double MaxHeightSafetyPadding = 12 + 20 + 20; // a few pixels to be safe
public static readonly DependencyProperty UniformFitEnabledProperty = DependencyProperty.Register(
nameof(UniformFitEnabled), typeof(bool), typeof(ImageContentViewer), new PropertyMetadata(true));
private DispatcherQueueTimer? _resizeDebounceTimer;
private Microsoft.UI.Xaml.Controls.Page? _parentPage;
public bool UniformFitEnabled
{
get => (bool)GetValue(UniformFitEnabledProperty);
set => SetValue(UniformFitEnabledProperty, value);
}
public ContentImageViewModel? ViewModel
{
get => (ContentImageViewModel?)DataContext;
set => DataContext = value;
}
public ImageContentViewer()
{
InitializeComponent();
Loaded += ImageContentViewer_Loaded;
Unloaded += ImageContentViewer_Unloaded;
}
private void ImageContentViewer_Loaded(object sender, RoutedEventArgs e)
{
ApplyImage();
ApplyMaxDimensions();
if (ViewModel is not null)
{
ViewModel.PropertyChanged += ViewModel_PropertyChanged;
}
// Debounce timer for resize
var dq = DispatcherQueue.GetForCurrentThread();
_resizeDebounceTimer = dq.CreateTimer();
_resizeDebounceTimer.Interval = TimeSpan.FromMilliseconds(120);
_resizeDebounceTimer.IsRepeating = false;
_resizeDebounceTimer.Tick += ResizeDebounceTimer_Tick;
// Hook to parent Page size changes to keep MaxHeight in sync
_parentPage = FindParentPage();
if (_parentPage is not null)
{
_parentPage.SizeChanged += ParentPage_SizeChanged;
UpdateBorderMaxHeight();
}
else
{
// Fallback to this control's ActualHeight
UpdateBorderMaxHeight(useSelf: true);
}
// Initial overlay layout
LayoutOverlayButton();
}
private void ImageContentViewer_Unloaded(object sender, RoutedEventArgs e)
{
if (ViewModel is not null)
{
ViewModel.PropertyChanged -= ViewModel_PropertyChanged;
}
if (_resizeDebounceTimer is not null)
{
_resizeDebounceTimer.Tick -= ResizeDebounceTimer_Tick;
}
if (_parentPage is not null)
{
_parentPage.SizeChanged -= ParentPage_SizeChanged;
_parentPage = null;
}
}
private void ParentPage_SizeChanged(object sender, SizeChangedEventArgs e)
{
// Debounce updates a bit to avoid frequent layout passes
_resizeDebounceTimer?.Start();
LayoutOverlayButton();
}
private void ResizeDebounceTimer_Tick(DispatcherQueueTimer sender, object args)
{
UpdateBorderMaxHeight();
LayoutOverlayButton();
}
private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
var name = e.PropertyName;
if (name == nameof(ContentImageViewModel.Image))
{
ApplyImage();
LayoutOverlayButton();
}
else if (name is nameof(ContentImageViewModel.MaxWidth) or nameof(ContentImageViewModel.MaxHeight))
{
ApplyMaxDimensions();
UpdateBorderMaxHeight();
LayoutOverlayButton();
}
}
private void ApplyImage()
{
Image.SourceKey = ViewModel?.Image;
}
private void ApplyMaxDimensions()
{
// Apply optional max dimensions from the view model to the content container.
if (ViewModel is null)
{
return;
}
ImageBorder.MaxWidth = ViewModel.MaxWidth;
}
private async void CopyImage_Click(object sender, RoutedEventArgs e)
{
if (this.Image.Source is FontIconSource fontIconSource)
{
ClipboardHelper.SetText(fontIconSource.Glyph);
SendCopiedImageToast();
return;
}
try
{
var renderTarget = new RenderTargetBitmap();
await renderTarget.RenderAsync(this.Image);
var pixelBuffer = await renderTarget.GetPixelsAsync();
var pixels = pixelBuffer.ToArray();
var stream = new InMemoryRandomAccessStream();
var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, stream);
encoder.SetPixelData(
BitmapPixelFormat.Bgra8,
BitmapAlphaMode.Premultiplied,
(uint)renderTarget.PixelWidth,
(uint)renderTarget.PixelHeight,
96,
96,
pixels);
await encoder.FlushAsync();
var dataPackage = new DataPackage();
dataPackage.SetBitmap(RandomAccessStreamReference.CreateFromStream(stream));
Clipboard.SetContent(dataPackage);
SendCopiedImageToast();
}
catch
{
CopyImageUri_Click(sender, e);
}
}
private void CopyImageUri_Click(object sender, RoutedEventArgs e)
{
var iconVm = ViewModel?.Image;
var lightTheme = ActualTheme == ElementTheme.Light;
var data = lightTheme ? iconVm?.Light : iconVm?.Dark;
var srcKey = data?.Icon ?? string.Empty;
if (Uri.TryCreate(srcKey, UriKind.Absolute, out var uri) &&
uri.Scheme is "http" or "https")
{
ClipboardHelper.SetText(srcKey);
WeakReferenceMessenger.Default.Send(new ShowToastMessage(RS_.GetString("ImageContentViewer_Toast_CopiedLink")));
}
}
private static void SendCopiedImageToast()
{
WeakReferenceMessenger.Default.Send(new ShowToastMessage(RS_.GetString("ImageContentViewer_Toast_CopiedImage")));
}
private void OpenZoomOverlay_Click(object sender, RoutedEventArgs e)
{
// Full-window overlay using a Popup attached to this control's XamlRoot
if (XamlRoot is null)
{
return;
}
var popup = new Popup
{
IsLightDismissEnabled = false,
XamlRoot = XamlRoot,
};
var overlay = new Grid
{
Width = XamlRoot.Size.Width,
Height = XamlRoot.Size.Height,
Background = new AcrylicBrush
{
TintColor = Microsoft.UI.Colors.Black,
TintOpacity = 0.7,
TintLuminosityOpacity = 0.5,
FallbackColor = Microsoft.UI.Colors.Gray,
AlwaysUseFallback = false,
},
TabFocusNavigation = KeyboardNavigationMode.Local,
};
// Close popup on Esc pressed at overlay level
overlay.KeyDown += (s, args) =>
{
if (args.Key == VirtualKey.Escape)
{
popup.IsOpen = false;
args.Handled = true;
}
};
var closeBtn = new Button
{
Style = (Style)Application.Current.Resources["SubtleButtonStyle"],
Content = new SymbolIcon { Symbol = Symbol.Cancel },
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Top,
Margin = new Thickness(12),
};
closeBtn.Click += (_, __) => popup.IsOpen = false;
// Zoom/pan viewer using current icon
var viewer = new ImageViewer();
viewer.Initialize(Image.SourceKey);
viewer.HorizontalAlignment = HorizontalAlignment.Stretch;
viewer.VerticalAlignment = VerticalAlignment.Stretch;
// Also close when viewer requests cancellation (e.g., Escape from viewer)
viewer.CancelRequested += (_, __) =>
{
popup.IsOpen = false;
// Move focus back; otherwise it might go to place where sun doesn't shine in after closing the popup
this.Focus(FocusState.Programmatic);
};
overlay.Children.Add(viewer);
overlay.Children.Add(closeBtn);
popup.Child = overlay;
TypedEventHandler<XamlRoot, XamlRootChangedEventArgs>? onRootChanged = (_, _) =>
{
overlay.Width = popup.XamlRoot.Size.Width;
overlay.Height = popup.XamlRoot.Size.Height;
};
popup.XamlRoot.Changed += onRootChanged;
popup.Closed += (_, __) =>
{
popup.XamlRoot.Changed -= onRootChanged;
popup.Child = null;
};
popup.IsOpen = true;
overlay.Focus(FocusState.Programmatic);
}
private void OpenZoomOverlayButton_Loaded(object sender, RoutedEventArgs e)
{
LayoutOverlayButton();
}
private void OpenZoomOverlayButton_SizeChanged(object sender, SizeChangedEventArgs e)
{
LayoutOverlayButton();
}
private void FitViewbox_SizeChanged(object sender, SizeChangedEventArgs e)
{
LayoutOverlayButton();
}
private Microsoft.UI.Xaml.Controls.Page? FindParentPage()
{
DependencyObject? current = this;
while (current is not null)
{
current = VisualTreeHelper.GetParent(current);
if (current is Microsoft.UI.Xaml.Controls.Page page)
{
return page;
}
}
return null;
}
private void UpdateBorderMaxHeight(bool useSelf = false)
{
var height = useSelf ? ActualHeight : (_parentPage?.ActualHeight ?? ActualHeight);
if (height > 0)
{
var pageLimit = Math.Max(0, height - MaxHeightSafetyPadding);
if (ViewModel?.MaxHeight is double vmHeight and > 0)
{
ImageBorder.MaxHeight = Math.Min(pageLimit, vmHeight);
}
else
{
ImageBorder.MaxHeight = pageLimit;
}
}
else if (ViewModel?.MaxHeight is double vmHeight2 and > 0)
{
ImageBorder.MaxHeight = vmHeight2; // fallback if page height not ready
}
}
private void LayoutOverlayButton()
{
if (OpenZoomOverlayButton is null || FitViewbox is null || OverlayCanvas is null || Image is null)
{
return;
}
// If layout isn't ready, skip
if (FitViewbox.ActualWidth <= 0 || FitViewbox.ActualHeight <= 0 || OverlayCanvas.ActualWidth <= 0 || OverlayCanvas.ActualHeight <= 0)
{
return;
}
// Compute the transformed bounds of the image content relative to the overlay canvas.
// This accounts for Viewbox scaling/clipping due to MaxHeight constraints and custom max dimensions.
try
{
var gt = Image.TransformToVisual(OverlayCanvas);
var rect = gt.TransformBounds(new Rect(0, 0, Image.ActualWidth, Image.ActualHeight));
const double margin = 8.0;
var buttonWidth = OpenZoomOverlayButton.ActualWidth;
var x = rect.Right - buttonWidth - margin;
var y = rect.Top + margin;
// Clamp inside overlay bounds just in case
if (x < margin)
{
x = margin;
}
if (y < margin)
{
y = margin;
}
if (x > OverlayCanvas.ActualWidth - buttonWidth - margin)
{
x = OverlayCanvas.ActualWidth - buttonWidth - margin;
}
if (y > OverlayCanvas.ActualHeight - OpenZoomOverlayButton.ActualHeight - margin)
{
y = OverlayCanvas.ActualHeight - OpenZoomOverlayButton.ActualHeight - margin;
}
Canvas.SetLeft(OpenZoomOverlayButton, x);
Canvas.SetTop(OpenZoomOverlayButton, y);
}
catch
{
// Fallback: keep it at top-right of the viewbox area
const double margin = 8.0;
var buttonWidth = OpenZoomOverlayButton.ActualWidth;
var x = FitViewbox.ActualWidth - buttonWidth - margin;
var y = margin;
Canvas.SetLeft(OpenZoomOverlayButton, x);
Canvas.SetTop(OpenZoomOverlayButton, y);
}
}
}

View File

@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="Microsoft.CmdPal.UI.ExtViews.Controls.PlainTextContentViewer"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Border Padding="12,8,8,8">
<ScrollViewer
x:Name="Scroller"
HorizontalScrollBarVisibility="Auto"
HorizontalScrollMode="Auto"
IsDeferredScrollingEnabled="False"
IsHorizontalScrollChainingEnabled="False">
<TextBlock
x:Name="ContentTextBlock"
IsTextSelectionEnabled="True"
PointerWheelChanged="ContentTextBlock_PointerWheelChanged">
<TextBlock.ContextFlyout>
<MenuFlyout>
<MenuFlyoutItem
x:Name="CopySelectionItem"
x:Uid="PlainTextContentViewer_Copy"
Click="CopySelection_Click"
Icon="Copy"
Text="Copy">
<MenuFlyoutItem.KeyboardAccelerators>
<KeyboardAccelerator Key="C" Modifiers="Control" />
</MenuFlyoutItem.KeyboardAccelerators>
</MenuFlyoutItem>
<MenuFlyoutItem
x:Uid="PlainTextContentViewer_SelectAll"
Click="SelectAll_Click"
Icon="SelectAll"
Text="Select All">
<MenuFlyoutItem.KeyboardAccelerators>
<KeyboardAccelerator Key="A" Modifiers="Control" />
</MenuFlyoutItem.KeyboardAccelerators>
</MenuFlyoutItem>
<MenuFlyoutSeparator />
<ToggleMenuFlyoutItem
x:Uid="PlainTextContentViewer_WordWrap"
IsChecked="{x:Bind WordWrapEnabled, Mode=TwoWay}"
Text="Word Wrap" />
<ToggleMenuFlyoutItem
x:Uid="PlainTextContentViewer_MonospaceFont"
IsChecked="{x:Bind UseMonospace, Mode=TwoWay}"
Text="Monospace Font" />
<MenuFlyoutSeparator />
<MenuFlyoutItem
x:Uid="PlainTextContentViewer_ZoomIn"
Click="ZoomIn_Click"
Icon="ZoomIn"
Text="Zoom In">
<MenuFlyoutItem.KeyboardAccelerators>
<KeyboardAccelerator Key="Add" Modifiers="Control" />
</MenuFlyoutItem.KeyboardAccelerators>
</MenuFlyoutItem>
<MenuFlyoutItem
x:Uid="PlainTextContentViewer_ZoomOut"
Click="ZoomOut_Click"
Icon="ZoomOut"
Text="Zoom Out">
<MenuFlyoutItem.KeyboardAccelerators>
<KeyboardAccelerator Key="Subtract" Modifiers="Control" />
</MenuFlyoutItem.KeyboardAccelerators>
</MenuFlyoutItem>
<MenuFlyoutItem
x:Uid="PlainTextContentViewer_ResetZoom"
Click="ResetZoom_Click"
Text="Reset Zoom">
<MenuFlyoutItem.Icon>
<FontIcon Glyph="&#xE9A6;" />
</MenuFlyoutItem.Icon>
<MenuFlyoutItem.KeyboardAccelerators>
<KeyboardAccelerator Key="Number0" Modifiers="Control" />
</MenuFlyoutItem.KeyboardAccelerators>
</MenuFlyoutItem>
</MenuFlyout>
</TextBlock.ContextFlyout>
</TextBlock>
</ScrollViewer>
</Border>
</UserControl>

View File

@@ -0,0 +1,216 @@
// 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 Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.UI.Input;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Windows.System;
using Windows.UI.Core;
namespace Microsoft.CmdPal.UI.ExtViews.Controls;
public sealed partial class PlainTextContentViewer : UserControl
{
private const double MinFontSize = 8.0;
private const double MaxFontSize = 72.0;
private const double FontSizeStep = 2.0;
private double _defaultFontSize;
private double _fontSize;
public static readonly DependencyProperty WordWrapEnabledProperty = DependencyProperty.Register(
nameof(WordWrapEnabled), typeof(bool), typeof(PlainTextContentViewer), new PropertyMetadata(false, OnWrapChanged));
public static readonly DependencyProperty UseMonospaceProperty = DependencyProperty.Register(
nameof(UseMonospace), typeof(bool), typeof(PlainTextContentViewer), new PropertyMetadata(false, OnFontChanged));
public static readonly DependencyProperty TextProperty = DependencyProperty.Register(
nameof(Text), typeof(string), typeof(PlainTextContentViewer), new PropertyMetadata(default(string), OnTextChanged));
public bool WordWrapEnabled
{
get => (bool)GetValue(WordWrapEnabledProperty);
set => SetValue(WordWrapEnabledProperty, value);
}
public bool UseMonospace
{
get => (bool)GetValue(UseMonospaceProperty);
set => SetValue(UseMonospaceProperty, value);
}
public string Text
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
public PlainTextContentViewer()
{
InitializeComponent();
UpdateFont();
_defaultFontSize = ContentTextBlock.FontSize;
_fontSize = _defaultFontSize;
IsTabStop = true;
KeyDown += OnKeyDown;
}
private static void OnTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) => (d as PlainTextContentViewer)?.UpdateText();
private static void OnFontChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) => (d as PlainTextContentViewer)?.UpdateFont();
private static void OnWrapChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) => (d as PlainTextContentViewer)?.UpdateWordWrap();
private void UpdateText()
{
if (ContentTextBlock is null)
{
return;
}
ContentTextBlock.Text = Text;
}
private void UpdateWordWrap()
{
ContentTextBlock.TextWrapping = WordWrapEnabled ? TextWrapping.Wrap : TextWrapping.NoWrap;
Scroller.HorizontalScrollBarVisibility = WordWrapEnabled ? ScrollBarVisibility.Disabled : ScrollBarVisibility.Auto;
InvalidateLayout();
}
private void UpdateFont()
{
if (ContentTextBlock is null)
{
return;
}
try
{
ContentTextBlock.FontFamily = UseMonospace ? new FontFamily("Cascadia Mono, Consolas") : FontFamily.XamlAutoFontFamily;
}
catch
{
ContentTextBlock.FontFamily = FontFamily.XamlAutoFontFamily;
}
InvalidateLayout();
}
private void CopySelection_Click(object sender, RoutedEventArgs e)
{
var txt = string.IsNullOrEmpty(ContentTextBlock?.SelectedText) ? ContentTextBlock?.Text : ContentTextBlock?.SelectedText;
if (!string.IsNullOrEmpty(txt))
{
ClipboardHelper.SetText(txt);
}
}
private void SelectAll_Click(object sender, RoutedEventArgs e)
{
ContentTextBlock.SelectAll();
}
private void ZoomIn()
{
_fontSize = Math.Min(_fontSize + FontSizeStep, MaxFontSize);
ApplyFontSize();
}
private void ZoomOut()
{
_fontSize = Math.Max(_fontSize - FontSizeStep, MinFontSize);
ApplyFontSize();
}
private void ResetZoom()
{
_fontSize = _defaultFontSize;
ApplyFontSize();
}
private void ApplyFontSize()
{
ContentTextBlock.FontSize = _fontSize;
InvalidateLayout();
}
/// <summary>
/// Changing font properties on a TextBlock inside a ScrollViewer can leave
/// stale layout state, causing the text to disappear until the next
/// interaction. Re-setting the Text property forces the TextBlock to
/// discard its cached layout and fully re-render.
/// </summary>
private void InvalidateLayout()
{
var text = ContentTextBlock.Text;
ContentTextBlock.Text = string.Empty;
ContentTextBlock.Text = text;
}
private void ZoomIn_Click(object sender, RoutedEventArgs e) => ZoomIn();
private void ZoomOut_Click(object sender, RoutedEventArgs e) => ZoomOut();
private void ResetZoom_Click(object sender, RoutedEventArgs e) => ResetZoom();
private static bool IsCtrlDown()
{
var state = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Control);
return (state & CoreVirtualKeyStates.Down) == CoreVirtualKeyStates.Down;
}
private void OnKeyDown(object sender, KeyRoutedEventArgs e)
{
if (!IsCtrlDown())
{
return;
}
switch (e.Key)
{
case VirtualKey.Add:
case (VirtualKey)187: // =/+ key
ZoomIn();
e.Handled = true;
break;
case VirtualKey.Subtract:
case (VirtualKey)189: // -/_ key
ZoomOut();
e.Handled = true;
break;
case VirtualKey.Number0:
case VirtualKey.NumberPad0:
ResetZoom();
e.Handled = true;
break;
}
}
private void ContentTextBlock_PointerWheelChanged(object sender, PointerRoutedEventArgs e)
{
if (!IsCtrlDown())
{
return;
}
var point = e.GetCurrentPoint(ContentTextBlock);
var delta = point.Properties.MouseWheelDelta;
if (delta > 0)
{
ZoomIn();
}
else if (delta < 0)
{
ZoomOut();
}
e.Handled = true;
}
}

View File

@@ -365,7 +365,7 @@
AutomationProperties.AccessibilityView="Raw"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
SourceKey="{x:Bind Icon, Mode=OneWay}"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested20}" />
SourceRequested="{x:Bind help:IconProvider.SourceRequested20}" />
<!--
Title and subtitle are intentionally in a nested Grid instead in the outer container,
@@ -476,7 +476,7 @@
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorPrimary}"
SourceKey="{x:Bind Icon, Mode=OneWay}"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested32}" />
SourceRequested="{x:Bind help:IconProvider.SourceRequested32}" />
</StackPanel>
</DataTemplate>
@@ -503,7 +503,7 @@
FontSize="14"
Foreground="{ThemeResource TextFillColorPrimary}"
SourceKey="{x:Bind Icon, Mode=OneWay}"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested64}" />
SourceRequested="{x:Bind help:IconProvider.SourceRequested64}" />
<TextBlock
x:Name="TitleTextBlock"
Grid.Row="1"
@@ -548,7 +548,7 @@
CornerRadius="4"
Foreground="{ThemeResource TextFillColorPrimary}"
SourceKey="{x:Bind Icon, Mode=OneWay}"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested256}" />
SourceRequested="{x:Bind help:IconProvider.SourceRequested256}" />
</Viewbox>
</Grid>
@@ -674,7 +674,7 @@
Margin="8"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
SourceKey="{x:Bind ViewModel.EmptyContent.Icon, Mode=OneWay}"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested64}" />
SourceRequested="{x:Bind help:IconProvider.SourceRequested64}" />
<TextBlock
Margin="0,4,0,0"
HorizontalAlignment="Center"

View File

@@ -16,6 +16,8 @@ namespace Microsoft.CmdPal.UI.Helpers;
internal sealed partial class IconLoaderService : IIconLoaderService
{
public static readonly Size NoResize = Size.Empty;
private const DispatcherQueuePriority LoadingPriorityOnDispatcher = DispatcherQueuePriority.Low;
private const int DefaultIconSize = 256;
private const int MaxWorkerCount = 4;
@@ -214,11 +216,6 @@ internal sealed partial class IconLoaderService : IIconLoaderService
private static IconSource? GetStringIconSource(string iconString, string? fontFamily, Size size)
{
var iconSize = (int)Math.Max(size.Width, size.Height);
if (iconSize == 0)
{
iconSize = DefaultIconSize;
}
return IconPathConverter.IconSourceMUX(iconString, fontFamily, iconSize);
}
}

View File

@@ -11,7 +11,7 @@ namespace Microsoft.CmdPal.UI.Helpers;
/// <summary>
/// Common async event handler provides the cache lookup function for the <see cref="IconBox.SourceRequested"/> deferred event.
/// </summary>
public static partial class IconCacheProvider
public static partial class IconProvider
{
/*
Memory Usage Considerations (raw estimates):
@@ -29,6 +29,7 @@ public static partial class IconCacheProvider
private static IIconSourceProvider _provider32 = null!;
private static IIconSourceProvider _provider64 = null!;
private static IIconSourceProvider _provider256 = null!;
private static IIconSourceProvider _providerUnbound = null!;
public static void Initialize(IServiceProvider serviceProvider)
{
@@ -37,6 +38,7 @@ public static partial class IconCacheProvider
_provider32 = serviceProvider.GetRequiredKeyedService<IIconSourceProvider>(WellKnownIconSize.Size32);
_provider64 = serviceProvider.GetRequiredKeyedService<IIconSourceProvider>(WellKnownIconSize.Size64);
_provider256 = serviceProvider.GetRequiredKeyedService<IIconSourceProvider>(WellKnownIconSize.Size256);
_providerUnbound = serviceProvider.GetRequiredKeyedService<IIconSourceProvider>(WellKnownIconSize.Unbound);
}
private static async void SourceRequestedCore(IIconSourceProvider service, SourceRequestedEventArgs args)
@@ -80,5 +82,8 @@ public static partial class IconCacheProvider
public static void SourceRequested256(IconBox sender, SourceRequestedEventArgs args)
=> SourceRequestedCore(_provider256, args);
public static void SourceRequestedOriginal(IconBox sender, SourceRequestedEventArgs args)
=> SourceRequestedCore(_providerUnbound, args);
#pragma warning restore IDE0060 // Remove unused parameter
}

View File

@@ -36,6 +36,10 @@ internal static class IconServiceRegistration
WellKnownIconSize.Size256,
(_, _) => new CachedIconSourceProvider(loader, 256, 64));
services.AddKeyedSingleton<IIconSourceProvider>(
WellKnownIconSize.Unbound,
(_, _) => new IconSourceProvider(loader, IconLoaderService.NoResize, isPriority: true));
return services;
}
}

View File

@@ -12,15 +12,17 @@ internal sealed class IconSourceProvider : IIconSourceProvider
{
private readonly IconLoaderService _loader;
private readonly Size _iconSize;
private readonly bool _isPriority;
public IconSourceProvider(IconLoaderService loader, Size iconSize)
public IconSourceProvider(IconLoaderService loader, Size iconSize, bool isPriority = false)
{
_loader = loader;
_iconSize = iconSize;
_isPriority = isPriority;
}
public IconSourceProvider(IconLoaderService loader, int iconSize)
: this(loader, new Size(iconSize, iconSize))
public IconSourceProvider(IconLoaderService loader, int iconSize, bool isPriority = false)
: this(loader, new Size(iconSize, iconSize), isPriority)
{
}
@@ -34,7 +36,8 @@ internal sealed class IconSourceProvider : IIconSourceProvider
icon.Data?.Unsafe,
_iconSize,
scale,
tcs);
tcs,
_isPriority ? IconLoadPriority.High : IconLoadPriority.Low);
return tcs.Task;
}

View File

@@ -11,4 +11,5 @@ internal enum WellKnownIconSize
Size32 = 32,
Size64 = 64,
Size256 = 256,
Unbound = -1,
}

View File

@@ -84,9 +84,12 @@
<None Remove="Controls\CommandPalettePreview.xaml" />
<None Remove="Controls\DevRibbon.xaml" />
<None Remove="Controls\FallbackRankerDialog.xaml" />
<None Remove="Controls\ImageViewer.xaml" />
<None Remove="Controls\ScreenPreview.xaml" />
<None Remove="Controls\ScrollContainer.xaml" />
<None Remove="Controls\SearchBar.xaml" />
<None Remove="ExtViews\Controls\ImageContentViewer.xaml" />
<None Remove="ExtViews\Controls\PlainTextContentViewer.xaml" />
<None Remove="ListDetailPage.xaml" />
<None Remove="LoadingPage.xaml" />
<None Remove="MainPage.xaml" />
@@ -103,6 +106,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Segmented" />
@@ -315,6 +319,11 @@
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="ExtViews\Controls\PlainTextContentViewer.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<!-- </AdaptiveCardsWorkaround> -->
<!-- Build information -->

View File

@@ -92,7 +92,7 @@
Height="16"
Margin="0,3,8,0"
SourceKey="{x:Bind Icon, Mode=OneWay}"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested20}" />
SourceRequested="{x:Bind help:IconProvider.SourceRequested20}" />
<TextBlock Style="{StaticResource BodyTextBlockStyle}" Text="{x:Bind Name}" />
</StackPanel>
</Button>
@@ -301,7 +301,7 @@
ui:VisualExtensions.NormalizedCenterPoint="0.5,0.5"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
SourceKey="{x:Bind ViewModel.CurrentPage.Icon, Mode=OneWay}"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested20}"
SourceRequested="{x:Bind help:IconProvider.SourceRequested20}"
Visibility="{x:Bind ViewModel.CurrentPage.IsRootPage, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}">
<animations:Implicit.ShowAnimations>
<animations:OpacityAnimation
@@ -434,7 +434,7 @@
HorizontalAlignment="Left"
AutomationProperties.AccessibilityView="Raw"
SourceKey="{x:Bind ViewModel.Details.HeroImage, Mode=OneWay}"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested64}"
SourceRequested="{x:Bind help:IconProvider.SourceRequested64}"
Visibility="{x:Bind HasHeroImage, Mode=OneWay}" />
<TextBlock

View File

@@ -233,7 +233,7 @@
Height="20"
AutomationProperties.AccessibilityView="Raw"
SourceKey="{x:Bind Icon, Mode=OneWay}"
SourceRequested="{x:Bind helpers:IconCacheProvider.SourceRequested20}" />
SourceRequested="{x:Bind helpers:IconProvider.SourceRequested20}" />
</cpControls:ContentIcon.Content>
</cpControls:ContentIcon>
</controls:SettingsCard.HeaderIcon>

View File

@@ -61,7 +61,7 @@
Height="20"
AutomationProperties.AccessibilityView="Raw"
SourceKey="{x:Bind ViewModel.Icon, Mode=OneWay}"
SourceRequested="{x:Bind helpers:IconCacheProvider.SourceRequested20}" />
SourceRequested="{x:Bind helpers:IconProvider.SourceRequested20}" />
</cpcontrols:ContentIcon.Content>
</cpcontrols:ContentIcon>
</controls:SettingsCard.HeaderIcon>
@@ -95,7 +95,7 @@
Height="20"
AutomationProperties.AccessibilityView="Raw"
SourceKey="{x:Bind Icon, Mode=OneWay}"
SourceRequested="{x:Bind helpers:IconCacheProvider.SourceRequested20}" />
SourceRequested="{x:Bind helpers:IconProvider.SourceRequested20}" />
</cpcontrols:ContentIcon.Content>
</cpcontrols:ContentIcon>
</controls:SettingsExpander.HeaderIcon>
@@ -167,7 +167,7 @@
Height="20"
AutomationProperties.AccessibilityView="Raw"
SourceKey="{x:Bind Icon, Mode=OneWay}"
SourceRequested="{x:Bind helpers:IconCacheProvider.SourceRequested20}" />
SourceRequested="{x:Bind helpers:IconProvider.SourceRequested20}" />
</cpcontrols:ContentIcon.Content>
</cpcontrols:ContentIcon>
</controls:SettingsExpander.HeaderIcon>

View File

@@ -241,7 +241,7 @@
Height="20"
AutomationProperties.AccessibilityView="Raw"
SourceKey="{x:Bind Icon, Mode=OneWay}"
SourceRequested="{x:Bind helpers:IconCacheProvider.SourceRequested20}" />
SourceRequested="{x:Bind helpers:IconProvider.SourceRequested20}" />
</controls:Case>
<controls:Case Value="False">
<Image

View File

@@ -958,13 +958,13 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<data name="Dock_AddBand_NoCommandsAvailable.Text" xml:space="preserve">
<value>All available bands are already pinned.</value>
</data>
<data name="Dock_Pin_Instruction.Text" xml:space="preserve">
<data name="Dock_Pin_Instruction.Text" xml:space="preserve">
<value>To pin commands, extensions or apps, use the Pin to Dock command in Command Palette.</value>
</data>
<data name="Dock_Bands_Header.Text" xml:space="preserve">
<data name="Dock_Bands_Header.Text" xml:space="preserve">
<value>Bands</value>
</data>
<data name="Dock_AddBand_StartTooltip.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<data name="Dock_AddBand_StartTooltip.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>Add band to start</value>
</data>
<data name="Dock_AddBand_CenterTooltip.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
@@ -987,7 +987,79 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
</data>
<data name="SettingsPage_NewInfoBadge.Text" xml:space="preserve">
<value>NEW</value>
<comment>Must be all caps</comment>
<comment>Must be all caps</comment>
</data>
<data name="ImageContentViewer_ZoomAndScroll.Text" xml:space="preserve">
<value>Zoom &amp; Scroll</value>
</data>
<data name="ImageContentViewer_OpenZoomViewer.Text" xml:space="preserve">
<value>Open image viewer</value>
</data>
<data name="ImageContentViewer_CopyImage.Text" xml:space="preserve">
<value>Copy image</value>
</data>
<data name="ImageViewer_ZoomIn.Content" xml:space="preserve">
<value>Zoom in</value>
</data>
<data name="ImageViewer_ZoomOut.Content" xml:space="preserve">
<value>Zoom out</value>
</data>
<data name="ImageViewer_ZoomToFit.Content" xml:space="preserve">
<value>Zoom to fit</value>
</data>
<data name="PlainTextContentViewer_Copy.Text" xml:space="preserve">
<value>Copy</value>
</data>
<data name="PlainTextContentViewer_SelectAll.Text" xml:space="preserve">
<value>Select all</value>
</data>
<data name="PlainTextContentViewer_WordWrap.Text" xml:space="preserve">
<value>Word wrap</value>
</data>
<data name="PlainTextContentViewer_MonospaceFont.Text" xml:space="preserve">
<value>Monospace font</value>
</data>
<data name="PlainTextContentViewer_ZoomIn.Text" xml:space="preserve">
<value>Zoom in</value>
</data>
<data name="PlainTextContentViewer_ZoomOut.Text" xml:space="preserve">
<value>Zoom out</value>
</data>
<data name="PlainTextContentViewer_ResetZoom.Text" xml:space="preserve">
<value>Reset zoom</value>
</data>
<data name="ImageContentViewer_Toast_CopiedImage" xml:space="preserve">
<value>Copied image to clipboard</value>
</data>
<data name="ImageContentViewer_Toast_CopiedLink" xml:space="preserve">
<value>Copied link to clipboard</value>
</data>
<data name="ImageViewer_ZoomInButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Zoom in</value>
</data>
<data name="ImageViewer_ZoomInButton.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>Zoom in (Ctrl+Plus key)</value>
</data>
<data name="ImageViewer_ZoomOutButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Zoom out</value>
</data>
<data name="ImageViewer_ZoomOutButton.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>Zoom out (Ctrl+Minus key)</value>
</data>
<data name="ImageViewer_ZoomToFitButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Zoom to fit</value>
</data>
<data name="ImageViewer_ZoomToFitButton.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>Zoom to fit (Ctrl+0)</value>
</data>
<data name="ImageContentViewer_OpenZoomOverlayButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Open image viewer</value>
</data>
<data name="ImageContentViewer_OpenZoomOverlayButton.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
<value>Open image viewer</value>
</data>
<data name="ImageContentViewer_CopyImageUri.Text" xml:space="preserve">
<value>Copy image link</value>
</data>
<data name="PinToDock_DialogTitle" xml:space="preserve">
<value>Pin to Dock</value>

View File

@@ -2,7 +2,7 @@
#include "IconPathConverter.h"
#include "IconPathConverter.g.cpp"
#include "FontIconGlyphClassifier.h"
#include "FontIconGlyphClassifier.h"
#include <Shlobj.h>
#include <Shlobj_core.h>
@@ -115,10 +115,13 @@ namespace winrt::Microsoft::Terminal::UI::implementation
{
typename ImageIconSource<TIconSource>::type iconSource;
winrt::Microsoft::UI::Xaml::Media::Imaging::SvgImageSource source{ iconUri };
source.RasterizePixelWidth(static_cast<double>(targetSize));
// Set only single dimension here; the image might not be square and
// this will preserve the aspect ratio (for the price of keeping height unbound).
// source.RasterizePixelHeight(static_cast<double>(targetSize));
if (targetSize > 0)
{
source.RasterizePixelWidth(static_cast<double>(targetSize));
// Set only single dimension here; the image might not be square and
// this will preserve the aspect ratio (for the price of keeping height unbound).
// source.RasterizePixelHeight(static_cast<double>(targetSize));
}
iconSource.ImageSource(source);
return iconSource;
}
@@ -126,10 +129,13 @@ namespace winrt::Microsoft::Terminal::UI::implementation
{
typename ImageIconSource<TIconSource>::type iconSource;
winrt::Microsoft::UI::Xaml::Media::Imaging::BitmapImage bitmapImage;
bitmapImage.DecodePixelWidth(targetSize);
// Set only single dimension here; the image might not be square and
// this will preserve the aspect ratio (for the price of keeping height unbound).
// bitmapImage.DecodePixelHeight(targetSize);
if (targetSize > 0)
{
bitmapImage.DecodePixelWidth(targetSize);
// Set only single dimension here; the image might not be square and
// this will preserve the aspect ratio (for the price of keeping height unbound).
// bitmapImage.DecodePixelHeight(targetSize);
}
bitmapImage.UriSource(iconUri);
iconSource.ImageSource(bitmapImage);
return iconSource;
@@ -210,7 +216,7 @@ namespace winrt::Microsoft::Terminal::UI::implementation
typename FontIconSource<TIconSource>::type icon;
icon.FontFamily(winrt::Microsoft::UI::Xaml::Media::FontFamily{ family });
icon.FontSize(targetSize);
icon.FontSize(targetSize > 0 ? targetSize : 8);
icon.Glyph(glyph_kind == FontIconGlyphKind::Invalid ? L"\u25CC" : iconPath);
iconSource = icon;
}
@@ -349,7 +355,7 @@ namespace winrt::Microsoft::Terminal::UI::implementation
// * C:\Program Files\PowerShell\6-preview\pwsh.exe, 0 (this doesn't exist for me)
// * C:\Program Files\PowerShell\7\pwsh.exe, 0
const auto swBitmap{ _getBitmapFromIconFileAsync(winrt::hstring{ iconPathWithoutIndex }, index, targetSize) };
const auto swBitmap{ _getBitmapFromIconFileAsync(winrt::hstring{ iconPathWithoutIndex }, index, targetSize >= 0 ? targetSize : 256) };
if (swBitmap == nullptr)
{
return nullptr;
@@ -379,7 +385,8 @@ namespace winrt::Microsoft::Terminal::UI::implementation
return imageIconSource;
}
Microsoft::UI::Xaml::Controls::IconElement IconPathConverter::IconMUX(const winrt::hstring& iconPath) {
Microsoft::UI::Xaml::Controls::IconElement IconPathConverter::IconMUX(const winrt::hstring& iconPath)
{
return IconMUX(iconPath, 24);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 760 KiB

View File

@@ -39,6 +39,62 @@ internal sealed partial class SampleContentPage : ContentPage
}
}
[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Sample code")]
internal sealed partial class SamplePlainTextContentPage : ContentPage
{
private readonly PlainTextContent _samplePlainText = new()
{
Text = """
# Sample Plain Text Content
This is a sample plain text content page.
You can right-click the content and switch wrap mode on or off, or change the font.
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
""",
};
private readonly PlainTextContent _samplePlainText2 = new()
{
Text = """
# Sample Plain Text Content
This is a sample plain text content page. This one is monospace and wraps by default.
You can right-click the content and switch wrap mode on or off, or change the font.
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
""",
FontFamily = FontFamily.Monospace,
WrapWords = true,
};
public override IContent[] GetContent() => [_samplePlainText, _samplePlainText2];
public SamplePlainTextContentPage()
{
Name = "Plain Text";
Title = "Sample Plain Text Content";
Icon = new IconInfo("\uE8D2"); // Text Document
}
}
[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Sample code")]
internal sealed partial class SampleImageContentPage : ContentPage
{
private readonly ImageContent _sampleImage = new(IconHelpers.FromRelativePath("Assets/Images/win-11-bloom-6k.jpg"));
private readonly ImageContent _sampleImage2 = new(IconHelpers.FromRelativePath("Assets/Images/win-11-bloom-6k.jpg")) { MaxWidth = 200, MaxHeight = 200 };
private readonly ImageContent _sampleImage3 = new(IconHelpers.FromRelativePath("Assets/Images/FluentEmojiChipmunk.svg")) { MaxWidth = 200, MaxHeight = 200 };
public override IContent[] GetContent() => [_sampleImage, _sampleImage2, _sampleImage3];
public SampleImageContentPage()
{
Name = "Image";
Title = "Sample Image Content";
Icon = new IconInfo("\uE722"); // Picture
}
}
[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Sample code")]
internal sealed partial class SampleContentForm : FormContent
{

View File

@@ -66,6 +66,16 @@ public partial class SamplesListPage : ListPage
Title = "Sample content page",
Subtitle = "Display mixed forms, markdown, and other types of content",
},
new ListItem(new SamplePlainTextContentPage())
{
Title = "Sample plain text content page",
Subtitle = "Display a page of plain text content",
},
new ListItem(new SampleImageContentPage())
{
Title = "Sample image content page",
Subtitle = "Display a page with an image",
},
new ListItem(new SampleTreeContentPage())
{
Title = "Sample nested content",

View File

@@ -0,0 +1,25 @@
// 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.
namespace Microsoft.CommandPalette.Extensions.Toolkit;
public partial class ImageContent : BaseObservable, IImageContent
{
public const int Unlimited = -1;
public IIconInfo? Image { get; set => SetProperty(ref field, value); }
public int MaxHeight { get; set => SetProperty(ref field, value); } = Unlimited;
public int MaxWidth { get; set => SetProperty(ref field, value); } = Unlimited;
public ImageContent()
{
}
public ImageContent(IIconInfo? image)
{
Image = image;
}
}

View File

@@ -0,0 +1,23 @@
// 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.
namespace Microsoft.CommandPalette.Extensions.Toolkit;
public partial class PlainTextContent : BaseObservable, IPlainTextContent
{
public FontFamily FontFamily { get; set => SetProperty(ref field, value); }
public bool WrapWords { get; set => SetProperty(ref field, value); }
public string? Text { get; set => SetProperty(ref field, value); }
public PlainTextContent()
{
}
public PlainTextContent(string? text)
{
Text = text;
}
}

View File

@@ -349,6 +349,26 @@ namespace Microsoft.CommandPalette.Extensions
IContent[] GetChildren();
}
enum FontFamily
{
UserInterface,
Monospace,
};
[contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)]
interface IPlainTextContent requires IContent {
String Text { get; };
FontFamily FontFamily { get; };
Boolean WrapWords { get; };
}
[contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)]
interface IImageContent requires IContent {
IIconInfo Image { get; };
Int32 MaxWidth { get; };
Int32 MaxHeight { get; };
}
[contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)]
interface IContentPage requires IPage, INotifyItemsChanged {
IContent[] GetContent();