From ee6a57b7847f14a4d70e08045783ffd7ee418fa5 Mon Sep 17 00:00:00 2001 From: Markus Ewald Date: Wed, 9 Jul 2025 23:10:54 +0200 Subject: [PATCH] Restored previous attempt at IndexOf(), but it's broken either way --- .../AsyncVirtualObservableReadOnlyList.cs | 25 +- Source/IActiveWindowTracker.cs | 70 ++-- Source/IWindowManager.cs | 228 +++++------ Source/Messages/AvaloniaMessagePresenter.cs | 376 +++++++++--------- 4 files changed, 354 insertions(+), 345 deletions(-) diff --git a/Source/Collections/AsyncVirtualObservableReadOnlyList.cs b/Source/Collections/AsyncVirtualObservableReadOnlyList.cs index cc4781e..2871bdd 100644 --- a/Source/Collections/AsyncVirtualObservableReadOnlyList.cs +++ b/Source/Collections/AsyncVirtualObservableReadOnlyList.cs @@ -289,7 +289,19 @@ namespace Nuclex.Avalonia.Collections { /// Item whose index will be determined /// The index of the item in the list or -1 if not found public int IndexOf(TItem item) { - return this.typedList.IndexOf(item); + requireCount(); + requireAllPages(); + + // TODO: this won't work, it will compare the placeholder items :-/ + + IComparer itemComparer = Comparer.Default; + for(int index = 0; index < this.assumedCount.Value; ++index) { + if(itemComparer.Compare(this.typedList[index], item) == 0) { + return index; + } + } + + return -1; } /// Inserts an item into the list at the specified index @@ -344,10 +356,8 @@ namespace Nuclex.Avalonia.Collections { /// Item the list will be checked for /// True if the list contains the specified items public bool Contains(TItem item) { - requireCount(); - requireAllPages(); - - return this.typedList.Contains(item); + // TODO: this won't work, it will compare the placeholder items :-/ + return (IndexOf(item) != -1); } /// Copies the contents of the list into an array @@ -359,14 +369,13 @@ namespace Nuclex.Avalonia.Collections { requireCount(); requireAllPages(); + // TODO: this won't work, it will copy the placeholder items :-/ this.typedList.CopyTo(array, arrayIndex); } /// Total number of items in the list public int Count { - get { - return requireCount(); - } + get { return requireCount(); } } /// Whether the list is a read-only list diff --git a/Source/IActiveWindowTracker.cs b/Source/IActiveWindowTracker.cs index 82d341c..ecb899d 100644 --- a/Source/IActiveWindowTracker.cs +++ b/Source/IActiveWindowTracker.cs @@ -1,35 +1,35 @@ -#region Apache License 2.0 -/* -Nuclex Foundation libraries for .NET -Copyright (C) 2002-2025 Markus Ewald / Nuclex Development Labs - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -#endregion // Apache License 2.0 - -using System; - -using Avalonia.Controls; - -namespace Nuclex.Avalonia { - - /// Allows the currently active top level window to be looked up - public interface IActiveWindowTracker { - - /// The currently active top-level or modal window - Window? ActiveWindow { get; } - - } - -} // namespace Nuclex.Avalonia - +#region Apache License 2.0 +/* +Nuclex Foundation libraries for .NET +Copyright (C) 2002-2025 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; + +using Avalonia.Controls; + +namespace Nuclex.Avalonia { + + /// Allows the currently active top level window to be looked up + public interface IActiveWindowTracker { + + /// The currently active top-level or modal window + Window? ActiveWindow { get; } + + } + +} // namespace Nuclex.Avalonia + diff --git a/Source/IWindowManager.cs b/Source/IWindowManager.cs index e050f52..d68ed93 100644 --- a/Source/IWindowManager.cs +++ b/Source/IWindowManager.cs @@ -1,114 +1,114 @@ -#region Apache License 2.0 -/* -Nuclex Foundation libraries for .NET -Copyright (C) 2002-2025 Markus Ewald / Nuclex Development Labs - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -#endregion // Apache License 2.0 - -using System; -using System.Threading.Tasks; - -using Avalonia.Controls; - -namespace Nuclex.Avalonia { - - /// Manages open windows and connecting them to view models - public interface IWindowManager : IActiveWindowTracker { - - /// Opens a view as a new root window of the application - /// - /// Type of view model a root window will be opened for - /// - /// - /// View model a window will be opened for. If null, the view model will be - /// created as well (unless the dialog already specifies one as a resource) - /// - /// - /// Whether the view model should be disposed when the view is closed - /// - /// The window that has been opened by the window manager - Window OpenRoot( - TViewModel? viewModel = null, bool disposeOnClose = true - ) where TViewModel : class; - - /// Displays a view as a modal window - /// - /// Type of the view model for which a view will be displayed - /// - /// - /// View model a modal window will be displayed for. If null, the view model will - /// be created as well (unless the dialog already specifies one as a resource) - /// - /// - /// Whether the view model should be disposed when the view is closed - /// - /// The return value of the modal window - Task ShowModalAsync( - TViewModel? viewModel = null, bool disposeOnClose = false - ) where TViewModel : class; - - /// Displays a view as a modal window - /// - /// Type of result the modal dialog will return to the caller - /// - /// - /// Type of the view model for which a view will be displayed - /// - /// - /// View model a modal window will be displayed for. If null, the view model will - /// be created as well (unless the dialog already specifies one as a resource) - /// - /// - /// Whether the view model should be disposed when the view is closed - /// - /// The return value of the modal window - Task ShowModalAsync( - TViewModel? viewModel = null, bool disposeOnClose = true - ) where TViewModel : class; - - /// Creates the view for the specified view model - /// - /// Type of view model for which a view will be created - /// - /// - /// View model a view will be created for. If null, the view model will be - /// created as well (unless the dialog already specifies one as a resource) - /// - /// The view for the specified view model - Control CreateView(TViewModel? viewModel = null) - where TViewModel : class; - - /// Creates a view model without a matching view - /// Type of view model that will be created - /// The new view model - /// - /// - /// This is useful if a view model needs to create child view models (i.e. paged container - /// and wants to ensure the same dependency injector (if any) if used as the window - /// manager uses for other view models it creates. - /// - /// - /// This way, view models can set up their child view models without having to immediately - /// bind a view to them. Later on, views can use the window manager to create a matching - /// child view and store it in a container. - /// - /// - TViewModel CreateViewModel() - where TViewModel : class; - - } - -} // namespace Nuclex.Avalonia - +#region Apache License 2.0 +/* +Nuclex Foundation libraries for .NET +Copyright (C) 2002-2025 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Threading.Tasks; + +using Avalonia.Controls; + +namespace Nuclex.Avalonia { + + /// Manages open windows and connecting them to view models + public interface IWindowManager : IActiveWindowTracker { + + /// Opens a view as a new root window of the application + /// + /// Type of view model a root window will be opened for + /// + /// + /// View model a window will be opened for. If null, the view model will be + /// created as well (unless the dialog already specifies one as a resource) + /// + /// + /// Whether the view model should be disposed when the view is closed + /// + /// The window that has been opened by the window manager + Window OpenRoot( + TViewModel? viewModel = null, bool disposeOnClose = true + ) where TViewModel : class; + + /// Displays a view as a modal window + /// + /// Type of the view model for which a view will be displayed + /// + /// + /// View model a modal window will be displayed for. If null, the view model will + /// be created as well (unless the dialog already specifies one as a resource) + /// + /// + /// Whether the view model should be disposed when the view is closed + /// + /// The return value of the modal window + Task ShowModalAsync( + TViewModel? viewModel = null, bool disposeOnClose = false + ) where TViewModel : class; + + /// Displays a view as a modal window + /// + /// Type of result the modal dialog will return to the caller + /// + /// + /// Type of the view model for which a view will be displayed + /// + /// + /// View model a modal window will be displayed for. If null, the view model will + /// be created as well (unless the dialog already specifies one as a resource) + /// + /// + /// Whether the view model should be disposed when the view is closed + /// + /// The return value of the modal window + Task ShowModalAsync( + TViewModel? viewModel = null, bool disposeOnClose = true + ) where TViewModel : class; + + /// Creates the view for the specified view model + /// + /// Type of view model for which a view will be created + /// + /// + /// View model a view will be created for. If null, the view model will be + /// created as well (unless the dialog already specifies one as a resource) + /// + /// The view for the specified view model + Control CreateView(TViewModel? viewModel = null) + where TViewModel : class; + + /// Creates a view model without a matching view + /// Type of view model that will be created + /// The new view model + /// + /// + /// This is useful if a view model needs to create child view models (i.e. paged container + /// and wants to ensure the same dependency injector (if any) if used as the window + /// manager uses for other view models it creates. + /// + /// + /// This way, view models can set up their child view models without having to immediately + /// bind a view to them. Later on, views can use the window manager to create a matching + /// child view and store it in a container. + /// + /// + TViewModel CreateViewModel() + where TViewModel : class; + + } + +} // namespace Nuclex.Avalonia + diff --git a/Source/Messages/AvaloniaMessagePresenter.cs b/Source/Messages/AvaloniaMessagePresenter.cs index e7c181b..af5345b 100644 --- a/Source/Messages/AvaloniaMessagePresenter.cs +++ b/Source/Messages/AvaloniaMessagePresenter.cs @@ -1,188 +1,188 @@ -#region Apache License 2.0 -/* -Nuclex Foundation libraries for .NET -Copyright (C) 2002-2025 Markus Ewald / Nuclex Development Labs - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -#endregion // Apache License 2.0 - -using System; -using System.Threading.Tasks; -using System.Diagnostics; -using System.Runtime.InteropServices; -using System.Runtime.Versioning; -#if NET6_0_OR_GREATER -using System.Runtime.Versioning; -#endif - -using Avalonia.Controls; - -using MessageBoxIcon = MsBox.Avalonia.Enums.Icon; -using MessageBoxButtons = MsBox.Avalonia.Enums.ButtonEnum; -using MessageDialogResult = MsBox.Avalonia.Enums.ButtonResult; - -namespace Nuclex.Avalonia.Messages { - - /// Uses Avalonia to display message boxes - public class AvaloniaMessagePresenter : IMessageService { - - #region class MessageScope - - /// Triggers the message displayed and acknowledged events - private class MessageScope : IDisposable { - - /// - /// Initializes a new message scope, triggering the message displayed event - /// - /// Message service the scope belongs to - /// Image of the message being displayed - /// Text contained in the message being displayed - public MessageScope( - AvaloniaMessagePresenter self, MessageBoxIcon image, MessageText text - ) { - EventHandler? messageDisplayed = self.MessageDisplaying; - if(messageDisplayed != null) { - messageDisplayed(this, new MessageEventArgs(image, text)); - } - - this.self = self; - } - - /// Triggers the message acknowledged event - public void Dispose() { - EventHandler? messageAcknowledged = self.MessageAcknowledged; - if(messageAcknowledged != null) { - messageAcknowledged(this, EventArgs.Empty); - } - } - - /// Message service the scope belongs to - private AvaloniaMessagePresenter self; - - } - - #endregion // class MessageScope - - /// Triggered when a message is displayed to the user - public event EventHandler? MessageDisplaying; - - /// Triggered when the user has acknowledged the current message - public event EventHandler? MessageAcknowledged; - - /// Initializes a new Avalonia message service - /// Used to determine the current top-level window - public AvaloniaMessagePresenter(IActiveWindowTracker tracker) { - this.tracker = tracker; - } - - /// Asks the user a question that can be answered via several buttons - /// Image that will be shown on the message box - /// Text that will be shown to the user - /// Buttons available for the user to click on - /// The button the user has clicked on - public Task ShowQuestionAsync( - MessageBoxIcon image, MessageText text, MessageBoxButtons buttons - ) { - using(var scope = new MessageScope(this, image, text)) { - MsBox.Avalonia.Base.IMsBox messageBox = ( - MsBox.Avalonia.MessageBoxManager.GetMessageBoxStandard( - new MsBox.Avalonia.Dto.MessageBoxStandardParams() { - ContentTitle = text.Caption, - ContentHeader = text.Message, - ContentMessage = text.Details ?? string.Empty, - ButtonDefinitions = buttons, - Icon = image, - WindowStartupLocation = WindowStartupLocation.CenterOwner - } - ) - ); - return messageBox.ShowAsync(); // TODO: Make modal to current or main window - } - } - - /// Displays a notification to the user - /// Image that will be shown on the message bx - /// Text that will be shown to the user - public Task ShowNotificationAsync(MessageBoxIcon image, MessageText text) { - using(var scope = new MessageScope(this, image, text)) { - MsBox.Avalonia.Base.IMsBox messageBox = ( - MsBox.Avalonia.MessageBoxManager.GetMessageBoxStandard( - new MsBox.Avalonia.Dto.MessageBoxStandardParams() { - ContentTitle = text.Caption, - ContentHeader = text.Message, - ContentMessage = text.Details ?? string.Empty, - ButtonDefinitions = MessageBoxButtons.Ok, - Icon = image, - WindowStartupLocation = WindowStartupLocation.CenterOwner - } - ) - ); - - Window? activeWindow = this.tracker.ActiveWindow; - if(activeWindow == null) { - return messageBox.ShowAsync(); - } else { - //return messageBox.ShowAsPopupAsync(activeWindow); - return messageBox.ShowWindowDialogAsync(activeWindow); - } - } - } - - /// Reports an error using the system's message box functions - /// Title of the message box - /// Message text that will be displayed - public static void FallbackReportError(string title, string message) { - // TODO: Escape quotes for the command-line tools - // TODO: Wait for the child process to exit so display is certain - - if(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - MessageBoxW(IntPtr.Zero, message, title, MB_OK | MB_ICONEXCLAMATION); - } else if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - Process.Start("zenity", $"--error --title=\"{title}\" --text=\"{message}\""); - } else if(RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - Process.Start("osascript", $"-e 'display dialog \"{message}\" with title \"{title}\" with icon stop'"); - } - } - - /// Windows only: display a message box with an OK button -#if NET6_0_OR_GREATER - [SupportedOSPlatform("windows")] -#endif - private const uint MB_OK = 0x00000000; - - /// Windows only: display a message box with an Exclamation icon -#if NET6_0_OR_GREATER - [SupportedOSPlatform("windows")] -#endif - private const uint MB_ICONEXCLAMATION = 0x00000030; - - /// Windows only: displays a native Windows message box - /// Handle of the window that owns the message box - /// Text that should be in the message box - /// Caption or window title of the message box - /// Which icons and buttons that message box should have - /// How the user closed the message box and which button they clicked - [DllImport("user32.dll", CharSet = CharSet.Unicode)] -#if NET6_0_OR_GREATER - [SupportedOSPlatform("windows")] -#endif - private static extern int MessageBoxW(IntPtr parentWindowHandle, string text, string caption, uint type); - - // Provides the currently active top-level window - private IActiveWindowTracker tracker; - - } - -} // namespace Nuclex.Avalonia.Messages - +#region Apache License 2.0 +/* +Nuclex Foundation libraries for .NET +Copyright (C) 2002-2025 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Threading.Tasks; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +#if NET6_0_OR_GREATER +using System.Runtime.Versioning; +#endif + +using Avalonia.Controls; + +using MessageBoxIcon = MsBox.Avalonia.Enums.Icon; +using MessageBoxButtons = MsBox.Avalonia.Enums.ButtonEnum; +using MessageDialogResult = MsBox.Avalonia.Enums.ButtonResult; + +namespace Nuclex.Avalonia.Messages { + + /// Uses Avalonia to display message boxes + public class AvaloniaMessagePresenter : IMessageService { + + #region class MessageScope + + /// Triggers the message displayed and acknowledged events + private class MessageScope : IDisposable { + + /// + /// Initializes a new message scope, triggering the message displayed event + /// + /// Message service the scope belongs to + /// Image of the message being displayed + /// Text contained in the message being displayed + public MessageScope( + AvaloniaMessagePresenter self, MessageBoxIcon image, MessageText text + ) { + EventHandler? messageDisplayed = self.MessageDisplaying; + if(messageDisplayed != null) { + messageDisplayed(this, new MessageEventArgs(image, text)); + } + + this.self = self; + } + + /// Triggers the message acknowledged event + public void Dispose() { + EventHandler? messageAcknowledged = self.MessageAcknowledged; + if(messageAcknowledged != null) { + messageAcknowledged(this, EventArgs.Empty); + } + } + + /// Message service the scope belongs to + private AvaloniaMessagePresenter self; + + } + + #endregion // class MessageScope + + /// Triggered when a message is displayed to the user + public event EventHandler? MessageDisplaying; + + /// Triggered when the user has acknowledged the current message + public event EventHandler? MessageAcknowledged; + + /// Initializes a new Avalonia message service + /// Used to determine the current top-level window + public AvaloniaMessagePresenter(IActiveWindowTracker tracker) { + this.tracker = tracker; + } + + /// Asks the user a question that can be answered via several buttons + /// Image that will be shown on the message box + /// Text that will be shown to the user + /// Buttons available for the user to click on + /// The button the user has clicked on + public Task ShowQuestionAsync( + MessageBoxIcon image, MessageText text, MessageBoxButtons buttons + ) { + using(var scope = new MessageScope(this, image, text)) { + MsBox.Avalonia.Base.IMsBox messageBox = ( + MsBox.Avalonia.MessageBoxManager.GetMessageBoxStandard( + new MsBox.Avalonia.Dto.MessageBoxStandardParams() { + ContentTitle = text.Caption, + ContentHeader = text.Message, + ContentMessage = text.Details ?? string.Empty, + ButtonDefinitions = buttons, + Icon = image, + WindowStartupLocation = WindowStartupLocation.CenterOwner + } + ) + ); + return messageBox.ShowAsync(); // TODO: Make modal to current or main window + } + } + + /// Displays a notification to the user + /// Image that will be shown on the message bx + /// Text that will be shown to the user + public Task ShowNotificationAsync(MessageBoxIcon image, MessageText text) { + using(var scope = new MessageScope(this, image, text)) { + MsBox.Avalonia.Base.IMsBox messageBox = ( + MsBox.Avalonia.MessageBoxManager.GetMessageBoxStandard( + new MsBox.Avalonia.Dto.MessageBoxStandardParams() { + ContentTitle = text.Caption, + ContentHeader = text.Message, + ContentMessage = text.Details ?? string.Empty, + ButtonDefinitions = MessageBoxButtons.Ok, + Icon = image, + WindowStartupLocation = WindowStartupLocation.CenterOwner + } + ) + ); + + Window? activeWindow = this.tracker.ActiveWindow; + if(activeWindow == null) { + return messageBox.ShowAsync(); + } else { + //return messageBox.ShowAsPopupAsync(activeWindow); + return messageBox.ShowWindowDialogAsync(activeWindow); + } + } + } + + /// Reports an error using the system's message box functions + /// Title of the message box + /// Message text that will be displayed + public static void FallbackReportError(string title, string message) { + // TODO: Escape quotes for the command-line tools + // TODO: Wait for the child process to exit so display is certain + + if(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { + MessageBoxW(IntPtr.Zero, message, title, MB_OK | MB_ICONEXCLAMATION); + } else if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { + Process.Start("zenity", $"--error --title=\"{title}\" --text=\"{message}\""); + } else if(RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { + Process.Start("osascript", $"-e 'display dialog \"{message}\" with title \"{title}\" with icon stop'"); + } + } + + /// Windows only: display a message box with an OK button +#if NET6_0_OR_GREATER + [SupportedOSPlatform("windows")] +#endif + private const uint MB_OK = 0x00000000; + + /// Windows only: display a message box with an Exclamation icon +#if NET6_0_OR_GREATER + [SupportedOSPlatform("windows")] +#endif + private const uint MB_ICONEXCLAMATION = 0x00000030; + + /// Windows only: displays a native Windows message box + /// Handle of the window that owns the message box + /// Text that should be in the message box + /// Caption or window title of the message box + /// Which icons and buttons that message box should have + /// How the user closed the message box and which button they clicked + [DllImport("user32.dll", CharSet = CharSet.Unicode)] +#if NET6_0_OR_GREATER + [SupportedOSPlatform("windows")] +#endif + private static extern int MessageBoxW(IntPtr parentWindowHandle, string text, string caption, uint type); + + // Provides the currently active top-level window + private IActiveWindowTracker tracker; + + } + +} // namespace Nuclex.Avalonia.Messages +