mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-04 18:26:39 +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:
@@ -77,7 +77,7 @@ public partial class App : Application, IDisposable
|
||||
|
||||
Services = ConfigureServices(appInfoService);
|
||||
|
||||
IconCacheProvider.Initialize(Services);
|
||||
IconProvider.Initialize(Services);
|
||||
|
||||
this.InitializeComponent();
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
Margin="4,0,0,0"
|
||||
HorizontalAlignment="Left"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested20}" />
|
||||
SourceRequested="{x:Bind help:IconProvider.SourceRequested20}" />
|
||||
<TextBlock
|
||||
x:Name="TitleTextBlock"
|
||||
Grid.Column="1"
|
||||
@@ -90,7 +90,7 @@
|
||||
HorizontalAlignment="Left"
|
||||
Foreground="{ThemeResource SystemFillColorCriticalBrush}"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested20}" />
|
||||
SourceRequested="{x:Bind help:IconProvider.SourceRequested20}" />
|
||||
<TextBlock
|
||||
x:Name="TitleTextBlock"
|
||||
Grid.Column="1"
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
Height="16"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind helpers:IconCacheProvider.SourceRequested20}" />
|
||||
SourceRequested="{x:Bind helpers:IconProvider.SourceRequested20}" />
|
||||
</cpcontrols:ContentIcon.Content>
|
||||
</cpcontrols:ContentIcon>
|
||||
</controls:SettingsCard.HeaderIcon>
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
SourceKey="{x:Bind Icon}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested20}" />
|
||||
SourceRequested="{x:Bind help:IconProvider.SourceRequested20}" />
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
@@ -72,7 +72,7 @@
|
||||
Margin="0,0,8,0"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested20}"
|
||||
SourceRequested="{x:Bind help:IconProvider.SourceRequested20}"
|
||||
Visibility="Collapsed" />
|
||||
<TextBlock x:Name="SelectedFilterText" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
iconBox.SourceRequested += IconCacheProvider.SourceRequested20;
|
||||
iconBox.SourceRequested += IconProvider.SourceRequested20;
|
||||
iconBox.Visibility = HasIcon ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,10 @@ public partial class ContentTemplateSelector : DataTemplateSelector
|
||||
|
||||
public DataTemplate? TreeTemplate { get; set; }
|
||||
|
||||
public DataTemplate? PlainTextTemplate { get; set; }
|
||||
|
||||
public DataTemplate? ImageTemplate { get; set; }
|
||||
|
||||
protected override DataTemplate? SelectTemplateCore(object item)
|
||||
{
|
||||
return item is ContentViewModel element
|
||||
@@ -26,6 +30,8 @@ public partial class ContentTemplateSelector : DataTemplateSelector
|
||||
ContentFormViewModel => FormTemplate,
|
||||
ContentMarkdownViewModel => MarkdownTemplate,
|
||||
ContentTreeViewModel => TreeTemplate,
|
||||
ContentImageViewModel => ImageTemplate,
|
||||
ContentPlainTextViewModel => PlainTextTemplate,
|
||||
_ => null,
|
||||
}
|
||||
: null;
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested16}" />
|
||||
SourceRequested="{x:Bind help:IconProvider.SourceRequested16}" />
|
||||
</local:DockItemControl.Icon>
|
||||
</local:DockItemControl>
|
||||
</DataTemplate>
|
||||
@@ -168,7 +168,7 @@
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
SourceKey="{x:Bind IconViewModel, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested20}" />
|
||||
SourceRequested="{x:Bind help:IconProvider.SourceRequested20}" />
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:extViews="using:Microsoft.CmdPal.UI.ExtViews.Controls"
|
||||
xmlns:help="using:Microsoft.CmdPal.UI.Helpers"
|
||||
xmlns:local="using:Microsoft.CmdPal.UI"
|
||||
xmlns:markdownImageProviders="using:Microsoft.CmdPal.UI.Helpers.MarkdownImageProviders"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
@@ -38,12 +40,17 @@
|
||||
<cmdpalUI:ContentTemplateSelector
|
||||
x:Key="ContentTemplateSelector"
|
||||
FormTemplate="{StaticResource FormContentTemplate}"
|
||||
ImageTemplate="{StaticResource ImageContentTemplate}"
|
||||
MarkdownTemplate="{StaticResource MarkdownContentTemplate}"
|
||||
PlainTextTemplate="{StaticResource PlainTextContentTemplate}"
|
||||
TreeTemplate="{StaticResource TreeContentTemplate}" />
|
||||
|
||||
<cmdpalUI:ContentTemplateSelector
|
||||
x:Key="NestedContentTemplateSelector"
|
||||
FormTemplate="{StaticResource NestedFormContentTemplate}"
|
||||
ImageTemplate="{StaticResource ImageContentTemplate}"
|
||||
MarkdownTemplate="{StaticResource NestedMarkdownContentTemplate}"
|
||||
PlainTextTemplate="{StaticResource PlainTextContentTemplate}"
|
||||
TreeTemplate="{StaticResource TreeContentTemplate}" />
|
||||
|
||||
<DataTemplate x:Key="FormContentTemplate" x:DataType="viewModels:ContentFormViewModel">
|
||||
@@ -62,6 +69,21 @@
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="PlainTextContentTemplate" x:DataType="viewModels:ContentPlainTextViewModel">
|
||||
<Grid Margin="0,4,4,4" Padding="12,8,8,8">
|
||||
<extViews:PlainTextContentViewer
|
||||
Text="{x:Bind Text, Mode=OneWay}"
|
||||
UseMonospace="{x:Bind UseMonospace, Mode=OneWay}"
|
||||
WordWrapEnabled="{x:Bind WordWrapEnabled, Mode=OneWay}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="ImageContentTemplate" x:DataType="viewModels:ContentImageViewModel">
|
||||
<Grid Margin="0,4,4,4" Padding="12,8,8,8">
|
||||
<extViews:ImageContentViewer DataContext="{x:Bind}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="NestedFormContentTemplate" x:DataType="viewModels:ContentFormViewModel">
|
||||
<Grid>
|
||||
<cmdPalControls:ContentFormControl ViewModel="{x:Bind}" />
|
||||
|
||||
@@ -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"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested20}" />
|
||||
SourceRequested="{x:Bind help:IconProvider.SourceRequested20}" />
|
||||
|
||||
<!--
|
||||
Title and subtitle are intentionally in a nested Grid instead in the outer container,
|
||||
@@ -476,7 +476,7 @@
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorPrimary}"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested32}" />
|
||||
SourceRequested="{x:Bind help:IconProvider.SourceRequested32}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
|
||||
@@ -503,7 +503,7 @@
|
||||
FontSize="14"
|
||||
Foreground="{ThemeResource TextFillColorPrimary}"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested64}" />
|
||||
SourceRequested="{x:Bind help:IconProvider.SourceRequested64}" />
|
||||
<TextBlock
|
||||
x:Name="TitleTextBlock"
|
||||
Grid.Row="1"
|
||||
@@ -548,7 +548,7 @@
|
||||
CornerRadius="4"
|
||||
Foreground="{ThemeResource TextFillColorPrimary}"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested256}" />
|
||||
SourceRequested="{x:Bind help:IconProvider.SourceRequested256}" />
|
||||
</Viewbox>
|
||||
</Grid>
|
||||
|
||||
@@ -674,7 +674,7 @@
|
||||
Margin="8"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
SourceKey="{x:Bind ViewModel.EmptyContent.Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested64}" />
|
||||
SourceRequested="{x:Bind help:IconProvider.SourceRequested64}" />
|
||||
<TextBlock
|
||||
Margin="0,4,0,0"
|
||||
HorizontalAlignment="Center"
|
||||
|
||||
@@ -16,6 +16,8 @@ namespace Microsoft.CmdPal.UI.Helpers;
|
||||
|
||||
internal sealed partial class IconLoaderService : IIconLoaderService
|
||||
{
|
||||
public static readonly Size NoResize = Size.Empty;
|
||||
|
||||
private const DispatcherQueuePriority LoadingPriorityOnDispatcher = DispatcherQueuePriority.Low;
|
||||
private const int DefaultIconSize = 256;
|
||||
private const int MaxWorkerCount = 4;
|
||||
@@ -214,11 +216,6 @@ internal sealed partial class IconLoaderService : IIconLoaderService
|
||||
private static IconSource? GetStringIconSource(string iconString, string? fontFamily, Size size)
|
||||
{
|
||||
var iconSize = (int)Math.Max(size.Width, size.Height);
|
||||
if (iconSize == 0)
|
||||
{
|
||||
iconSize = DefaultIconSize;
|
||||
}
|
||||
|
||||
return IconPathConverter.IconSourceMUX(iconString, fontFamily, iconSize);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Microsoft.CmdPal.UI.Helpers;
|
||||
/// <summary>
|
||||
/// Common async event handler provides the cache lookup function for the <see cref="IconBox.SourceRequested"/> deferred event.
|
||||
/// </summary>
|
||||
public static partial class IconCacheProvider
|
||||
public static partial class IconProvider
|
||||
{
|
||||
/*
|
||||
Memory Usage Considerations (raw estimates):
|
||||
@@ -29,6 +29,7 @@ public static partial class IconCacheProvider
|
||||
private static IIconSourceProvider _provider32 = null!;
|
||||
private static IIconSourceProvider _provider64 = null!;
|
||||
private static IIconSourceProvider _provider256 = null!;
|
||||
private static IIconSourceProvider _providerUnbound = null!;
|
||||
|
||||
public static void Initialize(IServiceProvider serviceProvider)
|
||||
{
|
||||
@@ -37,6 +38,7 @@ public static partial class IconCacheProvider
|
||||
_provider32 = serviceProvider.GetRequiredKeyedService<IIconSourceProvider>(WellKnownIconSize.Size32);
|
||||
_provider64 = serviceProvider.GetRequiredKeyedService<IIconSourceProvider>(WellKnownIconSize.Size64);
|
||||
_provider256 = serviceProvider.GetRequiredKeyedService<IIconSourceProvider>(WellKnownIconSize.Size256);
|
||||
_providerUnbound = serviceProvider.GetRequiredKeyedService<IIconSourceProvider>(WellKnownIconSize.Unbound);
|
||||
}
|
||||
|
||||
private static async void SourceRequestedCore(IIconSourceProvider service, SourceRequestedEventArgs args)
|
||||
@@ -80,5 +82,8 @@ public static partial class IconCacheProvider
|
||||
|
||||
public static void SourceRequested256(IconBox sender, SourceRequestedEventArgs args)
|
||||
=> SourceRequestedCore(_provider256, args);
|
||||
|
||||
public static void SourceRequestedOriginal(IconBox sender, SourceRequestedEventArgs args)
|
||||
=> SourceRequestedCore(_providerUnbound, args);
|
||||
#pragma warning restore IDE0060 // Remove unused parameter
|
||||
}
|
||||
@@ -36,6 +36,10 @@ internal static class IconServiceRegistration
|
||||
WellKnownIconSize.Size256,
|
||||
(_, _) => new CachedIconSourceProvider(loader, 256, 64));
|
||||
|
||||
services.AddKeyedSingleton<IIconSourceProvider>(
|
||||
WellKnownIconSize.Unbound,
|
||||
(_, _) => new IconSourceProvider(loader, IconLoaderService.NoResize, isPriority: true));
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,15 +12,17 @@ internal sealed class IconSourceProvider : IIconSourceProvider
|
||||
{
|
||||
private readonly IconLoaderService _loader;
|
||||
private readonly Size _iconSize;
|
||||
private readonly bool _isPriority;
|
||||
|
||||
public IconSourceProvider(IconLoaderService loader, Size iconSize)
|
||||
public IconSourceProvider(IconLoaderService loader, Size iconSize, bool isPriority = false)
|
||||
{
|
||||
_loader = loader;
|
||||
_iconSize = iconSize;
|
||||
_isPriority = isPriority;
|
||||
}
|
||||
|
||||
public IconSourceProvider(IconLoaderService loader, int iconSize)
|
||||
: this(loader, new Size(iconSize, iconSize))
|
||||
public IconSourceProvider(IconLoaderService loader, int iconSize, bool isPriority = false)
|
||||
: this(loader, new Size(iconSize, iconSize), isPriority)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -34,7 +36,8 @@ internal sealed class IconSourceProvider : IIconSourceProvider
|
||||
icon.Data?.Unsafe,
|
||||
_iconSize,
|
||||
scale,
|
||||
tcs);
|
||||
tcs,
|
||||
_isPriority ? IconLoadPriority.High : IconLoadPriority.Low);
|
||||
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
@@ -11,4 +11,5 @@ internal enum WellKnownIconSize
|
||||
Size32 = 32,
|
||||
Size64 = 64,
|
||||
Size256 = 256,
|
||||
Unbound = -1,
|
||||
}
|
||||
|
||||
@@ -84,9 +84,12 @@
|
||||
<None Remove="Controls\CommandPalettePreview.xaml" />
|
||||
<None Remove="Controls\DevRibbon.xaml" />
|
||||
<None Remove="Controls\FallbackRankerDialog.xaml" />
|
||||
<None Remove="Controls\ImageViewer.xaml" />
|
||||
<None Remove="Controls\ScreenPreview.xaml" />
|
||||
<None Remove="Controls\ScrollContainer.xaml" />
|
||||
<None Remove="Controls\SearchBar.xaml" />
|
||||
<None Remove="ExtViews\Controls\ImageContentViewer.xaml" />
|
||||
<None Remove="ExtViews\Controls\PlainTextContentViewer.xaml" />
|
||||
<None Remove="ListDetailPage.xaml" />
|
||||
<None Remove="LoadingPage.xaml" />
|
||||
<None Remove="MainPage.xaml" />
|
||||
@@ -103,6 +106,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.Segmented" />
|
||||
@@ -315,6 +319,11 @@
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="ExtViews\Controls\PlainTextContentViewer.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<!-- </AdaptiveCardsWorkaround> -->
|
||||
|
||||
<!-- Build information -->
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
Height="16"
|
||||
Margin="0,3,8,0"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested20}" />
|
||||
SourceRequested="{x:Bind help:IconProvider.SourceRequested20}" />
|
||||
<TextBlock Style="{StaticResource BodyTextBlockStyle}" Text="{x:Bind Name}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
@@ -301,7 +301,7 @@
|
||||
ui:VisualExtensions.NormalizedCenterPoint="0.5,0.5"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
SourceKey="{x:Bind ViewModel.CurrentPage.Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested20}"
|
||||
SourceRequested="{x:Bind help:IconProvider.SourceRequested20}"
|
||||
Visibility="{x:Bind ViewModel.CurrentPage.IsRootPage, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}">
|
||||
<animations:Implicit.ShowAnimations>
|
||||
<animations:OpacityAnimation
|
||||
@@ -434,7 +434,7 @@
|
||||
HorizontalAlignment="Left"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
SourceKey="{x:Bind ViewModel.Details.HeroImage, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested64}"
|
||||
SourceRequested="{x:Bind help:IconProvider.SourceRequested64}"
|
||||
Visibility="{x:Bind HasHeroImage, Mode=OneWay}" />
|
||||
|
||||
<TextBlock
|
||||
|
||||
@@ -233,7 +233,7 @@
|
||||
Height="20"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind helpers:IconCacheProvider.SourceRequested20}" />
|
||||
SourceRequested="{x:Bind helpers:IconProvider.SourceRequested20}" />
|
||||
</cpControls:ContentIcon.Content>
|
||||
</cpControls:ContentIcon>
|
||||
</controls:SettingsCard.HeaderIcon>
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
Height="20"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
SourceKey="{x:Bind ViewModel.Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind helpers:IconCacheProvider.SourceRequested20}" />
|
||||
SourceRequested="{x:Bind helpers:IconProvider.SourceRequested20}" />
|
||||
</cpcontrols:ContentIcon.Content>
|
||||
</cpcontrols:ContentIcon>
|
||||
</controls:SettingsCard.HeaderIcon>
|
||||
@@ -95,7 +95,7 @@
|
||||
Height="20"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind helpers:IconCacheProvider.SourceRequested20}" />
|
||||
SourceRequested="{x:Bind helpers:IconProvider.SourceRequested20}" />
|
||||
</cpcontrols:ContentIcon.Content>
|
||||
</cpcontrols:ContentIcon>
|
||||
</controls:SettingsExpander.HeaderIcon>
|
||||
@@ -167,7 +167,7 @@
|
||||
Height="20"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind helpers:IconCacheProvider.SourceRequested20}" />
|
||||
SourceRequested="{x:Bind helpers:IconProvider.SourceRequested20}" />
|
||||
</cpcontrols:ContentIcon.Content>
|
||||
</cpcontrols:ContentIcon>
|
||||
</controls:SettingsExpander.HeaderIcon>
|
||||
|
||||
@@ -241,7 +241,7 @@
|
||||
Height="20"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind helpers:IconCacheProvider.SourceRequested20}" />
|
||||
SourceRequested="{x:Bind helpers:IconProvider.SourceRequested20}" />
|
||||
</controls:Case>
|
||||
<controls:Case Value="False">
|
||||
<Image
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
|
||||
Example:
|
||||
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</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>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
|
||||
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
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
|
||||
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
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
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
|
||||
: 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">
|
||||
<value>All available bands are already pinned.</value>
|
||||
</data>
|
||||
<data name="Dock_Pin_Instruction.Text" xml:space="preserve">
|
||||
<data name="Dock_Pin_Instruction.Text" xml:space="preserve">
|
||||
<value>To pin commands, extensions or apps, use the Pin to Dock command in Command Palette.</value>
|
||||
</data>
|
||||
<data name="Dock_Bands_Header.Text" xml:space="preserve">
|
||||
<data name="Dock_Bands_Header.Text" xml:space="preserve">
|
||||
<value>Bands</value>
|
||||
</data>
|
||||
<data name="Dock_AddBand_StartTooltip.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
|
||||
</data>
|
||||
<data name="Dock_AddBand_StartTooltip.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>Add band to start</value>
|
||||
</data>
|
||||
<data name="Dock_AddBand_CenterTooltip.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
|
||||
@@ -987,7 +987,79 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
</data>
|
||||
<data name="SettingsPage_NewInfoBadge.Text" xml:space="preserve">
|
||||
<value>NEW</value>
|
||||
<comment>Must be all caps</comment>
|
||||
<comment>Must be all caps</comment>
|
||||
</data>
|
||||
<data name="ImageContentViewer_ZoomAndScroll.Text" xml:space="preserve">
|
||||
<value>Zoom & Scroll</value>
|
||||
</data>
|
||||
<data name="ImageContentViewer_OpenZoomViewer.Text" xml:space="preserve">
|
||||
<value>Open image viewer</value>
|
||||
</data>
|
||||
<data name="ImageContentViewer_CopyImage.Text" xml:space="preserve">
|
||||
<value>Copy image</value>
|
||||
</data>
|
||||
<data name="ImageViewer_ZoomIn.Content" xml:space="preserve">
|
||||
<value>Zoom in</value>
|
||||
</data>
|
||||
<data name="ImageViewer_ZoomOut.Content" xml:space="preserve">
|
||||
<value>Zoom out</value>
|
||||
</data>
|
||||
<data name="ImageViewer_ZoomToFit.Content" xml:space="preserve">
|
||||
<value>Zoom to fit</value>
|
||||
</data>
|
||||
<data name="PlainTextContentViewer_Copy.Text" xml:space="preserve">
|
||||
<value>Copy</value>
|
||||
</data>
|
||||
<data name="PlainTextContentViewer_SelectAll.Text" xml:space="preserve">
|
||||
<value>Select all</value>
|
||||
</data>
|
||||
<data name="PlainTextContentViewer_WordWrap.Text" xml:space="preserve">
|
||||
<value>Word wrap</value>
|
||||
</data>
|
||||
<data name="PlainTextContentViewer_MonospaceFont.Text" xml:space="preserve">
|
||||
<value>Monospace font</value>
|
||||
</data>
|
||||
<data name="PlainTextContentViewer_ZoomIn.Text" xml:space="preserve">
|
||||
<value>Zoom in</value>
|
||||
</data>
|
||||
<data name="PlainTextContentViewer_ZoomOut.Text" xml:space="preserve">
|
||||
<value>Zoom out</value>
|
||||
</data>
|
||||
<data name="PlainTextContentViewer_ResetZoom.Text" xml:space="preserve">
|
||||
<value>Reset zoom</value>
|
||||
</data>
|
||||
<data name="ImageContentViewer_Toast_CopiedImage" xml:space="preserve">
|
||||
<value>Copied image to clipboard</value>
|
||||
</data>
|
||||
<data name="ImageContentViewer_Toast_CopiedLink" xml:space="preserve">
|
||||
<value>Copied link to clipboard</value>
|
||||
</data>
|
||||
<data name="ImageViewer_ZoomInButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Zoom in</value>
|
||||
</data>
|
||||
<data name="ImageViewer_ZoomInButton.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>Zoom in (Ctrl+Plus key)</value>
|
||||
</data>
|
||||
<data name="ImageViewer_ZoomOutButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Zoom out</value>
|
||||
</data>
|
||||
<data name="ImageViewer_ZoomOutButton.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>Zoom out (Ctrl+Minus key)</value>
|
||||
</data>
|
||||
<data name="ImageViewer_ZoomToFitButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Zoom to fit</value>
|
||||
</data>
|
||||
<data name="ImageViewer_ZoomToFitButton.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>Zoom to fit (Ctrl+0)</value>
|
||||
</data>
|
||||
<data name="ImageContentViewer_OpenZoomOverlayButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Open image viewer</value>
|
||||
</data>
|
||||
<data name="ImageContentViewer_OpenZoomOverlayButton.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>Open image viewer</value>
|
||||
</data>
|
||||
<data name="ImageContentViewer_CopyImageUri.Text" xml:space="preserve">
|
||||
<value>Copy image link</value>
|
||||
</data>
|
||||
<data name="PinToDock_DialogTitle" xml:space="preserve">
|
||||
<value>Pin to Dock</value>
|
||||
|
||||
Reference in New Issue
Block a user