Ben, 2026-08-28: four changes, then build it, sign it, and make it deployable
through Intune without alerts.
1. The row of icons is a fifth shorter. Every toolbar label mode comes down
together so the proportions between them are unchanged: 34 -> 27 for a
caption beside the icon, 52/56 -> 42/45 for a caption under it, with the
padding reduced to match. The row measures 48px on screen where it was 60.
2. Scan sits in the main bar. It is a solid white button on the band, to the
left of the window buttons, so a person can pull pages off a copier whatever
the toolbar is showing. Behind it, Services/ScanService.cs talks to the
copier through WIA - the imaging service that ships with Windows, reached
late-bound so a machine without it fails at a check instead of at load - and
Features/Scan/ScanDialog picks the copier, the feed, the colour, the detail
and the paper. Pages come back as images and take the same route into a
document as an imported photograph. "Make the words searchable" runs the
text recognition the application already carries.
3. The official MMD lockup, in white, because the band is red now.
4. The MMD design system locked on 2026-08-28: ONE merged top bar carrying the
mark, the name, the tabs and the tools; 4px corners; 24px window buttons;
the bevel on title bars only. Colours are NOT adjusted - the Red Plate
silver and ink set already in Themes/MMD.xaml is untouched.
Also fixed, both found by looking at the running application at 800x600:
- The default 1000x700 window does not fit an 800x600 plant screen, 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. MainWindow now starts maximised when the screen cannot hold the
default.
- The close button was invisible: CaptionCloseBrush defaults to the danger
red, which on a red band is red on red. The window buttons are white now,
and close inverts to a white plate with a red cross on hover.
Built and run on mmd-win-test-01 at 800x600 through keyboard and mouse only.
Signed with the internal MMD code-signing certificate and timestamped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
187 lines
6.5 KiB
C#
187 lines
6.5 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|