From 262b53cd4bde2bfc6c3a03390b8ca714abd52e61 Mon Sep 17 00:00:00 2001 From: Ben Olden-Cooligan Date: Fri, 6 Sep 2024 14:38:10 -0700 Subject: [PATCH] High-dpi fixes --- NAPS2.Lib.Mac/EtoForms/Mac/MacEtoPlatform.cs | 8 ++++- NAPS2.Lib/EtoForms/EtoPlatform.cs | 3 +- NAPS2.Lib/EtoForms/Layout/C.cs | 8 +++-- NAPS2.Lib/EtoForms/Ui/ChooseDeviceForm.cs | 25 ++++++++++---- NAPS2.Lib/EtoForms/Ui/DesktopCommands.cs | 3 +- .../Widgets/DeviceListViewBehavior.cs | 12 ++++++- .../EtoForms/Widgets/DeviceSelectorWidget.cs | 33 ++++++++++++------- NAPS2.Lib/Scan/DeviceCapsCache.cs | 4 +-- 8 files changed, 67 insertions(+), 29 deletions(-) diff --git a/NAPS2.Lib.Mac/EtoForms/Mac/MacEtoPlatform.cs b/NAPS2.Lib.Mac/EtoForms/Mac/MacEtoPlatform.cs index 846a37b723..283099996e 100644 --- a/NAPS2.Lib.Mac/EtoForms/Mac/MacEtoPlatform.cs +++ b/NAPS2.Lib.Mac/EtoForms/Mac/MacEtoPlatform.cs @@ -49,12 +49,18 @@ public override IListView CreateListView(ListViewBehavior behavior) => public override void ConfigureImageButton(Button button, ButtonFlags flags) { + var nsButton = (NSButton) button.ToNative(); if (button.ImagePosition == ButtonImagePosition.Above) { - var nsButton = (NSButton) button.ToNative(); nsButton.ImageHugsTitle = true; nsButton.Title = Environment.NewLine + nsButton.Title; } + var image = nsButton.Image; + if (image.Representations() is [NSBitmapImageRep rep, ..]) + { + image.Size = new CGSize(rep.PixelsWide / 2f, rep.PixelsHigh / 2f); + nsButton.Image = image; + } } public override Bitmap ToBitmap(IMemoryImage image) diff --git a/NAPS2.Lib/EtoForms/EtoPlatform.cs b/NAPS2.Lib/EtoForms/EtoPlatform.cs index 0e4de2a05f..81bbd5640f 100644 --- a/NAPS2.Lib/EtoForms/EtoPlatform.cs +++ b/NAPS2.Lib/EtoForms/EtoPlatform.cs @@ -109,7 +109,8 @@ public virtual void ConfigureZoomButton(Button button, string icon) { } - public virtual void AttachDpiDependency(Control control, Action callback) => callback(1f); + public virtual void AttachDpiDependency(Control control, Action callback) => + callback(GetScaleFactor(control.ParentWindow)); public virtual SizeF GetWrappedSize(Control control, int defaultWidth) { diff --git a/NAPS2.Lib/EtoForms/Layout/C.cs b/NAPS2.Lib/EtoForms/Layout/C.cs index a0e9549f0e..78d2a8eece 100644 --- a/NAPS2.Lib/EtoForms/Layout/C.cs +++ b/NAPS2.Lib/EtoForms/Layout/C.cs @@ -1,7 +1,6 @@ using System.Windows.Input; using Eto.Drawing; using Eto.Forms; -using NAPS2.Scan; namespace NAPS2.EtoForms.Layout; @@ -93,7 +92,8 @@ public static Button Button(ActionCommand command, ButtonImagePosition imagePosi return Button(command, command.IconName, imagePosition, flags); } - public static Button Button(ActionCommand command, string? iconName, ButtonImagePosition imagePosition = default, ButtonFlags flags = default) + public static Button Button(ActionCommand command, string? iconName, ButtonImagePosition imagePosition = default, + ButtonFlags flags = default) { var button = Button(command); if (command.Image != null) @@ -114,8 +114,10 @@ public static Button Button(ActionCommand command, string? iconName, ButtonImage button.ImagePosition = imagePosition; if (flags.HasFlag(ButtonFlags.LargeText)) { + var baseFontSize = button.Font.Size; EtoPlatform.Current.AttachDpiDependency(button, - scale => button.Font = new Font(button.Font.Family, 12 * scale)); + _ => button.Font = new Font(button.Font.Family, + baseFontSize * 4 / 3 * EtoPlatform.Current.GetLayoutScaleFactor(button.ParentWindow))); } EtoPlatform.Current.ConfigureImageButton(button, flags); return button; diff --git a/NAPS2.Lib/EtoForms/Ui/ChooseDeviceForm.cs b/NAPS2.Lib/EtoForms/Ui/ChooseDeviceForm.cs index 536e3f9d4e..0df9614f20 100644 --- a/NAPS2.Lib/EtoForms/Ui/ChooseDeviceForm.cs +++ b/NAPS2.Lib/EtoForms/Ui/ChooseDeviceForm.cs @@ -35,6 +35,7 @@ public class ChooseDeviceForm : EtoDialogBase private CancellationTokenSource? _getDevicesCts; private Driver? _activeQuery; + private string? _statusIconName; public ChooseDeviceForm(Naps2Config config, IIconProvider iconProvider, DeviceListViewBehavior deviceListViewBehavior, ScanningContext scanningContext, @@ -47,8 +48,10 @@ public ChooseDeviceForm(Naps2Config config, IIconProvider iconProvider, _selectDevice = C.OkButton(this, SelectDevice, UiStrings.Select); _deviceIconList = EtoPlatform.Current.CreateListView(deviceListViewBehavior); _deviceIconList.ImageSize = new Size(48, 32); - deviceListViewBehavior.SetImage(AlwaysAskMarker, iconProvider.GetIcon("ask")!); - deviceListViewBehavior.SetImage(ManualIpMarker, iconProvider.GetIcon("network_ip")!); + deviceListViewBehavior.SetIconName(AlwaysAskMarker, "ask"); + deviceListViewBehavior.SetIconName(ManualIpMarker, "network_ip"); + + EtoPlatform.Current.AttachDpiDependency(this, _ => UpdateStatusIcon()); _deviceTextList.Activated += (_, _) => _selectDevice.PerformClick(); _deviceIconList.ItemClicked += (_, _) => _selectDevice.PerformClick(); @@ -62,6 +65,15 @@ public ChooseDeviceForm(Naps2Config config, IIconProvider iconProvider, _textListVis.IsVisible = config.Get(c => c.DeviceListAsTextOnly); } + private void UpdateStatusIcon() + { + if (_statusIconName != null) + { + _statusIcon.Image = _iconProvider.GetIcon(_statusIconName, EtoPlatform.Current.GetScaleFactor(this)); + _statusIcon.Size = Size.Round(new SizeF(16, 16) * EtoPlatform.Current.GetLayoutScaleFactor(this)); + } + } + private void Driver_MouseUp(object? sender, EventArgs e) { QueryForDevices(); @@ -274,10 +286,8 @@ private void QueryForDevices() if (!cts.IsCancellationRequested) { _spinnerVis.IsVisible = false; - _statusIcon.Image = - DeviceList.Count > 0 - ? _iconProvider.GetIcon("accept_small") - : _iconProvider.GetIcon("exclamation_small"); + _statusIconName = DeviceList.Count > 0 ? "accept_small" : "exclamation_small"; + UpdateStatusIcon(); _statusLabel.Text = DeviceList.Count switch { > 1 => string.Format(UiStrings.DevicesFound, DeviceList.Count), @@ -294,7 +304,8 @@ private void QueryForDevices() if (!cts.IsCancellationRequested) { _spinnerVis.IsVisible = false; - _statusIcon.Image = _iconProvider.GetIcon("exclamation_small"); + _statusIconName = "exclamation_small"; + UpdateStatusIcon(); _statusLabel.Text = ex.Message; } }); diff --git a/NAPS2.Lib/EtoForms/Ui/DesktopCommands.cs b/NAPS2.Lib/EtoForms/Ui/DesktopCommands.cs index d4db7acfd4..f1bc1ae35d 100644 --- a/NAPS2.Lib/EtoForms/Ui/DesktopCommands.cs +++ b/NAPS2.Lib/EtoForms/Ui/DesktopCommands.cs @@ -68,7 +68,8 @@ public DesktopCommands(DesktopController desktopController, DesktopScanControlle }; SaveAll = new ActionCommand(imageListActions.SaveAllAsPdfOrImages) { - Text = UiStrings.SaveAll + Text = UiStrings.SaveAll, + IconName = "diskette" }; SaveSelected = new ActionCommand(imageListActions.SaveSelectedAsPdfOrImages) { diff --git a/NAPS2.Lib/EtoForms/Widgets/DeviceListViewBehavior.cs b/NAPS2.Lib/EtoForms/Widgets/DeviceListViewBehavior.cs index e13a054c86..d2375d8f25 100644 --- a/NAPS2.Lib/EtoForms/Widgets/DeviceListViewBehavior.cs +++ b/NAPS2.Lib/EtoForms/Widgets/DeviceListViewBehavior.cs @@ -6,6 +6,7 @@ namespace NAPS2.EtoForms.Widgets; public class DeviceListViewBehavior : ListViewBehavior { private readonly Dictionary _imageMap = new(); + private readonly Dictionary _iconNameMap = new(); public DeviceListViewBehavior(ColorScheme colorScheme) : base(colorScheme) { @@ -16,10 +17,19 @@ public DeviceListViewBehavior(ColorScheme colorScheme) : base(colorScheme) public void SetImage(ScanDevice item, Image image) => _imageMap[item] = image; + public void SetIconName(ScanDevice item, string iconName) => _iconNameMap[item] = iconName; + public override string GetLabel(ScanDevice item) => item.Name; public override Image GetImage(IListView listView, ScanDevice item) { - return (_imageMap.Get(item)?.Clone() ?? Icons.device.ToEtoImage()).PadTo(listView.ImageSize); + float scale = EtoPlatform.Current.GetScaleFactor(listView.Control.ParentWindow); + if (_imageMap.Get(item) is { } image) + { + int scaledSize = (int) Math.Round(48 * scale); + return image.Clone().ResizeTo(scaledSize).PadTo(listView.ImageSize); + } + string iconName = _iconNameMap.Get(item) ?? "device"; + return EtoPlatform.Current.IconProvider.GetIcon(iconName, scale)!.PadTo(listView.ImageSize); } } \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Widgets/DeviceSelectorWidget.cs b/NAPS2.Lib/EtoForms/Widgets/DeviceSelectorWidget.cs index a8d1b28650..cd8f299b5b 100644 --- a/NAPS2.Lib/EtoForms/Widgets/DeviceSelectorWidget.cs +++ b/NAPS2.Lib/EtoForms/Widgets/DeviceSelectorWidget.cs @@ -1,4 +1,5 @@ using System.Threading; +using Eto.Drawing; using Eto.Forms; using NAPS2.EtoForms.Layout; using NAPS2.Scan; @@ -19,6 +20,8 @@ public class DeviceSelectorWidget private readonly Button _chooseDevice = new() { Text = UiStrings.ChooseDevice }; private DeviceChoice _choice = DeviceChoice.None; + private Image? _deviceIconImage; + private string _deviceIconName = "device"; private CancellationTokenSource? _loadIconCts; public DeviceSelectorWidget(IScanPerformer scanPerformer, DeviceCapsCache deviceCapsCache, @@ -29,6 +32,7 @@ public DeviceSelectorWidget(IScanPerformer scanPerformer, DeviceCapsCache device _iconProvider = iconProvider; _parentWindow = parentWindow; _chooseDevice.Click += ChooseDevice; + EtoPlatform.Current.AttachDpiDependency(_deviceIcon, _ => UpdateDeviceIconImage()); } public required Func ProfileFunc { get; init; } @@ -76,7 +80,6 @@ public bool Enabled private async void ChooseDevice(object? sender, EventArgs args) { - ; var choice = await _scanPerformer.PromptForDevice(ProfileFunc(), AllowAlwaysAsk, _parentWindow.NativeHandle); if (choice.Device != null || choice.AlwaysAsk) { @@ -90,15 +93,15 @@ private async void ChooseDevice(object? sender, EventArgs args) public void SetDeviceIcon(string? iconUri) { - var cachedIcon = _deviceCapsCache.GetCachedIcon(iconUri); - EtoPlatform.Current.AttachDpiDependency(_deviceIcon, scale => - _deviceIcon.Image = - cachedIcon ?? (_choice.AlwaysAsk ? _iconProvider.GetIcon("ask", scale) : _iconProvider.GetIcon("device", scale))); + _deviceIconImage = _deviceCapsCache.GetCachedIcon(iconUri); + _deviceIconName = _choice.AlwaysAsk ? "ask" : "device"; + UpdateDeviceIconImage(); + if (((Window) _parentWindow).Loaded) { _parentWindow.LayoutController.Invalidate(); } - if (cachedIcon == null && iconUri != null) + if (_deviceIconImage == null && iconUri != null) { ReloadDeviceIcon(iconUri); } @@ -118,7 +121,8 @@ private void ReloadDeviceIcon(string iconUri) { if (!cts.IsCancellationRequested) { - _deviceIcon.Image = icon; + _deviceIconImage = icon; + UpdateDeviceIconImage(); _parentWindow.LayoutController.Invalidate(); } }); @@ -126,6 +130,14 @@ private void ReloadDeviceIcon(string iconUri) }); } + private void UpdateDeviceIconImage() + { + float scale = EtoPlatform.Current.GetScaleFactor(_deviceIcon.ParentWindow); + _deviceIcon.Image = _deviceIconImage ?? _iconProvider.GetIcon(_deviceIconName, scale); + var size = _deviceIconImage != null ? new SizeF(48, 48) : new SizeF(32, 32); + _deviceIcon.Size = Size.Round(size * EtoPlatform.Current.GetLayoutScaleFactor(_deviceIcon.ParentWindow)); + } + public static implicit operator LayoutElement(DeviceSelectorWidget control) { return control.AsControl(); @@ -142,11 +154,8 @@ public LayoutElement AsControl() _deviceDriver, C.Filler() ).Spacing(5).Visible(_deviceVis).Scale(), - // TODO: We should probably have a compact choose-device button for the sidebar. - // It should also change the name of the profile if it matches the device name. - // i.e. for users that are naming their own profiles, its their responsibility to keep the devices - // matched up. For a "basic" user that might only create one profile, its name should keep matched - // with the device. + // TODO: We can consider a compact choose-device button for the sidebar, but maybe simpler to force + // creation of separate profiles ShowChooseDevice ? _chooseDevice.AlignCenter() : C.None() ) ); diff --git a/NAPS2.Lib/Scan/DeviceCapsCache.cs b/NAPS2.Lib/Scan/DeviceCapsCache.cs index 7d7f76a3e5..44881f4d47 100644 --- a/NAPS2.Lib/Scan/DeviceCapsCache.cs +++ b/NAPS2.Lib/Scan/DeviceCapsCache.cs @@ -6,8 +6,6 @@ namespace NAPS2.Scan; public class DeviceCapsCache { - private const int ICON_SIZE = 48; - private readonly Dictionary _capsCache = new(); private readonly Dictionary _iconCache = new(); @@ -120,7 +118,7 @@ private async Task DoLoadIcon(string iconUri) var imageBytes = await client.GetByteArrayAsync(iconUri); image = _imageContext.Load(imageBytes); } - return image.PerformTransform(new ThumbnailTransform(ICON_SIZE)).ToEtoImage(); + return image.ToEtoImage(); } private DeviceKey GetDeviceKey(ScanProfile profile)