Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39130f41e6 |
@@ -0,0 +1,163 @@
|
||||
<!-- Pull pages off a copier. Chrome follows the MMD design system locked on 2026-08-28:
|
||||
a front-window title bar (white to silver, 3px red top edge, ink text), 4px corners,
|
||||
flat buttons, one red fill for the single action the person came to do. Every colour and
|
||||
radius is a theme key from Themes/MMD.xaml - nothing is picked here. -->
|
||||
<Window x:Class="MmdPdf.Features.Scan.ScanDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="Scan a document"
|
||||
Width="520" SizeToContent="Height"
|
||||
WindowStyle="None" ResizeMode="NoResize" ShowInTaskbar="False"
|
||||
Background="Transparent"
|
||||
TextOptions.TextFormattingMode="Display"
|
||||
TextOptions.TextRenderingMode="ClearType"
|
||||
UseLayoutRounding="True" SnapsToDevicePixels="True"
|
||||
WindowStartupLocation="CenterOwner">
|
||||
|
||||
<Border Background="{DynamicResource BgFlyout}"
|
||||
BorderBrush="{DynamicResource AppBorderBrush}" BorderThickness="1"
|
||||
CornerRadius="{DynamicResource ControlCornerRadius}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Title bar: the front-window treatment from the design system -->
|
||||
<Border Grid.Row="0" Height="34"
|
||||
Background="{DynamicResource WindowFrontTitleBrush}"
|
||||
BorderBrush="{DynamicResource PrimaryBrush}" BorderThickness="0,3,0,0"
|
||||
MouseLeftButtonDown="TitleBar_MouseLeftButtonDown">
|
||||
<Grid Margin="12,0,8,0">
|
||||
<TextBlock x:Name="TitleText" Text="Scan a document" VerticalAlignment="Center"
|
||||
FontFamily="Segoe UI" FontSize="12.5" FontWeight="Bold"
|
||||
Foreground="{DynamicResource TextBrush}"/>
|
||||
<Button x:Name="CloseChrome" Click="Cancel_Click" HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center" Width="24" Height="24"
|
||||
Background="Transparent" BorderThickness="0" Cursor="Hand"
|
||||
Foreground="{DynamicResource TextBrush}" Content="✕"
|
||||
FontFamily="Segoe UI" FontSize="11"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Body -->
|
||||
<Grid Grid.Row="1" Margin="16,14,16,6">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<StackPanel Grid.Column="0">
|
||||
<Grid Margin="0,0,0,8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="86"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="COPIER" VerticalAlignment="Center" FontFamily="Consolas" FontSize="9.5"
|
||||
Foreground="{DynamicResource DimTextBrush}"/>
|
||||
<ComboBox x:Name="CopierBox" Grid.Column="1" Height="26" FontSize="11.5"/>
|
||||
</Grid>
|
||||
<Grid Margin="0,0,0,8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="86"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="FEED" VerticalAlignment="Center" FontFamily="Consolas" FontSize="9.5"
|
||||
Foreground="{DynamicResource DimTextBrush}"/>
|
||||
<ComboBox x:Name="FeedBox" Grid.Column="1" Height="26" FontSize="11.5"/>
|
||||
</Grid>
|
||||
<Grid Margin="0,0,0,8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="86"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="COLOUR" VerticalAlignment="Center" FontFamily="Consolas" FontSize="9.5"
|
||||
Foreground="{DynamicResource DimTextBrush}"/>
|
||||
<ComboBox x:Name="ColourBox" Grid.Column="1" Height="26" FontSize="11.5"/>
|
||||
</Grid>
|
||||
<Grid Margin="0,0,0,8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="86"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="DETAIL" VerticalAlignment="Center" FontFamily="Consolas" FontSize="9.5"
|
||||
Foreground="{DynamicResource DimTextBrush}"/>
|
||||
<ComboBox x:Name="DetailBox" Grid.Column="1" Height="26" FontSize="11.5"/>
|
||||
</Grid>
|
||||
<Grid Margin="0,0,0,10">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="86"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="PAPER" VerticalAlignment="Center" FontFamily="Consolas" FontSize="9.5"
|
||||
Foreground="{DynamicResource DimTextBrush}"/>
|
||||
<ComboBox x:Name="PaperBox" Grid.Column="1" Height="26" FontSize="11.5"/>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="86"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="AFTERWARDS" VerticalAlignment="Top" Margin="0,2,0,0"
|
||||
FontFamily="Consolas" FontSize="9.5"
|
||||
Foreground="{DynamicResource DimTextBrush}"/>
|
||||
<StackPanel Grid.Column="1">
|
||||
<CheckBox x:Name="SearchableBox" IsChecked="True" FontSize="11.5">
|
||||
<TextBlock Text="Make the words searchable, in French and English"
|
||||
TextWrapping="Wrap" MaxWidth="180"/>
|
||||
</CheckBox>
|
||||
<CheckBox x:Name="StraightenBox" Margin="0,6,0,0" FontSize="11.5">
|
||||
<TextBlock Text="Straighten crooked pages" TextWrapping="Wrap" MaxWidth="180"/>
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Sheet preview: what one page will look like -->
|
||||
<Border Grid.Column="1" Width="132" Margin="16,0,0,0"
|
||||
Background="{DynamicResource BgCanvas}"
|
||||
BorderBrush="{DynamicResource InputBorderBrush}" BorderThickness="1"
|
||||
CornerRadius="{DynamicResource ControlCornerRadius}">
|
||||
<Border Width="96" Height="132" Margin="10" VerticalAlignment="Center"
|
||||
Background="White" BorderBrush="{DynamicResource InputBorderBrush}" BorderThickness="1">
|
||||
<StackPanel Margin="9,9,8,0">
|
||||
<Rectangle Height="3" Width="56" HorizontalAlignment="Left" Margin="0,0,0,5"
|
||||
Fill="{DynamicResource DimTextBrush}"/>
|
||||
<Rectangle Height="2" Margin="0,0,0,4" Fill="{DynamicResource TreeLineBrush}"/>
|
||||
<Rectangle Height="2" Width="66" HorizontalAlignment="Left" Margin="0,0,0,4"
|
||||
Fill="{DynamicResource TreeLineBrush}"/>
|
||||
<Rectangle Height="2" Width="44" HorizontalAlignment="Left" Margin="0,0,0,4"
|
||||
Fill="{DynamicResource TreeLineBrush}"/>
|
||||
<Rectangle Height="2" Margin="0,0,0,4" Fill="{DynamicResource TreeLineBrush}"/>
|
||||
<Rectangle Height="2" Width="60" HorizontalAlignment="Left" Fill="{DynamicResource TreeLineBrush}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- Footer -->
|
||||
<Border Grid.Row="2" Background="{DynamicResource SurfaceBrush}"
|
||||
BorderBrush="{DynamicResource PaneBorderBrush}" BorderThickness="0,1,0,0"
|
||||
CornerRadius="0,0,4,4" Padding="16,10">
|
||||
<Grid>
|
||||
<TextBlock x:Name="FootNote" VerticalAlignment="Center" FontSize="10.5"
|
||||
Foreground="{DynamicResource DimTextBrush}"
|
||||
Text="Pages arrive as one document you can edit"/>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<Button x:Name="CancelBtn" Content="Cancel" Click="Cancel_Click"
|
||||
Height="28" Padding="13,0" Margin="0,0,8,0" FontSize="11.5" FontWeight="SemiBold"/>
|
||||
<Button x:Name="ScanBtn" Content="Scan" Click="Scan_Click"
|
||||
Height="28" Padding="13,0" FontSize="11.5" FontWeight="SemiBold"
|
||||
Background="{DynamicResource PrimaryBrush}"
|
||||
Foreground="{DynamicResource OnPrimaryBrush}"
|
||||
BorderBrush="{DynamicResource PrimaryBrush}" BorderThickness="1"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Window>
|
||||
@@ -0,0 +1,186 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using MmdPdf.Services;
|
||||
|
||||
namespace MmdPdf.Features.Scan
|
||||
{
|
||||
/// <summary>
|
||||
/// Asks the copier for pages. The person picks the machine, the feed, how much ink and how
|
||||
/// much detail; everything else is the copier's own default. The pages come back as image
|
||||
/// files which MainWindow turns into one document.
|
||||
///
|
||||
/// A run cannot be interrupted mid-sheet - that is the copier's hardware, not a decision made
|
||||
/// here - so Cancel stops after the sheet in progress and keeps the pages already read.
|
||||
/// </summary>
|
||||
public partial class ScanDialog : Window
|
||||
{
|
||||
private readonly List<ScannerInfo> _scanners;
|
||||
private CancellationTokenSource? _cancel;
|
||||
private bool _running;
|
||||
|
||||
/// <summary>Image files, one per page, in the order the copier read them.</summary>
|
||||
internal string[] Pages { get; private set; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>The person asked for the words to be made searchable afterwards.</summary>
|
||||
internal bool MakeSearchable { get; private set; }
|
||||
|
||||
/// <summary>The person asked for crooked pages to be straightened afterwards.</summary>
|
||||
internal bool Straighten { get; private set; }
|
||||
|
||||
public ScanDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_scanners = ScanService.IsAvailable ? ScanService.ListScanners() : new List<ScannerInfo>();
|
||||
|
||||
foreach (var s in _scanners) CopierBox.Items.Add(s.Name);
|
||||
if (_scanners.Count > 0)
|
||||
{
|
||||
CopierBox.SelectedIndex = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
CopierBox.Items.Add(ScanService.IsAvailable
|
||||
? "No copier found on this machine"
|
||||
: "This machine has no imaging service");
|
||||
CopierBox.SelectedIndex = 0;
|
||||
CopierBox.IsEnabled = false;
|
||||
ScanBtn.IsEnabled = false;
|
||||
FootNote.Text = "Switch the copier on, or connect it, then open this again";
|
||||
}
|
||||
|
||||
FeedBox.Items.Add("Feeder, one side");
|
||||
FeedBox.Items.Add("Feeder, both sides");
|
||||
FeedBox.Items.Add("Glass, one sheet");
|
||||
FeedBox.SelectedIndex = 0;
|
||||
|
||||
ColourBox.Items.Add("Black and white");
|
||||
ColourBox.Items.Add("Shades of grey");
|
||||
ColourBox.Items.Add("Colour");
|
||||
ColourBox.SelectedIndex = 0;
|
||||
|
||||
DetailBox.Items.Add("200 dpi, quick");
|
||||
DetailBox.Items.Add("300 dpi, good for text");
|
||||
DetailBox.Items.Add("600 dpi, fine detail");
|
||||
DetailBox.SelectedIndex = 1;
|
||||
|
||||
foreach (var p in ScanService.PaperSizes) PaperBox.Items.Add(p);
|
||||
PaperBox.SelectedIndex = 0;
|
||||
}
|
||||
|
||||
private void TitleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (e.ButtonState == MouseButtonState.Pressed) DragMove();
|
||||
}
|
||||
|
||||
private void Cancel_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_running)
|
||||
{
|
||||
_cancel?.Cancel();
|
||||
FootNote.Text = "Stopping after this sheet…";
|
||||
return;
|
||||
}
|
||||
DialogResult = false;
|
||||
Close();
|
||||
}
|
||||
|
||||
private async void Scan_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_running || _scanners.Count == 0) return;
|
||||
|
||||
var device = _scanners[Math.Max(0, CopierBox.SelectedIndex)];
|
||||
var feed = FeedBox.SelectedIndex switch
|
||||
{
|
||||
1 => ScanFeed.FeederBothSides,
|
||||
2 => ScanFeed.Flatbed,
|
||||
_ => ScanFeed.FeederOneSide,
|
||||
};
|
||||
var colour = ColourBox.SelectedIndex switch
|
||||
{
|
||||
1 => ScanColour.Greyscale,
|
||||
2 => ScanColour.Colour,
|
||||
_ => ScanColour.BlackAndWhite,
|
||||
};
|
||||
int dpi = DetailBox.SelectedIndex switch
|
||||
{
|
||||
0 => 200,
|
||||
2 => 600,
|
||||
_ => 300,
|
||||
};
|
||||
string paper = PaperBox.SelectedItem as string ?? "A4 portrait";
|
||||
|
||||
MakeSearchable = SearchableBox.IsChecked == true;
|
||||
Straighten = StraightenBox.IsChecked == true;
|
||||
|
||||
_running = true;
|
||||
_cancel = new CancellationTokenSource();
|
||||
ScanBtn.IsEnabled = false;
|
||||
CancelBtn.Content = "Stop";
|
||||
SetInputsEnabled(false);
|
||||
FootNote.Text = feed == ScanFeed.Flatbed ? "Reading the sheet on the glass…" : "Reading…";
|
||||
|
||||
var progress = new Progress<int>(n =>
|
||||
FootNote.Text = n == 1 ? "1 page read" : n.ToString(CultureInfo.InvariantCulture) + " pages read");
|
||||
|
||||
try
|
||||
{
|
||||
var token = _cancel.Token;
|
||||
var reporter = (IProgress<int>)progress;
|
||||
string[] pages = await Task.Run(() =>
|
||||
ScanService.Acquire(device.DeviceId, feed, colour, dpi, paper,
|
||||
maxPages: feed == ScanFeed.Flatbed ? 1 : 200,
|
||||
cancel: token,
|
||||
onPage: reporter.Report).ToArray(), token);
|
||||
|
||||
Pages = pages;
|
||||
if (pages.Length == 0)
|
||||
{
|
||||
FootNote.Text = "The copier returned no pages";
|
||||
ResetAfterRun();
|
||||
return;
|
||||
}
|
||||
|
||||
DialogResult = true;
|
||||
Close();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
FootNote.Text = "Stopped";
|
||||
ResetAfterRun();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
FootNote.Text = "The copier could not be used: " + ex.Message;
|
||||
ResetAfterRun();
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetAfterRun()
|
||||
{
|
||||
_running = false;
|
||||
_cancel?.Dispose();
|
||||
_cancel = null;
|
||||
ScanBtn.IsEnabled = _scanners.Count > 0;
|
||||
CancelBtn.Content = "Cancel";
|
||||
SetInputsEnabled(true);
|
||||
}
|
||||
|
||||
private void SetInputsEnabled(bool on)
|
||||
{
|
||||
CopierBox.IsEnabled = on && _scanners.Count > 0;
|
||||
FeedBox.IsEnabled = on;
|
||||
ColourBox.IsEnabled = on;
|
||||
DetailBox.IsEnabled = on;
|
||||
PaperBox.IsEnabled = on;
|
||||
SearchableBox.IsEnabled = on;
|
||||
StraightenBox.IsEnabled = on;
|
||||
}
|
||||
}
|
||||
}
|
||||
+58
-6
@@ -112,12 +112,12 @@
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Padding" Value="10,6"/>
|
||||
<Setter Property="Padding" Value="8,4"/>
|
||||
<Setter Property="FontFamily" Value="Segoe MDL2 Assets"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Width" Value="36"/>
|
||||
<Setter Property="Height" Value="32"/>
|
||||
<Setter Property="Width" Value="30"/>
|
||||
<Setter Property="Height" Value="26"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
@@ -231,6 +231,49 @@
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- Solid tool button on the band: flat, 1px border, 4px corner. The one place a
|
||||
white fill appears on the red ground, so the tool it carries reads as the
|
||||
action always within reach. -->
|
||||
<Style x:Key="BandButton" TargetType="Button">
|
||||
<Setter Property="Background" Value="#FFFFFF"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||
<Setter Property="BorderBrush" Value="#FFFFFF"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="Padding" Value="10,0"/>
|
||||
<Setter Property="Height" Value="24"/>
|
||||
<Setter Property="Margin" Value="0,0,10,0"/>
|
||||
<Setter Property="FontFamily" Value="Segoe UI"/>
|
||||
<Setter Property="FontSize" Value="10.5"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{DynamicResource ControlCornerRadius}"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#F1F1EC"/>
|
||||
<Setter Property="BorderBrush" Value="#F1F1EC"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter Property="Background" Value="#E4E3DC"/>
|
||||
<Setter Property="BorderBrush" Value="#E4E3DC"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Opacity" Value="0.55"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="ChromeButton" TargetType="Button">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource CaptionGlyphBrush}"/>
|
||||
@@ -1179,10 +1222,10 @@
|
||||
<!-- Soft drop shadow: a blurred, offset copy behind the crisp wordmark -->
|
||||
<StackPanel Orientation="Horizontal" Margin="1,1,0,0" Opacity="{DynamicResource WordmarkShadowOpacity}">
|
||||
<StackPanel.Effect><BlurEffect Radius="2"/></StackPanel.Effect>
|
||||
<Image Source="Resources/mmd-lockup-red.png" Height="19" Stretch="Uniform" HorizontalAlignment="Left" VerticalAlignment="Center" RenderOptions.BitmapScalingMode="HighQuality" SnapsToDevicePixels="True"/>
|
||||
<Image Source="Resources/mmd-lockup-white.png" Height="18" Stretch="Uniform" HorizontalAlignment="Left" VerticalAlignment="Center" RenderOptions.BitmapScalingMode="HighQuality" SnapsToDevicePixels="True"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Image Source="Resources/mmd-lockup-red.png" Height="19" Stretch="Uniform" HorizontalAlignment="Left" VerticalAlignment="Center" RenderOptions.BitmapScalingMode="HighQuality" SnapsToDevicePixels="True"/>
|
||||
<Image Source="Resources/mmd-lockup-white.png" Height="18" Stretch="Uniform" HorizontalAlignment="Left" VerticalAlignment="Center" RenderOptions.BitmapScalingMode="HighQuality" SnapsToDevicePixels="True"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
@@ -1192,6 +1235,15 @@
|
||||
Margin="16,0,12,0" TextTrimming="CharacterEllipsis"
|
||||
Foreground="{DynamicResource FilenameBrush}" FontFamily="Consolas" FontSize="11"/>
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" HorizontalAlignment="Right" Margin="{DynamicResource CaptionButtonsMargin}">
|
||||
<Button x:Name="BandScanBtn" Click="Scan_Click" Style="{StaticResource BandButton}"
|
||||
ToolTip="Pull pages off a copier"
|
||||
WindowChrome.IsHitTestVisibleInChrome="True">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="12"
|
||||
VerticalAlignment="Center" Margin="0,0,6,0"/>
|
||||
<TextBlock Text="Scan" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Click="MinimizeBtn_Click" Style="{StaticResource ChromeButton}"
|
||||
Foreground="{DynamicResource CaptionGlyphBrush}"
|
||||
WindowChrome.IsHitTestVisibleInChrome="True">
|
||||
@@ -1237,7 +1289,7 @@
|
||||
<Border IsHitTestVisible="False" Opacity="{DynamicResource GrainOpacity}" Background="{StaticResource GrainBrushShared}"/>
|
||||
<!-- Background=Transparent is load-bearing: without it the grid's empty stretches have no
|
||||
hit surface and the right-click appearance picker only opens over the buttons. -->
|
||||
<Grid x:Name="ToolbarGrid" SizeChanged="ToolbarGrid_SizeChanged" Margin="4,2" Background="Transparent">
|
||||
<Grid x:Name="ToolbarGrid" SizeChanged="ToolbarGrid_SizeChanged" Margin="4,1" Background="Transparent">
|
||||
<Grid.ContextMenu>
|
||||
<!-- Items are built in code (BuildToolbarMenu, SettingsPanel.cs) so captions re-Loc on
|
||||
language switch; attached here so WPF supplies the inheritance context that lets
|
||||
|
||||
@@ -532,6 +532,22 @@ namespace MmdPdf
|
||||
// squared chrome on every move (and once now that the handle exists).
|
||||
LocationChanged += OnWindowLocationChanged;
|
||||
UpdateWindowChrome();
|
||||
FitToSmallScreen();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plant machines run at 800 by 600. The default 1000 by 700 window does not fit one, and
|
||||
/// a window taller than the screen puts its own title bar - and therefore Scan, the window
|
||||
/// buttons and the tabs - above the top edge where nobody can reach them. On a screen that
|
||||
/// cannot hold the default, start maximised instead.
|
||||
/// </summary>
|
||||
private void FitToSmallScreen()
|
||||
{
|
||||
if (WindowState != WindowState.Normal) return;
|
||||
var work = System.Windows.SystemParameters.WorkArea;
|
||||
if (work.Width <= 0 || work.Height <= 0) return;
|
||||
if (Width <= work.Width && Height <= work.Height) return;
|
||||
WindowState = WindowState.Maximized;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
||||
@@ -0,0 +1,373 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
|
||||
namespace MmdPdf.Services
|
||||
{
|
||||
/// <summary>Which side of the paper the copier reads, and from where.</summary>
|
||||
internal enum ScanFeed
|
||||
{
|
||||
Flatbed,
|
||||
FeederOneSide,
|
||||
FeederBothSides,
|
||||
}
|
||||
|
||||
/// <summary>How much ink the copier records.</summary>
|
||||
internal enum ScanColour
|
||||
{
|
||||
BlackAndWhite,
|
||||
Greyscale,
|
||||
Colour,
|
||||
}
|
||||
|
||||
/// <summary>One copier the machine can reach.</summary>
|
||||
internal sealed class ScannerInfo
|
||||
{
|
||||
internal ScannerInfo(string deviceId, string name)
|
||||
{
|
||||
DeviceId = deviceId;
|
||||
Name = name;
|
||||
}
|
||||
|
||||
internal string DeviceId { get; }
|
||||
internal string Name { get; }
|
||||
|
||||
public override string ToString() => Name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pulls pages off a network copier or a desk scanner through WIA, the imaging service that
|
||||
/// ships with Windows. Nothing is installed for this: the COM objects are created late-bound
|
||||
/// by ProgID, so a machine with no imaging stack fails at <see cref="IsAvailable"/> instead of
|
||||
/// at load time, and the application still starts.
|
||||
///
|
||||
/// The page images land in a folder of their own under the user's temp directory and are
|
||||
/// handed back as file paths; turning them into a document is PdfImport's job, the same way
|
||||
/// an imported photograph is handled.
|
||||
/// </summary>
|
||||
internal static class ScanService
|
||||
{
|
||||
// WIA item property ids. These are the documented constants; the COM interface takes
|
||||
// them as integers, so they are written here once rather than repeated as magic numbers.
|
||||
private const int WIA_IPS_CUR_INTENT = 6146;
|
||||
private const int WIA_IPS_XRES = 6147;
|
||||
private const int WIA_IPS_YRES = 6148;
|
||||
private const int WIA_IPS_XPOS = 6149;
|
||||
private const int WIA_IPS_YPOS = 6150;
|
||||
private const int WIA_IPS_XEXTENT = 6151;
|
||||
private const int WIA_IPS_YEXTENT = 6152;
|
||||
private const int WIA_IPA_DATATYPE = 4103;
|
||||
private const int WIA_IPA_DEPTH = 4104;
|
||||
|
||||
// Device properties that drive the sheet feeder.
|
||||
private const int WIA_DPS_DOCUMENT_HANDLING_SELECT = 3088;
|
||||
private const int WIA_DPS_DOCUMENT_HANDLING_STATUS = 3087;
|
||||
private const int WIA_DPS_PAGES = 3096;
|
||||
|
||||
private const int FEEDER = 0x001;
|
||||
private const int FLATBED = 0x002;
|
||||
private const int DUPLEX = 0x004;
|
||||
private const int FEED_READY = 0x001;
|
||||
|
||||
// Intent values: what the copier should optimise for.
|
||||
private const int INTENT_COLOUR = 0x00000001;
|
||||
private const int INTENT_GREYSCALE = 0x00000002;
|
||||
private const int INTENT_TEXT = 0x00000004;
|
||||
|
||||
private const string FORMAT_PNG = "{B96B3CAF-0728-11D3-9D7B-0000F81EF32E}";
|
||||
|
||||
// Paper the copier is asked for, in inches. A4 is the MMD standard; Letter is here
|
||||
// because the Dubai office buys it locally.
|
||||
private static readonly Dictionary<string, (double W, double H)> PaperInches =
|
||||
new Dictionary<string, (double, double)>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["A4 portrait"] = (8.27, 11.69),
|
||||
["A4 landscape"] = (11.69, 8.27),
|
||||
["A5 portrait"] = (5.83, 8.27),
|
||||
["Letter portrait"] = (8.5, 11.0),
|
||||
["Legal portrait"] = (8.5, 14.0),
|
||||
};
|
||||
|
||||
internal static IEnumerable<string> PaperSizes => PaperInches.Keys;
|
||||
|
||||
/// <summary>True when this machine has the Windows imaging service at all.</summary>
|
||||
internal static bool IsAvailable => Type.GetTypeFromProgID("WIA.DeviceManager") != null;
|
||||
|
||||
/// <summary>
|
||||
/// Every copier and scanner the machine can currently reach. An empty list is a normal
|
||||
/// answer - it means nothing is switched on - and never an exception the caller has to
|
||||
/// catch.
|
||||
/// </summary>
|
||||
internal static List<ScannerInfo> ListScanners()
|
||||
{
|
||||
var found = new List<ScannerInfo>();
|
||||
var progId = Type.GetTypeFromProgID("WIA.DeviceManager");
|
||||
if (progId == null) return found;
|
||||
|
||||
object? manager = null;
|
||||
try
|
||||
{
|
||||
manager = Activator.CreateInstance(progId);
|
||||
dynamic dm = manager!;
|
||||
dynamic infos = dm.DeviceInfos;
|
||||
int count = (int)infos.Count;
|
||||
for (int i = 1; i <= count; i++) // WIA collections are 1-based
|
||||
{
|
||||
dynamic info = infos[i];
|
||||
try
|
||||
{
|
||||
// 1 == ScannerDeviceType. Cameras and video devices are skipped.
|
||||
if ((int)info.Type != 1) continue;
|
||||
string name = ReadProperty(info.Properties, "Name") ?? "Copier";
|
||||
found.Add(new ScannerInfo((string)info.DeviceID, name));
|
||||
}
|
||||
finally { Release(info); }
|
||||
}
|
||||
}
|
||||
catch (COMException) { /* imaging service not running: nothing to offer */ }
|
||||
catch (InvalidCastException) { }
|
||||
finally { Release(manager); }
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads pages and writes each one as a PNG into its own folder. Returns the files in
|
||||
/// page order. Cancelling stops after the page in progress - a copier cannot be
|
||||
/// interrupted mid-sheet.
|
||||
/// </summary>
|
||||
/// <param name="onPage">Called with the running page count as each sheet lands.</param>
|
||||
internal static List<string> Acquire(
|
||||
string deviceId,
|
||||
ScanFeed feed,
|
||||
ScanColour colour,
|
||||
int dpi,
|
||||
string paper,
|
||||
int maxPages,
|
||||
CancellationToken cancel,
|
||||
Action<int>? onPage = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(deviceId)) throw new ArgumentException("No copier chosen.", nameof(deviceId));
|
||||
if (dpi < 75 || dpi > 1200) throw new ArgumentOutOfRangeException(nameof(dpi));
|
||||
|
||||
var progId = Type.GetTypeFromProgID("WIA.DeviceManager")
|
||||
?? throw new InvalidOperationException("This machine has no imaging service.");
|
||||
|
||||
string folder = Path.Combine(Path.GetTempPath(), "MmdPdf.Scan." + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(folder);
|
||||
|
||||
var pages = new List<string>();
|
||||
object? manager = null;
|
||||
object? device = null;
|
||||
try
|
||||
{
|
||||
manager = Activator.CreateInstance(progId);
|
||||
dynamic dm = manager!;
|
||||
dynamic infos = dm.DeviceInfos;
|
||||
|
||||
dynamic? chosen = null;
|
||||
int count = (int)infos.Count;
|
||||
for (int i = 1; i <= count; i++)
|
||||
{
|
||||
dynamic info = infos[i];
|
||||
if (string.Equals((string)info.DeviceID, deviceId, StringComparison.Ordinal)) { chosen = info; break; }
|
||||
Release(info);
|
||||
}
|
||||
if (chosen == null) throw new InvalidOperationException("That copier is no longer reachable.");
|
||||
|
||||
device = chosen.Connect();
|
||||
dynamic dev = device!;
|
||||
ConfigureFeeder(dev, feed, maxPages);
|
||||
|
||||
dynamic item = dev.Items[1];
|
||||
try
|
||||
{
|
||||
ConfigurePage(item, colour, dpi, paper);
|
||||
|
||||
for (int page = 1; page <= maxPages; page++)
|
||||
{
|
||||
cancel.ThrowIfCancellationRequested();
|
||||
|
||||
string path = Path.Combine(folder, "page-" + page.ToString("000", CultureInfo.InvariantCulture) + ".png");
|
||||
try
|
||||
{
|
||||
dynamic image = item.Transfer(FORMAT_PNG);
|
||||
try
|
||||
{
|
||||
image.SaveFile(path);
|
||||
pages.Add(path);
|
||||
onPage?.Invoke(pages.Count);
|
||||
}
|
||||
finally { Release(image); }
|
||||
}
|
||||
catch (COMException ex) when (IsOutOfPaper(ex))
|
||||
{
|
||||
break; // the feeder emptied: a normal end, not a failure
|
||||
}
|
||||
|
||||
if (feed == ScanFeed.Flatbed) break; // one sheet on the glass
|
||||
if (!FeederHasPaper(dev)) break;
|
||||
}
|
||||
}
|
||||
finally { Release(item); }
|
||||
}
|
||||
finally
|
||||
{
|
||||
Release(device);
|
||||
Release(manager);
|
||||
}
|
||||
|
||||
if (pages.Count == 0)
|
||||
{
|
||||
try { Directory.Delete(folder, true); } catch (IOException) { }
|
||||
catch (UnauthorizedAccessException) { }
|
||||
}
|
||||
return pages;
|
||||
}
|
||||
|
||||
private static void ConfigureFeeder(dynamic device, ScanFeed feed, int maxPages)
|
||||
{
|
||||
int handling = feed == ScanFeed.Flatbed ? FLATBED : FEEDER;
|
||||
if (feed == ScanFeed.FeederBothSides) handling |= DUPLEX;
|
||||
|
||||
TrySetProperty(device.Properties, WIA_DPS_DOCUMENT_HANDLING_SELECT, handling);
|
||||
if (feed != ScanFeed.Flatbed) TrySetProperty(device.Properties, WIA_DPS_PAGES, maxPages);
|
||||
}
|
||||
|
||||
private static void ConfigurePage(dynamic item, ScanColour colour, int dpi, string paper)
|
||||
{
|
||||
int intent = colour switch
|
||||
{
|
||||
ScanColour.Colour => INTENT_COLOUR,
|
||||
ScanColour.Greyscale => INTENT_GREYSCALE,
|
||||
_ => INTENT_TEXT,
|
||||
};
|
||||
int depth = colour switch
|
||||
{
|
||||
ScanColour.Colour => 24,
|
||||
ScanColour.Greyscale => 8,
|
||||
_ => 1,
|
||||
};
|
||||
int dataType = colour switch
|
||||
{
|
||||
ScanColour.Colour => 3, // WIA_DATA_COLOR
|
||||
ScanColour.Greyscale => 2, // WIA_DATA_GRAYSCALE
|
||||
_ => 0, // WIA_DATA_THRESHOLD
|
||||
};
|
||||
|
||||
TrySetProperty(item.Properties, WIA_IPS_CUR_INTENT, intent);
|
||||
TrySetProperty(item.Properties, WIA_IPA_DATATYPE, dataType);
|
||||
TrySetProperty(item.Properties, WIA_IPA_DEPTH, depth);
|
||||
TrySetProperty(item.Properties, WIA_IPS_XRES, dpi);
|
||||
TrySetProperty(item.Properties, WIA_IPS_YRES, dpi);
|
||||
|
||||
var size = PaperInches.TryGetValue(paper ?? string.Empty, out var found) ? found : PaperInches["A4 portrait"];
|
||||
TrySetProperty(item.Properties, WIA_IPS_XPOS, 0);
|
||||
TrySetProperty(item.Properties, WIA_IPS_YPOS, 0);
|
||||
TrySetProperty(item.Properties, WIA_IPS_XEXTENT, (int)Math.Round(size.W * dpi));
|
||||
TrySetProperty(item.Properties, WIA_IPS_YEXTENT, (int)Math.Round(size.H * dpi));
|
||||
}
|
||||
|
||||
private static bool FeederHasPaper(dynamic device)
|
||||
{
|
||||
try
|
||||
{
|
||||
object? raw = ReadPropertyValue(device.Properties, WIA_DPS_DOCUMENT_HANDLING_STATUS);
|
||||
if (raw == null) return false;
|
||||
return ((int)Convert.ToInt32(raw, CultureInfo.InvariantCulture) & FEED_READY) == FEED_READY;
|
||||
}
|
||||
catch (COMException) { return false; }
|
||||
catch (InvalidCastException) { return false; }
|
||||
}
|
||||
|
||||
/// <summary>The copier saying "the tray is empty", which ends a run rather than failing it.</summary>
|
||||
private static bool IsOutOfPaper(COMException ex)
|
||||
{
|
||||
const int WIA_ERROR_PAPER_EMPTY = unchecked((int)0x80210003);
|
||||
const int WIA_ERROR_PAPER_JAM = unchecked((int)0x80210002);
|
||||
return ex.ErrorCode == WIA_ERROR_PAPER_EMPTY || ex.ErrorCode == WIA_ERROR_PAPER_JAM;
|
||||
}
|
||||
|
||||
private static string? ReadProperty(dynamic properties, string name)
|
||||
{
|
||||
try
|
||||
{
|
||||
int count = (int)properties.Count;
|
||||
for (int i = 1; i <= count; i++)
|
||||
{
|
||||
dynamic p = properties[i];
|
||||
try
|
||||
{
|
||||
if (string.Equals((string)p.Name, name, StringComparison.OrdinalIgnoreCase))
|
||||
return Convert.ToString(p.get_Value(), CultureInfo.InvariantCulture);
|
||||
}
|
||||
finally { Release(p); }
|
||||
}
|
||||
}
|
||||
catch (COMException) { }
|
||||
catch (InvalidCastException) { }
|
||||
return null;
|
||||
}
|
||||
|
||||
private static object? ReadPropertyValue(dynamic properties, int propertyId)
|
||||
{
|
||||
try
|
||||
{
|
||||
int count = (int)properties.Count;
|
||||
for (int i = 1; i <= count; i++)
|
||||
{
|
||||
dynamic p = properties[i];
|
||||
try
|
||||
{
|
||||
if ((int)p.PropertyID == propertyId) return p.get_Value();
|
||||
}
|
||||
finally { Release(p); }
|
||||
}
|
||||
}
|
||||
catch (COMException) { }
|
||||
catch (InvalidCastException) { }
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets one property if the copier has it. Copiers differ wildly in what they expose, so a
|
||||
/// missing or read-only property is ignored rather than failing the run - the copier then
|
||||
/// uses its own default for that setting.
|
||||
/// </summary>
|
||||
private static void TrySetProperty(dynamic properties, int propertyId, int value)
|
||||
{
|
||||
try
|
||||
{
|
||||
int count = (int)properties.Count;
|
||||
for (int i = 1; i <= count; i++)
|
||||
{
|
||||
dynamic p = properties[i];
|
||||
try
|
||||
{
|
||||
if ((int)p.PropertyID != propertyId) continue;
|
||||
if ((bool)p.IsReadOnly) return;
|
||||
p.set_Value(value);
|
||||
return;
|
||||
}
|
||||
finally { Release(p); }
|
||||
}
|
||||
}
|
||||
catch (COMException) { }
|
||||
catch (InvalidCastException) { }
|
||||
}
|
||||
|
||||
private static void Release(object? comObject)
|
||||
{
|
||||
if (comObject == null) return;
|
||||
try
|
||||
{
|
||||
if (Marshal.IsComObject(comObject)) Marshal.ReleaseComObject(comObject);
|
||||
}
|
||||
catch (ArgumentException) { }
|
||||
catch (InvalidComObjectException) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using MmdPdf.Controls;
|
||||
using MmdPdf.Features.Scan;
|
||||
|
||||
namespace MmdPdf
|
||||
{
|
||||
// Pulling pages off a copier. The dialog does the talking to the hardware; this file only
|
||||
// turns the pages it hands back into a document, which is the same route an imported
|
||||
// photograph takes (ImportAndZip.cs).
|
||||
public partial class MainWindow
|
||||
{
|
||||
private void Scan_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var dlg = new ScanDialog { Owner = this };
|
||||
bool? answer;
|
||||
try
|
||||
{
|
||||
answer = dlg.ShowDialog();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
KillerDialog.Show(this, "The copier could not be reached.\n" + ex.Message,
|
||||
"MMD PDF", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (answer != true || dlg.Pages.Length == 0) return;
|
||||
|
||||
string name = "Scan " + DateTime.Now.ToString("d MMM yyyy HH:mm", CultureInfo.CurrentCulture);
|
||||
OpenImagesAsImportedTab(dlg.Pages, name);
|
||||
|
||||
// "Make the words searchable" is the copier setting a person ticks before the run;
|
||||
// the reading itself happens here, on the document that just opened, through the
|
||||
// text recognition the application already carries.
|
||||
if (dlg.MakeSearchable) MakeSearchablePdf();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -590,7 +590,7 @@ namespace MmdPdf
|
||||
// Text only: caption, no icon (nothing to shed - there'd be nothing left).
|
||||
if (textOnly)
|
||||
{
|
||||
btn.Width = double.NaN; btn.MinWidth = 0; btn.Height = 34; btn.Padding = new Thickness(8, 5, 8, 5);
|
||||
btn.Width = double.NaN; btn.MinWidth = 0; btn.Height = 27; btn.Padding = new Thickness(8, 3, 8, 3);
|
||||
btn.Content = new TextBlock
|
||||
{
|
||||
Text = Loc(key),
|
||||
@@ -604,7 +604,7 @@ namespace MmdPdf
|
||||
// Text beside the icon, while it still fits.
|
||||
if (beside && withLabel)
|
||||
{
|
||||
btn.Width = double.NaN; btn.MinWidth = 0; btn.Height = 34; btn.Padding = new Thickness(8, 5, 8, 5);
|
||||
btn.Width = double.NaN; btn.MinWidth = 0; btn.Height = 27; btn.Padding = new Thickness(8, 3, 8, 3);
|
||||
var row = new StackPanel { Orientation = Orientation.Horizontal, VerticalAlignment = VerticalAlignment.Center };
|
||||
row.Children.Add(new TextBlock
|
||||
{
|
||||
@@ -628,7 +628,7 @@ namespace MmdPdf
|
||||
// Text under the icon: the icon stacked over a small caption, while it still fits.
|
||||
if (under && withLabel)
|
||||
{
|
||||
btn.Width = double.NaN; btn.MinWidth = 0; btn.Height = large ? 56 : 52; btn.Padding = new Thickness(6, 4, 6, 4);
|
||||
btn.Width = double.NaN; btn.MinWidth = 0; btn.Height = large ? 45 : 42; btn.Padding = new Thickness(6, 2, 6, 2);
|
||||
var col = new StackPanel { Orientation = Orientation.Vertical, HorizontalAlignment = HorizontalAlignment.Center };
|
||||
col.Children.Add(new TextBlock
|
||||
{
|
||||
|
||||
+45
-7
@@ -10,6 +10,41 @@
|
||||
Square by default. Held for Ben's sign-off: dark table header rows and the
|
||||
window-chrome variant (V2 vs V7) are still being chosen. -->
|
||||
|
||||
<!-- The one merged top bar, locked 2026-08-28. MMD red, the white lockup on it,
|
||||
and the window buttons at its right end. These are the design system's own
|
||||
values; no colour here is new. -->
|
||||
<LinearGradientBrush x:Key="TitleBarBrush" StartPoint="0,0" EndPoint="0,1" po:Freeze="True">
|
||||
<GradientStop Offset="0.0" Color="#d4213f"/>
|
||||
<GradientStop Offset="0.55" Color="#c8102e"/>
|
||||
<GradientStop Offset="1.0" Color="#a50d26"/>
|
||||
</LinearGradientBrush>
|
||||
<sys:Double x:Key="TitleBarHeight">42</sys:Double>
|
||||
<Thickness x:Key="TitleBarPadding">14,0,0,0</Thickness>
|
||||
<SolidColorBrush x:Key="CaptionGlyphBrush" Color="#ffffff" po:Freeze="True"/>
|
||||
<sys:Double x:Key="CaptionButtonWidth">24</sys:Double>
|
||||
<sys:Double x:Key="CaptionButtonHeight">24</sys:Double>
|
||||
<Thickness x:Key="CaptionButtonsMargin">0,0,9,0</Thickness>
|
||||
<!-- The window buttons sit on the red band, so they are white. The close button
|
||||
inverts on hover - white plate, red cross - because red on red is invisible,
|
||||
which is exactly what the first build of this band showed. -->
|
||||
<SolidColorBrush x:Key="CaptionButtonBrush" Color="#1fffffff" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="CaptionHoverBrush" Color="#33ffffff" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="CaptionCloseBrush" Color="#ffffff" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="CaptionCloseHoverBrush" Color="#ffffff" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="CaptionCloseHoverFgBrush" Color="#c8102e" po:Freeze="True"/>
|
||||
<sys:Double x:Key="WordmarkShadowOpacity">0</sys:Double>
|
||||
<sys:Double x:Key="TitleIconSize">0</sys:Double>
|
||||
<Thickness x:Key="TitleIconMargin">0</Thickness>
|
||||
|
||||
<!-- A window that is in front, for the dialogs: white to silver with a red top edge.
|
||||
The bevel lives on title bars only; buttons are flat. -->
|
||||
<LinearGradientBrush x:Key="WindowFrontTitleBrush" StartPoint="0,0" EndPoint="0,1" po:Freeze="True">
|
||||
<GradientStop Offset="0.0" Color="#ffffff"/>
|
||||
<GradientStop Offset="0.62" Color="#ffffff"/>
|
||||
<GradientStop Offset="1.0" Color="#f1f1ec"/>
|
||||
</LinearGradientBrush>
|
||||
<sys:Double x:Key="FooterHeight">26</sys:Double>
|
||||
|
||||
<!-- Surfaces -->
|
||||
<SolidColorBrush x:Key="SurfaceBrush" Color="#f1f1ec" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="PaneBrush" Color="#e4e3dc" po:Freeze="True"/>
|
||||
@@ -54,8 +89,8 @@
|
||||
<SolidColorBrush x:Key="TextBrush" Color="#23221e" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="MutedTextBrush" Color="#3c3a34" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="MenuTextBrush" Color="#23221e" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="ChromeTextBrush" Color="#23221e" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="FilenameBrush" Color="#3c3a34" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="ChromeTextBrush" Color="#ffffff" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="FilenameBrush" Color="#e8ffffff" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="DimTextBrush" Color="#767368" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="TextFooter" Color="#6b6a64" po:Freeze="True"/>
|
||||
<SolidColorBrush x:Key="DangerRed" Color="#c8102e" po:Freeze="True"/>
|
||||
@@ -99,10 +134,13 @@
|
||||
<!-- No text stroke on a light corporate surface. -->
|
||||
<DropShadowEffect x:Key="TextStroke" Color="Black" BlurRadius="0" ShadowDepth="0" Direction="270" Opacity="0"/>
|
||||
|
||||
<!-- Square. The portal's chrome is square-edged; rounded corners are not MMD. -->
|
||||
<CornerRadius x:Key="ControlCornerRadius">0</CornerRadius>
|
||||
<CornerRadius x:Key="SmallCornerRadius">0</CornerRadius>
|
||||
<CornerRadius x:Key="FlyoutCornerRadius">0</CornerRadius>
|
||||
<CornerRadius x:Key="AccentSwatchCornerRadius">0</CornerRadius>
|
||||
<!-- Four-pixel corners, per the design system locked 2026-08-28. The window itself
|
||||
stays square at the top because the band runs edge to edge when maximised, which
|
||||
is how these machines are used. -->
|
||||
<CornerRadius x:Key="ControlCornerRadius">4</CornerRadius>
|
||||
<CornerRadius x:Key="SmallCornerRadius">4</CornerRadius>
|
||||
<CornerRadius x:Key="FlyoutCornerRadius">4</CornerRadius>
|
||||
<CornerRadius x:Key="AccentSwatchCornerRadius">4</CornerRadius>
|
||||
<CornerRadius x:Key="TitleBarCornerRadius">0</CornerRadius>
|
||||
|
||||
</ResourceDictionary>
|
||||
|
||||
Reference in New Issue
Block a user