diff --git a/NAPS2.Lib.WinForms/EtoForms/WinForms/WinFormsEtoPlatform.cs b/NAPS2.Lib.WinForms/EtoForms/WinForms/WinFormsEtoPlatform.cs index 5505fe413..315657f21 100644 --- a/NAPS2.Lib.WinForms/EtoForms/WinForms/WinFormsEtoPlatform.cs +++ b/NAPS2.Lib.WinForms/EtoForms/WinForms/WinFormsEtoPlatform.cs @@ -147,8 +147,11 @@ public class WinFormsEtoPlatform : EtoPlatform { if (control.ControlObject is wf.Label label) { - label.AutoSize = false; - label.Width = 0; + if (!control.ParentWindow.Loaded) + { + label.AutoSize = false; + label.Width = 0; + } return label.GetPreferredSize(new sd.Size(defaultWidth, 0)).ToEto(); } return base.GetWrappedSize(control, defaultWidth); diff --git a/NAPS2.Lib/EtoForms/EtoDialogBase.cs b/NAPS2.Lib/EtoForms/EtoDialogBase.cs index d2b073bd0..7805bbebb 100644 --- a/NAPS2.Lib/EtoForms/EtoDialogBase.cs +++ b/NAPS2.Lib/EtoForms/EtoDialogBase.cs @@ -16,7 +16,7 @@ public abstract class EtoDialogBase : Dialog, IFormBase Resizable = true; ShowInTaskbar = true; LayoutController.Bind(this); - LayoutController.ContentSet += (_, _) => FormStateController.UpdateLayoutSize(LayoutController); + LayoutController.Invalidated += (_, _) => FormStateController.UpdateLayoutSize(LayoutController); } // TODO: PR for Eto to integrate this diff --git a/NAPS2.Lib/EtoForms/EtoFormBase.cs b/NAPS2.Lib/EtoForms/EtoFormBase.cs index f2db03e71..915ab912c 100644 --- a/NAPS2.Lib/EtoForms/EtoFormBase.cs +++ b/NAPS2.Lib/EtoForms/EtoFormBase.cs @@ -14,7 +14,7 @@ public abstract class EtoFormBase : Form, IFormBase FormStateController = new FormStateController(this, config); Resizable = true; LayoutController.Bind(this); - LayoutController.ContentSet += (_, _) => FormStateController.UpdateLayoutSize(LayoutController); + LayoutController.Invalidated += (_, _) => FormStateController.UpdateLayoutSize(LayoutController); } public FormStateController FormStateController { get; } diff --git a/NAPS2.Lib/EtoForms/FormStateController.cs b/NAPS2.Lib/EtoForms/FormStateController.cs index c737d7df0..48257c1fb 100644 --- a/NAPS2.Lib/EtoForms/FormStateController.cs +++ b/NAPS2.Lib/EtoForms/FormStateController.cs @@ -4,6 +4,7 @@ using NAPS2.EtoForms.Layout; namespace NAPS2.EtoForms; +// TODO: Verify all the size handling works correctly with maximized forms. public class FormStateController { private readonly Window _window; @@ -46,10 +47,27 @@ public class FormStateController if (AutoLayoutSize) { _minimumClientSize = layoutController.GetLayoutSize(false); + var oldDefaultClientSize = DefaultClientSize; + var oldMaximumClientSize = _maximumClientSize; DefaultClientSize = layoutController.GetLayoutSize(true) + DefaultExtraLayoutSize; - if (FixedHeightLayout) + _maximumClientSize = FixedHeightLayout ? new Size(0, _minimumClientSize.Height) : Size.Empty; + + if (_loaded) { - _maximumClientSize = new Size(0, _minimumClientSize.Height); + // Dynamic re-sizing because the layout contents have changed (not just a normal resize/maximize etc). + var size = EtoPlatform.Current.GetClientSize(_window); + if (oldMaximumClientSize.Height > 0) + { + size.Height = Math.Min(size.Height, oldMaximumClientSize.Height); + } + size += DefaultClientSize - oldDefaultClientSize; + size = Size.Max(size, _minimumClientSize); + if (_maximumClientSize.Height > 0) + { + size.Height = Math.Min(size.Height, _maximumClientSize.Height); + } + EtoPlatform.Current.SetMinimumClientSize(_window, _minimumClientSize); + EtoPlatform.Current.SetClientSize(_window, size); } } } diff --git a/NAPS2.Lib/EtoForms/Layout/ControlWithLayoutAttributes.cs b/NAPS2.Lib/EtoForms/Layout/ControlWithLayoutAttributes.cs index 0a3178c71..2fb6d25d9 100644 --- a/NAPS2.Lib/EtoForms/Layout/ControlWithLayoutAttributes.cs +++ b/NAPS2.Lib/EtoForms/Layout/ControlWithLayoutAttributes.cs @@ -26,7 +26,7 @@ public class ControlWithLayoutAttributes : LayoutElement Padding? padding = null, int? spacingAfter = null, int? width = null, int? minWidth = null, int? maxWidth = null, int? naturalWidth = null, int? height = null, int? minHeight = null, int? maxHeight = null, int? naturalHeight = null, - int? wrapDefaultWidth = null, LayoutAlignment? alignment = null) + int? wrapDefaultWidth = null, LayoutAlignment? alignment = null, LayoutVisibility? visibility = null) { Control = control.Control; XScale = xScale ?? control.XScale; @@ -43,6 +43,7 @@ public class ControlWithLayoutAttributes : LayoutElement NaturalHeight = naturalHeight ?? control.NaturalHeight; WrapDefaultWidth = wrapDefaultWidth ?? control.WrapDefaultWidth; Alignment = alignment ?? control.Alignment; + Visibility = visibility ?? control.Visibility; } public static implicit operator ControlWithLayoutAttributes(Control control) => @@ -87,6 +88,11 @@ public class ControlWithLayoutAttributes : LayoutElement public override SizeF GetPreferredSize(LayoutContext context, RectangleF parentBounds) { var size = SizeF.Empty; + if (!IsVisible) + { + EnsureIsAdded(context); + return size; + } if (Control != null) { EnsureIsAdded(context); @@ -143,10 +149,15 @@ public class ControlWithLayoutAttributes : LayoutElement private void EnsureIsAdded(LayoutContext context) { if (Control == null) return; + Control.Visible = IsVisible; if (context.IsFirstLayout && !_isAdded) { EtoPlatform.Current.AddToContainer(context.Layout, Control, context.InOverlay); _isAdded = true; + if (Visibility != null) + { + Visibility.IsVisibleChanged += (_, _) => context.Invalidate(); + } } if (context.IsFirstLayout && !_isWindowSet && context.Window != null) { diff --git a/NAPS2.Lib/EtoForms/Layout/EtoLayoutExtensions.cs b/NAPS2.Lib/EtoForms/Layout/EtoLayoutExtensions.cs index 6dbcb96b0..a874f4b64 100644 --- a/NAPS2.Lib/EtoForms/Layout/EtoLayoutExtensions.cs +++ b/NAPS2.Lib/EtoForms/Layout/EtoLayoutExtensions.cs @@ -71,6 +71,8 @@ public static class EtoLayoutExtensions new ControlWithLayoutAttributes(control, padding: new Padding(all)); public static ControlWithLayoutAttributes Padding(this Control control, int left = 0, int top = 0, int right = 0, int bottom = 0) => new ControlWithLayoutAttributes(control, padding: new Padding(left, top, right, bottom)); + public static ControlWithLayoutAttributes Visible(this Control control, LayoutVisibility visibility) => + new ControlWithLayoutAttributes(control, visibility: visibility); public static ControlWithLayoutAttributes Wrap(this Label label, int defaultWidth) { @@ -90,6 +92,12 @@ public static class EtoLayoutExtensions new ControlWithLayoutAttributes(control, minHeight: minHeight); public static ControlWithLayoutAttributes MaxHeight(this ControlWithLayoutAttributes control, int maxHeight) => new ControlWithLayoutAttributes(control, maxHeight: maxHeight); + public static ControlWithLayoutAttributes NaturalSize(this ControlWithLayoutAttributes control, int width, int height) => + new ControlWithLayoutAttributes(control, naturalWidth: width, naturalHeight: height); + public static ControlWithLayoutAttributes NaturalWidth(this ControlWithLayoutAttributes control, int width) => + new ControlWithLayoutAttributes(control, naturalWidth: width); + public static ControlWithLayoutAttributes NaturalHeight(this ControlWithLayoutAttributes control, int height) => + new ControlWithLayoutAttributes(control, naturalHeight: height); public static ControlWithLayoutAttributes XScale(this ControlWithLayoutAttributes control) => new ControlWithLayoutAttributes(control, xScale: true); public static ControlWithLayoutAttributes YScale(this ControlWithLayoutAttributes control) => @@ -110,6 +118,8 @@ public static class EtoLayoutExtensions new ControlWithLayoutAttributes(control, padding: new Padding(all)); public static ControlWithLayoutAttributes Padding(this ControlWithLayoutAttributes control, int left = 0, int top = 0, int right = 0, int bottom = 0) => new ControlWithLayoutAttributes(control, padding: new Padding(left, top, right, bottom)); + public static ControlWithLayoutAttributes Visible(this ControlWithLayoutAttributes control, LayoutVisibility visibility) => + new ControlWithLayoutAttributes(control, visibility: visibility); public static LayoutColumn Padding(this LayoutColumn column, Padding padding) => new LayoutColumn(column, padding: padding); diff --git a/NAPS2.Lib/EtoForms/Layout/LayoutColumn.cs b/NAPS2.Lib/EtoForms/Layout/LayoutColumn.cs index 8cff42162..1a94890fe 100644 --- a/NAPS2.Lib/EtoForms/Layout/LayoutColumn.cs +++ b/NAPS2.Lib/EtoForms/Layout/LayoutColumn.cs @@ -44,13 +44,13 @@ public class LayoutColumn : LayoutLine protected internal override bool DoesChildScale(LayoutElement child) => child.YScale; - protected override int GetSpacing(int i, LayoutContext context) + protected override int GetSpacingCore(int i, LayoutContext context) { if (i < Children.Length - 1 && Children[i] is ControlWithLayoutAttributes { Control: Label }) { return Children[i].SpacingAfter ?? LabelSpacing ?? context.DefaultLabelSpacing; } - return base.GetSpacing(i, context); + return base.GetSpacingCore(i, context); } protected override float GetBreadth(SizeF size) => size.Width; diff --git a/NAPS2.Lib/EtoForms/Layout/LayoutContext.cs b/NAPS2.Lib/EtoForms/Layout/LayoutContext.cs index 2fc90b252..f48053e94 100644 --- a/NAPS2.Lib/EtoForms/Layout/LayoutContext.cs +++ b/NAPS2.Lib/EtoForms/Layout/LayoutContext.cs @@ -21,4 +21,6 @@ public record LayoutContext(Control Layout) public Window? Window { get; init; } public bool InOverlay { get; init; } + + public Action Invalidate { get; init; } } \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Layout/LayoutController.cs b/NAPS2.Lib/EtoForms/Layout/LayoutController.cs index 9f321fae5..ed1c0b76d 100644 --- a/NAPS2.Lib/EtoForms/Layout/LayoutController.cs +++ b/NAPS2.Lib/EtoForms/Layout/LayoutController.cs @@ -11,6 +11,7 @@ public class LayoutController private Window? _window; private bool _firstLayout = true; private bool _isShown; + private bool _layoutQueued; public LayoutElement? Content { @@ -22,7 +23,7 @@ public class LayoutController DoLayout(); if (setting) { - ContentSet?.Invoke(this, EventArgs.Empty); + Invalidated?.Invoke(this, EventArgs.Empty); } } } @@ -55,9 +56,17 @@ public class LayoutController return naturalSize; } - public void DoLayout() + public void Invalidate() { - if (_window == null || _content == null || !_isShown) return; + _layoutQueued = true; + Invalidated?.Invoke(this, EventArgs.Empty); + _layoutQueued = false; + DoLayout(); + } + + private void DoLayout() + { + if (_window == null || _content == null || !_isShown || _layoutQueued) return; // TODO: Handle added/removed things var size = EtoPlatform.Current.GetClientSize(_window); int p = RootPadding; @@ -82,9 +91,10 @@ public class LayoutController { DefaultSpacing = DefaultSpacing, DefaultLabelSpacing = DefaultLabelSpacing, - IsFirstLayout = _firstLayout + IsFirstLayout = _firstLayout, + Invalidate = Invalidate }; } - public event EventHandler? ContentSet; + public event EventHandler? Invalidated; } \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Layout/LayoutElement.cs b/NAPS2.Lib/EtoForms/Layout/LayoutElement.cs index b4cabf218..ce53c47a6 100644 --- a/NAPS2.Lib/EtoForms/Layout/LayoutElement.cs +++ b/NAPS2.Lib/EtoForms/Layout/LayoutElement.cs @@ -16,6 +16,8 @@ public abstract class LayoutElement protected internal bool XScale { get; set; } protected internal bool YScale { get; set; } protected internal LayoutAlignment Alignment { get; set; } + protected internal LayoutVisibility? Visibility { get; set; } + protected internal bool IsVisible => Visibility?.IsVisible ?? true; protected internal int? SpacingAfter { get; set; } public static implicit operator LayoutElement(Control control) => diff --git a/NAPS2.Lib/EtoForms/Layout/LayoutLine.cs b/NAPS2.Lib/EtoForms/Layout/LayoutLine.cs index f6b49f23e..205f1a9bb 100644 --- a/NAPS2.Lib/EtoForms/Layout/LayoutLine.cs +++ b/NAPS2.Lib/EtoForms/Layout/LayoutLine.cs @@ -55,9 +55,18 @@ public abstract class LayoutLine : LayoutContainer } } - protected virtual int GetSpacing(int i, LayoutContext context) + private int GetSpacing(int i, LayoutContext context) + { + if (Children.Skip(i + 1).All(child => !child.IsVisible)) + { + return 0; + } + if (!Children[i].IsVisible) return 0; + return GetSpacingCore(i, context); + } + + protected virtual int GetSpacingCore(int i, LayoutContext context) { - if (i == Children.Length - 1) return 0; return Children[i].SpacingAfter ?? Spacing ?? context.DefaultSpacing; } @@ -111,19 +120,20 @@ public abstract class LayoutLine : LayoutContainer { // If this line is supposed to be aligned with adjacent lines (e.g. 2 rows in a parent column or vice versa), // then our parent will have pre-calculated our cell sizes and scaling for us. - cellLengths = Aligned ? context.CellLengths : null; - cellScaling = Aligned ? context.CellScaling : null; + if (Aligned && context.CellLengths != null && context.CellScaling != null) + { + cellLengths = context.CellLengths; + cellScaling = context.CellScaling; + return; + } // If we aren't aligned or we don't have a parent to do that pre-calculation, then we just determine our cell // sizes and scaling directly without any special alignment constraints. - if (cellLengths == null || cellScaling == null) + cellLengths = new List(); + cellScaling = new List(); + foreach (var child in Children) { - cellLengths = new List(); - cellScaling = new List(); - foreach (var child in Children) - { - cellLengths.Add(GetLength(child.GetPreferredSize(childContext, bounds))); - cellScaling.Add(DoesChildScale(child)); - } + cellLengths.Add(GetLength(child.GetPreferredSize(childContext, bounds))); + cellScaling.Add(DoesChildScale(child)); } } diff --git a/NAPS2.Lib/EtoForms/Layout/LayoutVisibility.cs b/NAPS2.Lib/EtoForms/Layout/LayoutVisibility.cs new file mode 100644 index 000000000..f6a6c5bf9 --- /dev/null +++ b/NAPS2.Lib/EtoForms/Layout/LayoutVisibility.cs @@ -0,0 +1,22 @@ +namespace NAPS2.EtoForms.Layout; + +public class LayoutVisibility +{ + private bool _isVisible; + + public LayoutVisibility(bool isVisible) + { + _isVisible = isVisible; + } + + public bool IsVisible + { + get => _isVisible; set + { + _isVisible = value; + IsVisibleChanged?.Invoke(this, EventArgs.Empty); + } + } + + public event EventHandler? IsVisibleChanged; +} \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/MessageBoxErrorOutput.cs b/NAPS2.Lib/EtoForms/MessageBoxErrorOutput.cs index af898a63a..c25ca8937 100644 --- a/NAPS2.Lib/EtoForms/MessageBoxErrorOutput.cs +++ b/NAPS2.Lib/EtoForms/MessageBoxErrorOutput.cs @@ -1,14 +1,15 @@ using Eto.Forms; +using NAPS2.EtoForms.Ui; namespace NAPS2.EtoForms; public class MessageBoxErrorOutput : ErrorOutput { - private readonly DialogHelper _dialogHelper; + private readonly IFormFactory _formFactory; - public MessageBoxErrorOutput(DialogHelper dialogHelper) + public MessageBoxErrorOutput(IFormFactory formFactory) { - _dialogHelper = dialogHelper; + _formFactory = formFactory; } public override void DisplayError(string errorMessage) @@ -25,15 +26,12 @@ public class MessageBoxErrorOutput : ErrorOutput { Invoker.Current.SafeInvoke(() => ShowErrorWithDetails(errorMessage, exception.ToString())); } + private void ShowErrorWithDetails(string errorMessage, string details) { - // TODO: Migrate error form - MessageBox.Show(errorMessage, MessageBoxType.Error); - // var form = new FError - // { - // ErrorMessage = errorMessage, - // Details = details - // }; - // form.ShowDialog(); + var form = _formFactory.Create(); + form.ErrorMessage = errorMessage; + form.Details = details; + form.ShowModal(); } } \ No newline at end of file diff --git a/NAPS2.Lib/EtoForms/Ui/EmailSettingsForm.cs b/NAPS2.Lib/EtoForms/Ui/EmailSettingsForm.cs index 22878112e..16f374bef 100644 --- a/NAPS2.Lib/EtoForms/Ui/EmailSettingsForm.cs +++ b/NAPS2.Lib/EtoForms/Ui/EmailSettingsForm.cs @@ -91,7 +91,7 @@ public class EmailSettingsForm : EtoDialogBase _provider.Text = SettingsResources.EmailProvider_NotSelected; break; } - LayoutController.DoLayout(); + LayoutController.Invalidate(); } private void Save() diff --git a/NAPS2.Lib/EtoForms/Ui/ErrorForm.cs b/NAPS2.Lib/EtoForms/Ui/ErrorForm.cs new file mode 100644 index 000000000..0caea0150 --- /dev/null +++ b/NAPS2.Lib/EtoForms/Ui/ErrorForm.cs @@ -0,0 +1,52 @@ +using Eto.Forms; +using NAPS2.EtoForms.Layout; + +namespace NAPS2.EtoForms.Ui; + +public class ErrorForm : EtoDialogBase +{ + private readonly Label _message = new(); + private readonly TextArea _details = new() { ReadOnly = true }; + private readonly LayoutVisibility _detailsVisibility = new(false); + + public ErrorForm(Naps2Config config) + : base(config) + { + Title = UiStrings.ErrorFormTitle; + FormStateController.RestoreFormState = false; + + var image = new ImageView { Image = Icons.exclamation.ToEtoImage() }; + + FormStateController.FixedHeightLayout = true; + LayoutController.Content = L.Column( + L.Row( + image.AlignCenter().Padding(right: 5), + _message.Wrap(350).NaturalWidth(350).AlignCenter().XScale() + ), + L.Row( + C.Link(UiStrings.TechnicalDetails, ToggleDetails).AlignCenter(), + C.Filler(), + C.OkButton(this) + ), + _details.NaturalHeight(120).Visible(_detailsVisibility).YScale() + ); + } + + private void ToggleDetails() + { + FormStateController.FixedHeightLayout = _detailsVisibility.IsVisible; + _detailsVisibility.IsVisible = !_detailsVisibility.IsVisible; + } + + public string ErrorMessage + { + get => _message.Text; + set => _message.Text = value; + } + + public string Details + { + get => _details.Text; + set => _details.Text = value; + } +} \ No newline at end of file diff --git a/NAPS2.Lib/Icons.Designer.cs b/NAPS2.Lib/Icons.Designer.cs index 65ecc1636..f7414d775 100644 --- a/NAPS2.Lib/Icons.Designer.cs +++ b/NAPS2.Lib/Icons.Designer.cs @@ -459,6 +459,16 @@ namespace NAPS2 { } } + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] exclamation { + get { + object obj = ResourceManager.GetObject("exclamation", resourceCulture); + return ((byte[])(obj)); + } + } + /// /// Looks up a localized resource of type System.Byte[]. /// diff --git a/NAPS2.Lib/Icons.resx b/NAPS2.Lib/Icons.resx index 89e3b1ba0..9f7c58dfc 100644 --- a/NAPS2.Lib/Icons.resx +++ b/NAPS2.Lib/Icons.resx @@ -370,4 +370,7 @@ Icons\favicon.ico;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Icons\exclamation.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + \ No newline at end of file diff --git a/NAPS2.Lib/Icons/exclamation.png b/NAPS2.Lib/Icons/exclamation.png new file mode 100644 index 000000000..8270104d4 Binary files /dev/null and b/NAPS2.Lib/Icons/exclamation.png differ diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.Designer.cs b/NAPS2.Lib/Lang/Resources/UiStrings.Designer.cs index d3b15a45b..a50b0d976 100644 --- a/NAPS2.Lib/Lang/Resources/UiStrings.Designer.cs +++ b/NAPS2.Lib/Lang/Resources/UiStrings.Designer.cs @@ -671,6 +671,15 @@ namespace NAPS2.Lang.Resources { } } + /// + /// Looks up a localized string similar to Error. + /// + internal static string ErrorFormTitle { + get { + return ResourceManager.GetString("ErrorFormTitle", resourceCulture); + } + } + /// /// Looks up a localized string similar to Estimated download size: {0} MB. /// @@ -1481,6 +1490,15 @@ namespace NAPS2.Lang.Resources { } } + /// + /// Looks up a localized string similar to Technical Details. + /// + internal static string TechnicalDetails { + get { + return ResourceManager.GetString("TechnicalDetails", resourceCulture); + } + } + /// /// Looks up a localized string similar to Tiff Options. /// diff --git a/NAPS2.Lib/Lang/Resources/UiStrings.resx b/NAPS2.Lib/Lang/Resources/UiStrings.resx index 512c61c54..b780443b3 100644 --- a/NAPS2.Lib/Lang/Resources/UiStrings.resx +++ b/NAPS2.Lib/Lang/Resources/UiStrings.resx @@ -636,4 +636,10 @@ Waiting for authorization... + + Error + + + Technical Details + \ No newline at end of file