From 64bbfcef51c2a41a69eeedfe9b5bdf8df6cc51e5 Mon Sep 17 00:00:00 2001 From: Tom Laird-McConnell Date: Mon, 2 Dec 2024 07:41:53 -0800 Subject: [PATCH] Upgrade Avalonia to 11.2.1 (#159) --- Directory.Build.props | 2 +- src/Consolonia.Core/Consolonia.Core.csproj | 4 +- .../Controls/MessageBox.axaml.cs | 36 +++++++----- src/Consolonia.Core/Drawing/ConsoleBrush.cs | 12 +++- ...onsoloniaPlatformRenderInterfaceContext.cs | 8 +++ .../Drawing/ConsoloniaRenderInterface.cs | 7 +++ .../Drawing/DrawingContextImpl.cs | 24 +++++++- src/Consolonia.Core/Drawing/Line.cs | 5 ++ src/Consolonia.Core/Drawing/Rectangle.cs | 5 ++ src/Consolonia.Core/Drawing/RenderTarget.cs | 13 ++--- .../ArrowsAndKeyboardNavigationHandler.cs | 25 +++++++- .../Infrastructure/ConsoleWindow.cs | 57 ++++++++++++++----- .../Infrastructure/ConsoloniaApplication.cs | 4 +- .../Infrastructure/ConsoloniaPlatform.cs | 29 ++++++++++ src/Consolonia.Core/Text/FontManagerImpl.cs | 7 +++ .../GalleryViews/GalleryDataGrid.axaml.cs | 2 +- .../GalleryGradientBrush.axaml.cs | 3 +- .../GalleryViews/SomeDialogWindow.axaml | 2 +- .../GalleryViews/SomeDialogWindow.axaml.cs | 16 +++++- .../Helpers/ConsoloniaTextPresenter.cs | 12 ++-- .../Consolonia.Gallery.Tests/ComboBoxTests.cs | 2 - 21 files changed, 215 insertions(+), 60 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index c61df258..5c03ebf1 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -12,6 +12,6 @@ AVA3001 - 11.0.9 + 11.2.1 \ No newline at end of file diff --git a/src/Consolonia.Core/Consolonia.Core.csproj b/src/Consolonia.Core/Consolonia.Core.csproj index dec1d7b4..0f7f503b 100644 --- a/src/Consolonia.Core/Consolonia.Core.csproj +++ b/src/Consolonia.Core/Consolonia.Core.csproj @@ -9,9 +9,7 @@ - - - + diff --git a/src/Consolonia.Core/Controls/MessageBox.axaml.cs b/src/Consolonia.Core/Controls/MessageBox.axaml.cs index 63bd743a..6de944f8 100644 --- a/src/Consolonia.Core/Controls/MessageBox.axaml.cs +++ b/src/Consolonia.Core/Controls/MessageBox.axaml.cs @@ -43,19 +43,7 @@ public MessageBox() InitializeComponent(); - AttachedToVisualTree += (_, _) => - { - // we don't hook this up until the dialog is shown as the visible state is driven off of the DataContext - // which is set at ShowDialogAsync() time. - if (OkButton.IsVisible) - OkButton.AttachedToVisualTree += (_, _) => OkButton.Focus(); - else if (YesButton.IsVisible) - YesButton.AttachedToVisualTree += (_, _) => YesButton.Focus(); - else if (CancelButton.IsVisible) - CancelButton.AttachedToVisualTree += (_, _) => CancelButton.Focus(); - else if (NoButton.IsVisible) - NoButton.AttachedToVisualTree += (_, _) => NoButton.Focus(); - }; + AttachedToVisualTree += OnShowDialog; } public Mode Mode @@ -88,6 +76,28 @@ public object No set => SetAndRaise(NoProperty, ref _no, value); } + private void OnShowDialog(object sender, VisualTreeAttachmentEventArgs e) + { + // we don't hook this up until the dialog is shown as the visible state is driven off of the DataContext + // which is set at ShowDialogAsync() time. + AttachedToVisualTree -= OnShowDialog; + if (OkButton.IsVisible) + OkButton.AttachedToVisualTree += ButtonAttached; + else if (YesButton.IsVisible) + YesButton.AttachedToVisualTree += ButtonAttached; + else if (CancelButton.IsVisible) + CancelButton.AttachedToVisualTree += ButtonAttached; + else if (NoButton.IsVisible) + NoButton.AttachedToVisualTree += ButtonAttached; + } + + private void ButtonAttached(object sender, VisualTreeAttachmentEventArgs e) + { + var button = (Button)sender; + button.AttachedToVisualTree -= ButtonAttached; + button.Focus(); + } + public async Task ShowDialogAsync(Control parent, string text, string title = null) { DataContext = new MessageBoxViewModel(Mode, Ok, Cancel, Yes, No, text, title ?? Title); diff --git a/src/Consolonia.Core/Drawing/ConsoleBrush.cs b/src/Consolonia.Core/Drawing/ConsoleBrush.cs index ce4e7b02..a2d3bc88 100644 --- a/src/Consolonia.Core/Drawing/ConsoleBrush.cs +++ b/src/Consolonia.Core/Drawing/ConsoleBrush.cs @@ -126,10 +126,16 @@ public static ConsoleBrush FromPosition(IBrush brush, int x, int y, int width, i // Calculate the distance from the center double dx = x - centerX; double dy = y - centerY; - double distance = Math.Sqrt(dx * dx + dy * dy); - // Normalize the distance based on the brush radius - double normalizedDistance = distance / (Math.Min(width, height) * radialBrush.Radius); + // Calculate the distance based on separate X and Y radii + double distanceX = dx / (width * radialBrush.RadiusX.Scalar); + double distanceY = dy / (height * radialBrush.RadiusY.Scalar); + double distance = Math.Sqrt(distanceX * distanceX + distanceY * distanceY); + + // Normalize the distance + double normalizedDistance = distance / + Math.Sqrt(radialBrush.RadiusX.Scalar * radialBrush.RadiusX.Scalar + + radialBrush.RadiusY.Scalar * radialBrush.RadiusY.Scalar); // Clamp the normalized distance to [0, 1] normalizedDistance = Math.Min(Math.Max(normalizedDistance, 0), 1); diff --git a/src/Consolonia.Core/Drawing/ConsoloniaPlatformRenderInterfaceContext.cs b/src/Consolonia.Core/Drawing/ConsoloniaPlatformRenderInterfaceContext.cs index 3ecd180f..af9a867f 100644 --- a/src/Consolonia.Core/Drawing/ConsoloniaPlatformRenderInterfaceContext.cs +++ b/src/Consolonia.Core/Drawing/ConsoloniaPlatformRenderInterfaceContext.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Avalonia; using Avalonia.Platform; namespace Consolonia.Core.Drawing @@ -20,6 +21,13 @@ public IRenderTarget CreateRenderTarget(IEnumerable surfaces) return new RenderTarget(surfaces); } + public IDrawingContextLayerImpl CreateOffscreenRenderTarget(PixelSize pixelSize, double scaling) + { + throw new NotImplementedException(); + } + public bool IsLost => false; + + public IReadOnlyDictionary PublicFeatures { get; } = new Dictionary(); } } \ No newline at end of file diff --git a/src/Consolonia.Core/Drawing/ConsoloniaRenderInterface.cs b/src/Consolonia.Core/Drawing/ConsoloniaRenderInterface.cs index ebe65ab8..8747d071 100644 --- a/src/Consolonia.Core/Drawing/ConsoloniaRenderInterface.cs +++ b/src/Consolonia.Core/Drawing/ConsoloniaRenderInterface.cs @@ -176,10 +176,17 @@ public bool IsSupportedBitmapPixelFormat(PixelFormat format) throw new NotImplementedException(); } + public IPlatformRenderInterfaceRegion CreateRegion() + { + throw new NotImplementedException(); + } + public bool SupportsIndividualRoundRects => false; public AlphaFormat DefaultAlphaFormat => throw new NotImplementedException(); public PixelFormat DefaultPixelFormat => throw new NotImplementedException(); + + public bool SupportsRegions => false; } } \ No newline at end of file diff --git a/src/Consolonia.Core/Drawing/DrawingContextImpl.cs b/src/Consolonia.Core/Drawing/DrawingContextImpl.cs index 00c6811c..4402c758 100644 --- a/src/Consolonia.Core/Drawing/DrawingContextImpl.cs +++ b/src/Consolonia.Core/Drawing/DrawingContextImpl.cs @@ -215,7 +215,7 @@ public void DrawGlyphRun(IBrush foreground, IGlyphRunImpl glyphRun) DrawStringInternal(foreground, text, glyphRun.GlyphTypeface); } - public IDrawingContextLayerImpl CreateLayer(Size size) + public IDrawingContextLayerImpl CreateLayer(PixelSize size) { return new RenderTarget(_consoleWindow); } @@ -234,6 +234,11 @@ public void PushClip(RoundedRect clip) PushClip(clip.Rect); } + public void PushClip(IPlatformRenderInterfaceRegion region) + { + throw new NotImplementedException(); + } + public void PopClip() { _clipStack.Pop(); @@ -286,14 +291,27 @@ public object GetFeature(Type t) throw new NotImplementedException(); } - public RenderOptions RenderOptions { get; set; } - public Matrix Transform { get => _transform; set => _transform = value * _postTransform; } + public void DrawRegion(IBrush brush, IPen pen, IPlatformRenderInterfaceRegion region) + { + throw new NotImplementedException(); + } + + public void PushLayer(Rect bounds) + { + throw new NotImplementedException(); + } + + public void PopLayer() + { + throw new NotImplementedException(); + } + /// /// Draw a straight horizontal line or vertical line /// diff --git a/src/Consolonia.Core/Drawing/Line.cs b/src/Consolonia.Core/Drawing/Line.cs index 7090dc2d..8a273561 100644 --- a/src/Consolonia.Core/Drawing/Line.cs +++ b/src/Consolonia.Core/Drawing/Line.cs @@ -92,6 +92,11 @@ public bool TryGetSegment(double startDistance, double stopDistance, bool startO public IGeometryImpl SourceGeometry { get; } public Matrix Transform { get; } + public IGeometryImpl GetWidenedGeometry(IPen pen) + { + throw new NotImplementedException(); + } + public static Line CreateMyLine(Point p1, Point p2) { (double x, double y) = p2 - p1; diff --git a/src/Consolonia.Core/Drawing/Rectangle.cs b/src/Consolonia.Core/Drawing/Rectangle.cs index ad001372..b4718026 100644 --- a/src/Consolonia.Core/Drawing/Rectangle.cs +++ b/src/Consolonia.Core/Drawing/Rectangle.cs @@ -67,6 +67,11 @@ public bool TryGetSegment(double startDistance, double stopDistance, bool startO throw new NotImplementedException(); } + public IGeometryImpl GetWidenedGeometry(IPen pen) + { + throw new NotImplementedException(); + } + public Rect Bounds => _rect; public double ContourLength => (_rect.Width + _rect.Height) * 2; public IGeometryImpl SourceGeometry { get; } diff --git a/src/Consolonia.Core/Drawing/RenderTarget.cs b/src/Consolonia.Core/Drawing/RenderTarget.cs index 72fede23..40d56366 100644 --- a/src/Consolonia.Core/Drawing/RenderTarget.cs +++ b/src/Consolonia.Core/Drawing/RenderTarget.cs @@ -70,13 +70,15 @@ void IDrawingContextLayerImpl.Blit(IDrawingContextImpl context) bool IDrawingContextLayerImpl.CanBlit => true; - public IDrawingContextImpl CreateDrawingContext() + public bool IsCorrupted => false; + + public IDrawingContextImpl CreateDrawingContext(bool useScaledDrawing) { + if (useScaledDrawing) + throw new NotImplementedException("Consolonia doesn't support useScaledDrawing"); return new DrawingContextImpl(_consoleWindow); } - public bool IsCorrupted => false; - private void OnResized(Size size, WindowResizeReason reason) { @@ -147,11 +149,6 @@ private void RenderToDevice() } } - public IDrawingContextImpl CreateDrawingContext(bool useScaledDrawing) - { - return new DrawingContextImpl(_consoleWindow); - } - private struct FlushingBuffer { //todo: move class out diff --git a/src/Consolonia.Core/Infrastructure/ArrowsAndKeyboardNavigationHandler.cs b/src/Consolonia.Core/Infrastructure/ArrowsAndKeyboardNavigationHandler.cs index a2562213..dd4777fc 100644 --- a/src/Consolonia.Core/Infrastructure/ArrowsAndKeyboardNavigationHandler.cs +++ b/src/Consolonia.Core/Infrastructure/ArrowsAndKeyboardNavigationHandler.cs @@ -2,6 +2,7 @@ using System.Linq; using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.VisualTree; using Consolonia.Core.InternalHelpers; @@ -15,9 +16,9 @@ public class ArrowsAndKeyboardNavigationHandler : IKeyboardNavigationHandler //todo: check XTFocus https://github.com/jinek/Consolonia/issues/105#issuecomment-2089015880 private IInputRoot _owner; - public ArrowsAndKeyboardNavigationHandler(IKeyboardNavigationHandler keyboardNavigationHandler) + public ArrowsAndKeyboardNavigationHandler() { - _keyboardNavigationHandler = keyboardNavigationHandler; + _keyboardNavigationHandler = new KeyboardNavigationHandler(); } public void SetOwner(IInputRoot owner) @@ -124,8 +125,26 @@ private void OnKeyDown(object sender, KeyEventArgs e) { if (e.Handled) return; + if (e.Source is MenuItem) return; + + if (e.Key == Key.Escape) + { + // if there is a overlay popup, close it + OverlayPopupHost overlay = sender as OverlayPopupHost ?? + ((Visual)sender).FindDescendantOfType(); + if (overlay != null) + { + // it will have a popup as the parent. + var popup = overlay.Parent as Popup; + if (popup != null) + popup.Close(); + e.Handled = true; + return; + } + } + //see FocusManager.GetFocusManager - IInputElement current = TopLevel.GetTopLevel((Visual)sender)!.FocusManager!.GetFocusedElement(); + IInputElement current = TopLevel.GetTopLevel((Visual)sender)?.FocusManager?.GetFocusedElement(); if (e.KeyModifiers != KeyModifiers.None) return; diff --git a/src/Consolonia.Core/Infrastructure/ConsoleWindow.cs b/src/Consolonia.Core/Infrastructure/ConsoleWindow.cs index 77770287..a94328c0 100644 --- a/src/Consolonia.Core/Infrastructure/ConsoleWindow.cs +++ b/src/Consolonia.Core/Infrastructure/ConsoleWindow.cs @@ -6,9 +6,10 @@ using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.Platform; using Avalonia.Input; +using Avalonia.Input.Platform; using Avalonia.Input.Raw; -using Avalonia.Input.TextInput; using Avalonia.Platform; using Avalonia.Platform.Storage; using Avalonia.Rendering.Composition; @@ -73,12 +74,21 @@ public IPopupImpl CreatePopup() public void SetTransparencyLevelHint(IReadOnlyList transparencyLevels) { - throw new NotImplementedException("Consider this"); + Debug.WriteLine($"ConsoleWindow.SetTransparencyLevelHint({transparencyLevels}) called, not implemented"); } public void SetFrameThemeVariant(PlatformThemeVariant themeVariant) { - //todo: + //todo: Light or dark + switch (themeVariant) + { + case PlatformThemeVariant.Dark: + Debug.WriteLine($"ConsoleWindow.SetFrameThemeVariant({themeVariant}) called, not implemented"); + break; + case PlatformThemeVariant.Light: + Debug.WriteLine($"ConsoleWindow.SetFrameThemeVariant({themeVariant}) called, not implemented"); + break; + } } public Size ClientSize @@ -131,7 +141,8 @@ public void Activate() public void SetTopmost(bool value) { - throw new NotImplementedException(); + // todo + Debug.WriteLine($"ConsoleWindow.SetTopmost({value}) called, not implemented"); } public double DesktopScaling => 1d; @@ -149,7 +160,6 @@ public void SetTopmost(bool value) public Size MaxAutoSizeHint { get; } // ReSharper disable once UnassignedGetOnlyAutoProperty todo: what is this property - public IScreenImpl Screen => null!; public void SetTitle(string title) { @@ -181,7 +191,7 @@ public void ShowTaskbarIcon(bool value) public void CanResize(bool value) { - throw new NotImplementedException(); + // todo, enable/dsiable resizing of window } public void BeginMoveDrag(PointerPressedEventArgs e) @@ -207,22 +217,22 @@ public void Move(PixelPoint point) public void SetMinMaxSize(Size minSize, Size maxSize) { - throw new NotImplementedException(); + //throw new NotImplementedException(); } public void SetExtendClientAreaToDecorationsHint(bool extendIntoClientAreaHint) { - throw new NotImplementedException(); + // we don't support this, we can ignore } public void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints) { - throw new NotImplementedException(); + // we don't support this, we can ignore } public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight) { - throw new NotImplementedException(); + // we don't support this, we can ignore } public WindowState WindowState { get; set; } @@ -245,12 +255,31 @@ public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight) public object TryGetFeature(Type featureType) { - if (featureType == typeof(ISystemNavigationManagerImpl)) - return null; - if (featureType == typeof(ITextInputMethodImpl)) return null; if (featureType == typeof(IStorageProvider)) return AvaloniaLocator.Current.GetService(); - throw new NotImplementedException("Consider this"); + + if (featureType == typeof(IInsetsManager)) + // IInsetsManager doesn't apply to console applications. + return null; + + if (featureType == typeof(IClipboard)) + { + var clipboard = AvaloniaLocator.CurrentMutable.GetService(); + if (clipboard != null) + return clipboard; + } + + // TODO ISystemNavigationManagerImpl should be implemented to handle BACK navigation between pages of controls like mobile apps do. + // TODO ITextInputMethodImplshould be implemented to handle text IME input + Debug.WriteLine($"Missing Feature: {featureType.Name} is not implemented but someone is asking for it!"); + return null; + } + + public void GetWindowsZOrder(Span windows, Span zOrder) + { + // In console mode, all windows are considered to be at the same z-order level + for (int i = 0; i < zOrder.Length; i++) + zOrder[i] = 0; } // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources diff --git a/src/Consolonia.Core/Infrastructure/ConsoloniaApplication.cs b/src/Consolonia.Core/Infrastructure/ConsoloniaApplication.cs index 86f511b9..cda946f7 100644 --- a/src/Consolonia.Core/Infrastructure/ConsoloniaApplication.cs +++ b/src/Consolonia.Core/Infrastructure/ConsoloniaApplication.cs @@ -12,9 +12,9 @@ public class ConsoloniaApplication : Application public override void RegisterServices() { base.RegisterServices(); - var keyboardNavigationHandler = AvaloniaLocator.Current.GetRequiredService(); + AvaloniaLocator.CurrentMutable.Bind() - .ToConstant(new ArrowsAndKeyboardNavigationHandler(keyboardNavigationHandler)); + .ToTransient(); } public override void OnFrameworkInitializationCompleted() diff --git a/src/Consolonia.Core/Infrastructure/ConsoloniaPlatform.cs b/src/Consolonia.Core/Infrastructure/ConsoloniaPlatform.cs index 898361b3..8909e8c9 100644 --- a/src/Consolonia.Core/Infrastructure/ConsoloniaPlatform.cs +++ b/src/Consolonia.Core/Infrastructure/ConsoloniaPlatform.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics; +using System.Reflection; using Avalonia; using Avalonia.Controls.Platform; using Avalonia.Input; @@ -32,6 +33,11 @@ public ITrayIconImpl CreateTrayIcon() throw new NotImplementedException(); } + public ITopLevelImpl CreateEmbeddableTopLevel() + { + throw new NotImplementedException(); + } + public void Initialize() { NotSupported += InternalIgnore; @@ -58,6 +64,29 @@ public void Initialize() //.Bind().ToConstant(new PlatformSettingsStub()) //.Bind().ToConstant(new GtkSystemDialog()) /*.Bind().ToConstant(new LinuxMountedVolumeInfoProvider())*/; + + if (OperatingSystem.IsWindows()) + { + AvaloniaLocator.CurrentMutable.Bind() + .ToFunc(() => + { + Assembly assembly = Assembly.Load("Avalonia.Win32"); + ArgumentNullException.ThrowIfNull(assembly, "Avalonia.Win32"); + Type type = assembly.GetType(assembly.GetName().Name + ".ClipboardImpl"); + ArgumentNullException.ThrowIfNull(type, "ClipboardImpl"); + var clipboard = Activator.CreateInstance(type) as IClipboard; + ArgumentNullException.ThrowIfNull(clipboard, nameof(clipboard)); + return clipboard; + }); + } + else if (OperatingSystem.IsMacOS()) + { + // TODO: Implement or reuse MacOS clipboard + } + else if (OperatingSystem.IsLinux()) + { + // TODO: Implement or reuse X11 Clipboard + } } [DebuggerStepThrough] diff --git a/src/Consolonia.Core/Text/FontManagerImpl.cs b/src/Consolonia.Core/Text/FontManagerImpl.cs index fbf44abd..a77de5c1 100644 --- a/src/Consolonia.Core/Text/FontManagerImpl.cs +++ b/src/Consolonia.Core/Text/FontManagerImpl.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using Avalonia.Media; @@ -40,6 +41,12 @@ public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeigh return true; } + public bool TryCreateGlyphTypeface(Stream stream, FontSimulations fontSimulations, + [NotNullWhen(true)] out IGlyphTypeface glyphTypeface) + { + throw new NotImplementedException(); + } + public bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface) { throw new NotImplementedException(); diff --git a/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryDataGrid.axaml.cs b/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryDataGrid.axaml.cs index 8b5bfea7..762f0076 100644 --- a/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryDataGrid.axaml.cs +++ b/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryDataGrid.axaml.cs @@ -67,7 +67,7 @@ public GalleryDataGrid() // ReSharper disable once MemberCanBeMadeStatic.Local private void DataGrid1_LoadingRow(object sender, DataGridRowEventArgs e) { - e.Row.Header = e.Row.GetIndex() + 1; + e.Row.Header = e.Row.Index + 1; } private class ReversedStringComparer : IComparer, IComparer diff --git a/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryGradientBrush.axaml.cs b/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryGradientBrush.axaml.cs index ed372284..a973b412 100644 --- a/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryGradientBrush.axaml.cs +++ b/src/Consolonia.Gallery/Gallery/GalleryViews/GalleryGradientBrush.axaml.cs @@ -31,7 +31,8 @@ private void Radial_Click(object sender, Avalonia.Interactivity.RoutedEventArgs { Center = new RelativePoint(0.5, 0.5, RelativeUnit.Relative), GradientOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative), - Radius = 0.5, + RadiusX = RelativeScalar.Parse("50%"), + RadiusY = RelativeScalar.Parse("50%"), GradientStops = new GradientStops { new GradientStop { Color = Colors.Black, Offset = 0 }, diff --git a/src/Consolonia.Gallery/Gallery/GalleryViews/SomeDialogWindow.axaml b/src/Consolonia.Gallery/Gallery/GalleryViews/SomeDialogWindow.axaml index 25799465..8f94ebb5 100644 --- a/src/Consolonia.Gallery/Gallery/GalleryViews/SomeDialogWindow.axaml +++ b/src/Consolonia.Gallery/Gallery/GalleryViews/SomeDialogWindow.axaml @@ -28,7 +28,7 @@ _____/ :--' Bill Ames