mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-03 09:46:54 +02:00
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:
@@ -20,6 +20,8 @@ public partial class CommandPaletteContentPageViewModel : ContentPageViewModel
|
|||||||
IFormContent form => new ContentFormViewModel(form, context),
|
IFormContent form => new ContentFormViewModel(form, context),
|
||||||
IMarkdownContent markdown => new ContentMarkdownViewModel(markdown, context),
|
IMarkdownContent markdown => new ContentMarkdownViewModel(markdown, context),
|
||||||
ITreeContent tree => new ContentTreeViewModel(tree, context),
|
ITreeContent tree => new ContentTreeViewModel(tree, context),
|
||||||
|
IPlainTextContent plainText => new ContentPlainTextViewModel(plainText, context),
|
||||||
|
IImageContent image => new ContentImageViewModel(image, context),
|
||||||
_ => null,
|
_ => null,
|
||||||
};
|
};
|
||||||
return viewModel;
|
return viewModel;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -58,6 +58,8 @@ public partial class ContentTreeViewModel(ITreeContent _tree, WeakReference<IPag
|
|||||||
IFormContent form => new ContentFormViewModel(form, context),
|
IFormContent form => new ContentFormViewModel(form, context),
|
||||||
IMarkdownContent markdown => new ContentMarkdownViewModel(markdown, context),
|
IMarkdownContent markdown => new ContentMarkdownViewModel(markdown, context),
|
||||||
ITreeContent tree => new ContentTreeViewModel(tree, context),
|
ITreeContent tree => new ContentTreeViewModel(tree, context),
|
||||||
|
IPlainTextContent plainText => new ContentPlainTextViewModel(plainText, context),
|
||||||
|
IImageContent image => new ContentImageViewModel(image, context),
|
||||||
_ => null,
|
_ => null,
|
||||||
};
|
};
|
||||||
return viewModel;
|
return viewModel;
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ public partial class App : Application, IDisposable
|
|||||||
|
|
||||||
Services = ConfigureServices(appInfoService);
|
Services = ConfigureServices(appInfoService);
|
||||||
|
|
||||||
IconCacheProvider.Initialize(Services);
|
IconProvider.Initialize(Services);
|
||||||
|
|
||||||
this.InitializeComponent();
|
this.InitializeComponent();
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
Margin="4,0,0,0"
|
Margin="4,0,0,0"
|
||||||
HorizontalAlignment="Left"
|
HorizontalAlignment="Left"
|
||||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested20}" />
|
SourceRequested="{x:Bind help:IconProvider.SourceRequested20}" />
|
||||||
<TextBlock
|
<TextBlock
|
||||||
x:Name="TitleTextBlock"
|
x:Name="TitleTextBlock"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
HorizontalAlignment="Left"
|
HorizontalAlignment="Left"
|
||||||
Foreground="{ThemeResource SystemFillColorCriticalBrush}"
|
Foreground="{ThemeResource SystemFillColorCriticalBrush}"
|
||||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested20}" />
|
SourceRequested="{x:Bind help:IconProvider.SourceRequested20}" />
|
||||||
<TextBlock
|
<TextBlock
|
||||||
x:Name="TitleTextBlock"
|
x:Name="TitleTextBlock"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
Height="16"
|
Height="16"
|
||||||
AutomationProperties.AccessibilityView="Raw"
|
AutomationProperties.AccessibilityView="Raw"
|
||||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||||
SourceRequested="{x:Bind helpers:IconCacheProvider.SourceRequested20}" />
|
SourceRequested="{x:Bind helpers:IconProvider.SourceRequested20}" />
|
||||||
</cpcontrols:ContentIcon.Content>
|
</cpcontrols:ContentIcon.Content>
|
||||||
</cpcontrols:ContentIcon>
|
</cpcontrols:ContentIcon>
|
||||||
</controls:SettingsCard.HeaderIcon>
|
</controls:SettingsCard.HeaderIcon>
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
HorizontalAlignment="Left"
|
HorizontalAlignment="Left"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
SourceKey="{x:Bind Icon}"
|
SourceKey="{x:Bind Icon}"
|
||||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested20}" />
|
SourceRequested="{x:Bind help:IconProvider.SourceRequested20}" />
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
Margin="0,0,8,0"
|
Margin="0,0,8,0"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
AutomationProperties.AccessibilityView="Raw"
|
AutomationProperties.AccessibilityView="Raw"
|
||||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested20}"
|
SourceRequested="{x:Bind help:IconProvider.SourceRequested20}"
|
||||||
Visibility="Collapsed" />
|
Visibility="Collapsed" />
|
||||||
<TextBlock x:Name="SelectedFilterText" VerticalAlignment="Center" />
|
<TextBlock x:Name="SelectedFilterText" VerticalAlignment="Center" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|||||||
@@ -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="" />
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -72,7 +72,7 @@ public partial class Tag : Control
|
|||||||
|
|
||||||
if (GetTemplateChild(TagIconBox) is IconBox iconBox)
|
if (GetTemplateChild(TagIconBox) is IconBox iconBox)
|
||||||
{
|
{
|
||||||
iconBox.SourceRequested += IconCacheProvider.SourceRequested20;
|
iconBox.SourceRequested += IconProvider.SourceRequested20;
|
||||||
iconBox.Visibility = HasIcon ? Visibility.Visible : Visibility.Collapsed;
|
iconBox.Visibility = HasIcon ? Visibility.Visible : Visibility.Collapsed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ public partial class ContentTemplateSelector : DataTemplateSelector
|
|||||||
|
|
||||||
public DataTemplate? TreeTemplate { get; set; }
|
public DataTemplate? TreeTemplate { get; set; }
|
||||||
|
|
||||||
|
public DataTemplate? PlainTextTemplate { get; set; }
|
||||||
|
|
||||||
|
public DataTemplate? ImageTemplate { get; set; }
|
||||||
|
|
||||||
protected override DataTemplate? SelectTemplateCore(object item)
|
protected override DataTemplate? SelectTemplateCore(object item)
|
||||||
{
|
{
|
||||||
return item is ContentViewModel element
|
return item is ContentViewModel element
|
||||||
@@ -26,6 +30,8 @@ public partial class ContentTemplateSelector : DataTemplateSelector
|
|||||||
ContentFormViewModel => FormTemplate,
|
ContentFormViewModel => FormTemplate,
|
||||||
ContentMarkdownViewModel => MarkdownTemplate,
|
ContentMarkdownViewModel => MarkdownTemplate,
|
||||||
ContentTreeViewModel => TreeTemplate,
|
ContentTreeViewModel => TreeTemplate,
|
||||||
|
ContentImageViewModel => ImageTemplate,
|
||||||
|
ContentPlainTextViewModel => PlainTextTemplate,
|
||||||
_ => null,
|
_ => null,
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
|
|||||||
@@ -53,7 +53,7 @@
|
|||||||
AutomationProperties.AccessibilityView="Raw"
|
AutomationProperties.AccessibilityView="Raw"
|
||||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested16}" />
|
SourceRequested="{x:Bind help:IconProvider.SourceRequested16}" />
|
||||||
</local:DockItemControl.Icon>
|
</local:DockItemControl.Icon>
|
||||||
</local:DockItemControl>
|
</local:DockItemControl>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
@@ -168,7 +168,7 @@
|
|||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
SourceKey="{x:Bind IconViewModel, Mode=OneWay}"
|
SourceKey="{x:Bind IconViewModel, Mode=OneWay}"
|
||||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested20}" />
|
SourceRequested="{x:Bind help:IconProvider.SourceRequested20}" />
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
|
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
|
||||||
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
|
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
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:local="using:Microsoft.CmdPal.UI"
|
||||||
xmlns:markdownImageProviders="using:Microsoft.CmdPal.UI.Helpers.MarkdownImageProviders"
|
xmlns:markdownImageProviders="using:Microsoft.CmdPal.UI.Helpers.MarkdownImageProviders"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
@@ -38,12 +40,17 @@
|
|||||||
<cmdpalUI:ContentTemplateSelector
|
<cmdpalUI:ContentTemplateSelector
|
||||||
x:Key="ContentTemplateSelector"
|
x:Key="ContentTemplateSelector"
|
||||||
FormTemplate="{StaticResource FormContentTemplate}"
|
FormTemplate="{StaticResource FormContentTemplate}"
|
||||||
|
ImageTemplate="{StaticResource ImageContentTemplate}"
|
||||||
MarkdownTemplate="{StaticResource MarkdownContentTemplate}"
|
MarkdownTemplate="{StaticResource MarkdownContentTemplate}"
|
||||||
|
PlainTextTemplate="{StaticResource PlainTextContentTemplate}"
|
||||||
TreeTemplate="{StaticResource TreeContentTemplate}" />
|
TreeTemplate="{StaticResource TreeContentTemplate}" />
|
||||||
|
|
||||||
<cmdpalUI:ContentTemplateSelector
|
<cmdpalUI:ContentTemplateSelector
|
||||||
x:Key="NestedContentTemplateSelector"
|
x:Key="NestedContentTemplateSelector"
|
||||||
FormTemplate="{StaticResource NestedFormContentTemplate}"
|
FormTemplate="{StaticResource NestedFormContentTemplate}"
|
||||||
|
ImageTemplate="{StaticResource ImageContentTemplate}"
|
||||||
MarkdownTemplate="{StaticResource NestedMarkdownContentTemplate}"
|
MarkdownTemplate="{StaticResource NestedMarkdownContentTemplate}"
|
||||||
|
PlainTextTemplate="{StaticResource PlainTextContentTemplate}"
|
||||||
TreeTemplate="{StaticResource TreeContentTemplate}" />
|
TreeTemplate="{StaticResource TreeContentTemplate}" />
|
||||||
|
|
||||||
<DataTemplate x:Key="FormContentTemplate" x:DataType="viewModels:ContentFormViewModel">
|
<DataTemplate x:Key="FormContentTemplate" x:DataType="viewModels:ContentFormViewModel">
|
||||||
@@ -62,6 +69,21 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
</DataTemplate>
|
</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">
|
<DataTemplate x:Key="NestedFormContentTemplate" x:DataType="viewModels:ContentFormViewModel">
|
||||||
<Grid>
|
<Grid>
|
||||||
<cmdPalControls:ContentFormControl ViewModel="{x:Bind}" />
|
<cmdPalControls:ContentFormControl ViewModel="{x:Bind}" />
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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="" />
|
||||||
|
</MenuFlyoutItem.Icon>
|
||||||
|
<MenuFlyoutItem.KeyboardAccelerators>
|
||||||
|
<KeyboardAccelerator Key="Number0" Modifiers="Control" />
|
||||||
|
</MenuFlyoutItem.KeyboardAccelerators>
|
||||||
|
</MenuFlyoutItem>
|
||||||
|
</MenuFlyout>
|
||||||
|
</TextBlock.ContextFlyout>
|
||||||
|
</TextBlock>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Border>
|
||||||
|
</UserControl>
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -365,7 +365,7 @@
|
|||||||
AutomationProperties.AccessibilityView="Raw"
|
AutomationProperties.AccessibilityView="Raw"
|
||||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
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,
|
Title and subtitle are intentionally in a nested Grid instead in the outer container,
|
||||||
@@ -476,7 +476,7 @@
|
|||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Foreground="{ThemeResource TextFillColorPrimary}"
|
Foreground="{ThemeResource TextFillColorPrimary}"
|
||||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested32}" />
|
SourceRequested="{x:Bind help:IconProvider.SourceRequested32}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
|
|
||||||
@@ -503,7 +503,7 @@
|
|||||||
FontSize="14"
|
FontSize="14"
|
||||||
Foreground="{ThemeResource TextFillColorPrimary}"
|
Foreground="{ThemeResource TextFillColorPrimary}"
|
||||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested64}" />
|
SourceRequested="{x:Bind help:IconProvider.SourceRequested64}" />
|
||||||
<TextBlock
|
<TextBlock
|
||||||
x:Name="TitleTextBlock"
|
x:Name="TitleTextBlock"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
@@ -548,7 +548,7 @@
|
|||||||
CornerRadius="4"
|
CornerRadius="4"
|
||||||
Foreground="{ThemeResource TextFillColorPrimary}"
|
Foreground="{ThemeResource TextFillColorPrimary}"
|
||||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested256}" />
|
SourceRequested="{x:Bind help:IconProvider.SourceRequested256}" />
|
||||||
</Viewbox>
|
</Viewbox>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
@@ -674,7 +674,7 @@
|
|||||||
Margin="8"
|
Margin="8"
|
||||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
SourceKey="{x:Bind ViewModel.EmptyContent.Icon, Mode=OneWay}"
|
SourceKey="{x:Bind ViewModel.EmptyContent.Icon, Mode=OneWay}"
|
||||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested64}" />
|
SourceRequested="{x:Bind help:IconProvider.SourceRequested64}" />
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Margin="0,4,0,0"
|
Margin="0,4,0,0"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ namespace Microsoft.CmdPal.UI.Helpers;
|
|||||||
|
|
||||||
internal sealed partial class IconLoaderService : IIconLoaderService
|
internal sealed partial class IconLoaderService : IIconLoaderService
|
||||||
{
|
{
|
||||||
|
public static readonly Size NoResize = Size.Empty;
|
||||||
|
|
||||||
private const DispatcherQueuePriority LoadingPriorityOnDispatcher = DispatcherQueuePriority.Low;
|
private const DispatcherQueuePriority LoadingPriorityOnDispatcher = DispatcherQueuePriority.Low;
|
||||||
private const int DefaultIconSize = 256;
|
private const int DefaultIconSize = 256;
|
||||||
private const int MaxWorkerCount = 4;
|
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)
|
private static IconSource? GetStringIconSource(string iconString, string? fontFamily, Size size)
|
||||||
{
|
{
|
||||||
var iconSize = (int)Math.Max(size.Width, size.Height);
|
var iconSize = (int)Math.Max(size.Width, size.Height);
|
||||||
if (iconSize == 0)
|
|
||||||
{
|
|
||||||
iconSize = DefaultIconSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
return IconPathConverter.IconSourceMUX(iconString, fontFamily, iconSize);
|
return IconPathConverter.IconSourceMUX(iconString, fontFamily, iconSize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ namespace Microsoft.CmdPal.UI.Helpers;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Common async event handler provides the cache lookup function for the <see cref="IconBox.SourceRequested"/> deferred event.
|
/// Common async event handler provides the cache lookup function for the <see cref="IconBox.SourceRequested"/> deferred event.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static partial class IconCacheProvider
|
public static partial class IconProvider
|
||||||
{
|
{
|
||||||
/*
|
/*
|
||||||
Memory Usage Considerations (raw estimates):
|
Memory Usage Considerations (raw estimates):
|
||||||
@@ -29,6 +29,7 @@ public static partial class IconCacheProvider
|
|||||||
private static IIconSourceProvider _provider32 = null!;
|
private static IIconSourceProvider _provider32 = null!;
|
||||||
private static IIconSourceProvider _provider64 = null!;
|
private static IIconSourceProvider _provider64 = null!;
|
||||||
private static IIconSourceProvider _provider256 = null!;
|
private static IIconSourceProvider _provider256 = null!;
|
||||||
|
private static IIconSourceProvider _providerUnbound = null!;
|
||||||
|
|
||||||
public static void Initialize(IServiceProvider serviceProvider)
|
public static void Initialize(IServiceProvider serviceProvider)
|
||||||
{
|
{
|
||||||
@@ -37,6 +38,7 @@ public static partial class IconCacheProvider
|
|||||||
_provider32 = serviceProvider.GetRequiredKeyedService<IIconSourceProvider>(WellKnownIconSize.Size32);
|
_provider32 = serviceProvider.GetRequiredKeyedService<IIconSourceProvider>(WellKnownIconSize.Size32);
|
||||||
_provider64 = serviceProvider.GetRequiredKeyedService<IIconSourceProvider>(WellKnownIconSize.Size64);
|
_provider64 = serviceProvider.GetRequiredKeyedService<IIconSourceProvider>(WellKnownIconSize.Size64);
|
||||||
_provider256 = serviceProvider.GetRequiredKeyedService<IIconSourceProvider>(WellKnownIconSize.Size256);
|
_provider256 = serviceProvider.GetRequiredKeyedService<IIconSourceProvider>(WellKnownIconSize.Size256);
|
||||||
|
_providerUnbound = serviceProvider.GetRequiredKeyedService<IIconSourceProvider>(WellKnownIconSize.Unbound);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async void SourceRequestedCore(IIconSourceProvider service, SourceRequestedEventArgs args)
|
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)
|
public static void SourceRequested256(IconBox sender, SourceRequestedEventArgs args)
|
||||||
=> SourceRequestedCore(_provider256, args);
|
=> SourceRequestedCore(_provider256, args);
|
||||||
|
|
||||||
|
public static void SourceRequestedOriginal(IconBox sender, SourceRequestedEventArgs args)
|
||||||
|
=> SourceRequestedCore(_providerUnbound, args);
|
||||||
#pragma warning restore IDE0060 // Remove unused parameter
|
#pragma warning restore IDE0060 // Remove unused parameter
|
||||||
}
|
}
|
||||||
@@ -36,6 +36,10 @@ internal static class IconServiceRegistration
|
|||||||
WellKnownIconSize.Size256,
|
WellKnownIconSize.Size256,
|
||||||
(_, _) => new CachedIconSourceProvider(loader, 256, 64));
|
(_, _) => new CachedIconSourceProvider(loader, 256, 64));
|
||||||
|
|
||||||
|
services.AddKeyedSingleton<IIconSourceProvider>(
|
||||||
|
WellKnownIconSize.Unbound,
|
||||||
|
(_, _) => new IconSourceProvider(loader, IconLoaderService.NoResize, isPriority: true));
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,15 +12,17 @@ internal sealed class IconSourceProvider : IIconSourceProvider
|
|||||||
{
|
{
|
||||||
private readonly IconLoaderService _loader;
|
private readonly IconLoaderService _loader;
|
||||||
private readonly Size _iconSize;
|
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;
|
_loader = loader;
|
||||||
_iconSize = iconSize;
|
_iconSize = iconSize;
|
||||||
|
_isPriority = isPriority;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IconSourceProvider(IconLoaderService loader, int iconSize)
|
public IconSourceProvider(IconLoaderService loader, int iconSize, bool isPriority = false)
|
||||||
: this(loader, new Size(iconSize, iconSize))
|
: this(loader, new Size(iconSize, iconSize), isPriority)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,7 +36,8 @@ internal sealed class IconSourceProvider : IIconSourceProvider
|
|||||||
icon.Data?.Unsafe,
|
icon.Data?.Unsafe,
|
||||||
_iconSize,
|
_iconSize,
|
||||||
scale,
|
scale,
|
||||||
tcs);
|
tcs,
|
||||||
|
_isPriority ? IconLoadPriority.High : IconLoadPriority.Low);
|
||||||
|
|
||||||
return tcs.Task;
|
return tcs.Task;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,4 +11,5 @@ internal enum WellKnownIconSize
|
|||||||
Size32 = 32,
|
Size32 = 32,
|
||||||
Size64 = 64,
|
Size64 = 64,
|
||||||
Size256 = 256,
|
Size256 = 256,
|
||||||
|
Unbound = -1,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,9 +84,12 @@
|
|||||||
<None Remove="Controls\CommandPalettePreview.xaml" />
|
<None Remove="Controls\CommandPalettePreview.xaml" />
|
||||||
<None Remove="Controls\DevRibbon.xaml" />
|
<None Remove="Controls\DevRibbon.xaml" />
|
||||||
<None Remove="Controls\FallbackRankerDialog.xaml" />
|
<None Remove="Controls\FallbackRankerDialog.xaml" />
|
||||||
|
<None Remove="Controls\ImageViewer.xaml" />
|
||||||
<None Remove="Controls\ScreenPreview.xaml" />
|
<None Remove="Controls\ScreenPreview.xaml" />
|
||||||
<None Remove="Controls\ScrollContainer.xaml" />
|
<None Remove="Controls\ScrollContainer.xaml" />
|
||||||
<None Remove="Controls\SearchBar.xaml" />
|
<None Remove="Controls\SearchBar.xaml" />
|
||||||
|
<None Remove="ExtViews\Controls\ImageContentViewer.xaml" />
|
||||||
|
<None Remove="ExtViews\Controls\PlainTextContentViewer.xaml" />
|
||||||
<None Remove="ListDetailPage.xaml" />
|
<None Remove="ListDetailPage.xaml" />
|
||||||
<None Remove="LoadingPage.xaml" />
|
<None Remove="LoadingPage.xaml" />
|
||||||
<None Remove="MainPage.xaml" />
|
<None Remove="MainPage.xaml" />
|
||||||
@@ -103,6 +106,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" />
|
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" />
|
||||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.Segmented" />
|
<PackageReference Include="CommunityToolkit.WinUI.Controls.Segmented" />
|
||||||
@@ -315,6 +319,11 @@
|
|||||||
<Generator>MSBuild:Compile</Generator>
|
<Generator>MSBuild:Compile</Generator>
|
||||||
</Page>
|
</Page>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Page Update="ExtViews\Controls\PlainTextContentViewer.xaml">
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
</Page>
|
||||||
|
</ItemGroup>
|
||||||
<!-- </AdaptiveCardsWorkaround> -->
|
<!-- </AdaptiveCardsWorkaround> -->
|
||||||
|
|
||||||
<!-- Build information -->
|
<!-- Build information -->
|
||||||
|
|||||||
@@ -92,7 +92,7 @@
|
|||||||
Height="16"
|
Height="16"
|
||||||
Margin="0,3,8,0"
|
Margin="0,3,8,0"
|
||||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
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}" />
|
<TextBlock Style="{StaticResource BodyTextBlockStyle}" Text="{x:Bind Name}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -301,7 +301,7 @@
|
|||||||
ui:VisualExtensions.NormalizedCenterPoint="0.5,0.5"
|
ui:VisualExtensions.NormalizedCenterPoint="0.5,0.5"
|
||||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
SourceKey="{x:Bind ViewModel.CurrentPage.Icon, Mode=OneWay}"
|
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}}">
|
Visibility="{x:Bind ViewModel.CurrentPage.IsRootPage, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}">
|
||||||
<animations:Implicit.ShowAnimations>
|
<animations:Implicit.ShowAnimations>
|
||||||
<animations:OpacityAnimation
|
<animations:OpacityAnimation
|
||||||
@@ -434,7 +434,7 @@
|
|||||||
HorizontalAlignment="Left"
|
HorizontalAlignment="Left"
|
||||||
AutomationProperties.AccessibilityView="Raw"
|
AutomationProperties.AccessibilityView="Raw"
|
||||||
SourceKey="{x:Bind ViewModel.Details.HeroImage, Mode=OneWay}"
|
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}" />
|
Visibility="{x:Bind HasHeroImage, Mode=OneWay}" />
|
||||||
|
|
||||||
<TextBlock
|
<TextBlock
|
||||||
|
|||||||
@@ -233,7 +233,7 @@
|
|||||||
Height="20"
|
Height="20"
|
||||||
AutomationProperties.AccessibilityView="Raw"
|
AutomationProperties.AccessibilityView="Raw"
|
||||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||||
SourceRequested="{x:Bind helpers:IconCacheProvider.SourceRequested20}" />
|
SourceRequested="{x:Bind helpers:IconProvider.SourceRequested20}" />
|
||||||
</cpControls:ContentIcon.Content>
|
</cpControls:ContentIcon.Content>
|
||||||
</cpControls:ContentIcon>
|
</cpControls:ContentIcon>
|
||||||
</controls:SettingsCard.HeaderIcon>
|
</controls:SettingsCard.HeaderIcon>
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
Height="20"
|
Height="20"
|
||||||
AutomationProperties.AccessibilityView="Raw"
|
AutomationProperties.AccessibilityView="Raw"
|
||||||
SourceKey="{x:Bind ViewModel.Icon, Mode=OneWay}"
|
SourceKey="{x:Bind ViewModel.Icon, Mode=OneWay}"
|
||||||
SourceRequested="{x:Bind helpers:IconCacheProvider.SourceRequested20}" />
|
SourceRequested="{x:Bind helpers:IconProvider.SourceRequested20}" />
|
||||||
</cpcontrols:ContentIcon.Content>
|
</cpcontrols:ContentIcon.Content>
|
||||||
</cpcontrols:ContentIcon>
|
</cpcontrols:ContentIcon>
|
||||||
</controls:SettingsCard.HeaderIcon>
|
</controls:SettingsCard.HeaderIcon>
|
||||||
@@ -95,7 +95,7 @@
|
|||||||
Height="20"
|
Height="20"
|
||||||
AutomationProperties.AccessibilityView="Raw"
|
AutomationProperties.AccessibilityView="Raw"
|
||||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||||
SourceRequested="{x:Bind helpers:IconCacheProvider.SourceRequested20}" />
|
SourceRequested="{x:Bind helpers:IconProvider.SourceRequested20}" />
|
||||||
</cpcontrols:ContentIcon.Content>
|
</cpcontrols:ContentIcon.Content>
|
||||||
</cpcontrols:ContentIcon>
|
</cpcontrols:ContentIcon>
|
||||||
</controls:SettingsExpander.HeaderIcon>
|
</controls:SettingsExpander.HeaderIcon>
|
||||||
@@ -167,7 +167,7 @@
|
|||||||
Height="20"
|
Height="20"
|
||||||
AutomationProperties.AccessibilityView="Raw"
|
AutomationProperties.AccessibilityView="Raw"
|
||||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||||
SourceRequested="{x:Bind helpers:IconCacheProvider.SourceRequested20}" />
|
SourceRequested="{x:Bind helpers:IconProvider.SourceRequested20}" />
|
||||||
</cpcontrols:ContentIcon.Content>
|
</cpcontrols:ContentIcon.Content>
|
||||||
</cpcontrols:ContentIcon>
|
</cpcontrols:ContentIcon>
|
||||||
</controls:SettingsExpander.HeaderIcon>
|
</controls:SettingsExpander.HeaderIcon>
|
||||||
|
|||||||
@@ -241,7 +241,7 @@
|
|||||||
Height="20"
|
Height="20"
|
||||||
AutomationProperties.AccessibilityView="Raw"
|
AutomationProperties.AccessibilityView="Raw"
|
||||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||||
SourceRequested="{x:Bind helpers:IconCacheProvider.SourceRequested20}" />
|
SourceRequested="{x:Bind helpers:IconProvider.SourceRequested20}" />
|
||||||
</controls:Case>
|
</controls:Case>
|
||||||
<controls:Case Value="False">
|
<controls:Case Value="False">
|
||||||
<Image
|
<Image
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<root>
|
<root>
|
||||||
<!--
|
<!--
|
||||||
Microsoft ResX Schema
|
Microsoft ResX Schema
|
||||||
|
|
||||||
Version 2.0
|
Version 2.0
|
||||||
|
|
||||||
The primary goals of this format is to allow a simple XML format
|
The primary goals of this format is to allow a simple XML format
|
||||||
that is mostly human readable. The generation and parsing of the
|
that is mostly human readable. The generation and parsing of the
|
||||||
various data types are done through the TypeConverter classes
|
various data types are done through the TypeConverter classes
|
||||||
associated with the data types.
|
associated with the data types.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
... ado.net/XML headers & schema ...
|
... ado.net/XML headers & schema ...
|
||||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||||
<resheader name="version">2.0</resheader>
|
<resheader name="version">2.0</resheader>
|
||||||
@@ -26,36 +26,36 @@
|
|||||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||||
<comment>This is a comment</comment>
|
<comment>This is a comment</comment>
|
||||||
</data>
|
</data>
|
||||||
|
|
||||||
There are any number of "resheader" rows that contain simple
|
There are any number of "resheader" rows that contain simple
|
||||||
name/value pairs.
|
name/value pairs.
|
||||||
|
|
||||||
Each data row contains a name, and value. The row also contains a
|
Each data row contains a name, and value. The row also contains a
|
||||||
type or mimetype. Type corresponds to a .NET class that support
|
type or mimetype. Type corresponds to a .NET class that support
|
||||||
text/value conversion through the TypeConverter architecture.
|
text/value conversion through the TypeConverter architecture.
|
||||||
Classes that don't support this are serialized and stored with the
|
Classes that don't support this are serialized and stored with the
|
||||||
mimetype set.
|
mimetype set.
|
||||||
|
|
||||||
The mimetype is used for serialized objects, and tells the
|
The mimetype is used for serialized objects, and tells the
|
||||||
ResXResourceReader how to depersist the object. This is currently not
|
ResXResourceReader how to depersist the object. This is currently not
|
||||||
extensible. For a given mimetype the value must be set accordingly:
|
extensible. For a given mimetype the value must be set accordingly:
|
||||||
|
|
||||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||||
that the ResXResourceWriter will generate, however the reader can
|
that the ResXResourceWriter will generate, however the reader can
|
||||||
read any of the formats listed below.
|
read any of the formats listed below.
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.binary.base64
|
mimetype: application/x-microsoft.net.object.binary.base64
|
||||||
value : The object must be serialized with
|
value : The object must be serialized with
|
||||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||||
: and then encoded with base64 encoding.
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.soap.base64
|
mimetype: application/x-microsoft.net.object.soap.base64
|
||||||
value : The object must be serialized with
|
value : The object must be serialized with
|
||||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||||
: and then encoded with base64 encoding.
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||||
value : The object must be serialized into a byte array
|
value : The object must be serialized into a byte array
|
||||||
: using a System.ComponentModel.TypeConverter
|
: using a System.ComponentModel.TypeConverter
|
||||||
: and then encoded with base64 encoding.
|
: and then encoded with base64 encoding.
|
||||||
-->
|
-->
|
||||||
@@ -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">
|
<data name="Dock_AddBand_NoCommandsAvailable.Text" xml:space="preserve">
|
||||||
<value>All available bands are already pinned.</value>
|
<value>All available bands are already pinned.</value>
|
||||||
</data>
|
</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>
|
<value>To pin commands, extensions or apps, use the Pin to Dock command in Command Palette.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Dock_Bands_Header.Text" xml:space="preserve">
|
<data name="Dock_Bands_Header.Text" xml:space="preserve">
|
||||||
<value>Bands</value>
|
<value>Bands</value>
|
||||||
</data>
|
</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>
|
<value>Add band to start</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Dock_AddBand_CenterTooltip.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
|
<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>
|
||||||
<data name="SettingsPage_NewInfoBadge.Text" xml:space="preserve">
|
<data name="SettingsPage_NewInfoBadge.Text" xml:space="preserve">
|
||||||
<value>NEW</value>
|
<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 & 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>
|
||||||
<data name="PinToDock_DialogTitle" xml:space="preserve">
|
<data name="PinToDock_DialogTitle" xml:space="preserve">
|
||||||
<value>Pin to Dock</value>
|
<value>Pin to Dock</value>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
#include "IconPathConverter.h"
|
#include "IconPathConverter.h"
|
||||||
#include "IconPathConverter.g.cpp"
|
#include "IconPathConverter.g.cpp"
|
||||||
|
|
||||||
#include "FontIconGlyphClassifier.h"
|
#include "FontIconGlyphClassifier.h"
|
||||||
|
|
||||||
#include <Shlobj.h>
|
#include <Shlobj.h>
|
||||||
#include <Shlobj_core.h>
|
#include <Shlobj_core.h>
|
||||||
@@ -115,10 +115,13 @@ namespace winrt::Microsoft::Terminal::UI::implementation
|
|||||||
{
|
{
|
||||||
typename ImageIconSource<TIconSource>::type iconSource;
|
typename ImageIconSource<TIconSource>::type iconSource;
|
||||||
winrt::Microsoft::UI::Xaml::Media::Imaging::SvgImageSource source{ iconUri };
|
winrt::Microsoft::UI::Xaml::Media::Imaging::SvgImageSource source{ iconUri };
|
||||||
source.RasterizePixelWidth(static_cast<double>(targetSize));
|
if (targetSize > 0)
|
||||||
// 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.RasterizePixelWidth(static_cast<double>(targetSize));
|
||||||
// source.RasterizePixelHeight(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);
|
iconSource.ImageSource(source);
|
||||||
return iconSource;
|
return iconSource;
|
||||||
}
|
}
|
||||||
@@ -126,10 +129,13 @@ namespace winrt::Microsoft::Terminal::UI::implementation
|
|||||||
{
|
{
|
||||||
typename ImageIconSource<TIconSource>::type iconSource;
|
typename ImageIconSource<TIconSource>::type iconSource;
|
||||||
winrt::Microsoft::UI::Xaml::Media::Imaging::BitmapImage bitmapImage;
|
winrt::Microsoft::UI::Xaml::Media::Imaging::BitmapImage bitmapImage;
|
||||||
bitmapImage.DecodePixelWidth(targetSize);
|
if (targetSize > 0)
|
||||||
// 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.DecodePixelWidth(targetSize);
|
||||||
// bitmapImage.DecodePixelHeight(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);
|
bitmapImage.UriSource(iconUri);
|
||||||
iconSource.ImageSource(bitmapImage);
|
iconSource.ImageSource(bitmapImage);
|
||||||
return iconSource;
|
return iconSource;
|
||||||
@@ -210,7 +216,7 @@ namespace winrt::Microsoft::Terminal::UI::implementation
|
|||||||
|
|
||||||
typename FontIconSource<TIconSource>::type icon;
|
typename FontIconSource<TIconSource>::type icon;
|
||||||
icon.FontFamily(winrt::Microsoft::UI::Xaml::Media::FontFamily{ family });
|
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);
|
icon.Glyph(glyph_kind == FontIconGlyphKind::Invalid ? L"\u25CC" : iconPath);
|
||||||
iconSource = icon;
|
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\6-preview\pwsh.exe, 0 (this doesn't exist for me)
|
||||||
// * C:\Program Files\PowerShell\7\pwsh.exe, 0
|
// * 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)
|
if (swBitmap == nullptr)
|
||||||
{
|
{
|
||||||
return nullptr;
|
return nullptr;
|
||||||
@@ -379,7 +385,8 @@ namespace winrt::Microsoft::Terminal::UI::implementation
|
|||||||
return imageIconSource;
|
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);
|
return IconMUX(iconPath, 24);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 760 KiB |
@@ -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")]
|
[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Sample code")]
|
||||||
internal sealed partial class SampleContentForm : FormContent
|
internal sealed partial class SampleContentForm : FormContent
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -66,6 +66,16 @@ public partial class SamplesListPage : ListPage
|
|||||||
Title = "Sample content page",
|
Title = "Sample content page",
|
||||||
Subtitle = "Display mixed forms, markdown, and other types of content",
|
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())
|
new ListItem(new SampleTreeContentPage())
|
||||||
{
|
{
|
||||||
Title = "Sample nested content",
|
Title = "Sample nested content",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -348,6 +348,26 @@ namespace Microsoft.CommandPalette.Extensions
|
|||||||
IContent RootContent { get; };
|
IContent RootContent { get; };
|
||||||
IContent[] GetChildren();
|
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)]
|
[contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)]
|
||||||
interface IContentPage requires IPage, INotifyItemsChanged {
|
interface IContentPage requires IPage, INotifyItemsChanged {
|
||||||
|
|||||||
Reference in New Issue
Block a user