diff --git a/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.xml b/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.xml index 535504c04..7dec575f1 100644 --- a/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.xml +++ b/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.xml @@ -68,6 +68,9 @@ Gets or sets a list of LEDs to highlight + + + @@ -295,8 +298,8 @@ Represents a layer brush registered through - or - + or + @@ -349,7 +352,7 @@ Gets the type of event arguments this event triggers and that must be displayed as children - + @@ -389,7 +392,7 @@ - + @@ -417,7 +420,7 @@ - + @@ -453,7 +456,7 @@ Gets a list of child view models that visualize the elements in the list - + @@ -474,7 +477,7 @@ Gets the value of the property that is being visualized - + @@ -504,7 +507,7 @@ Gets the view model used to display the display value - + @@ -587,7 +590,7 @@ Gets a user-friendly representation of the - + Updates the datamodel and if in an parent, any children @@ -837,6 +840,104 @@ Represents a service provided by the Artemis Shared UI library + + + A service for UI related data model tasks + + + + + Gets a read-only list of all registered data model editors + + + + + Gets a read-only list of all registered data model displays + + + + + Creates a data model visualization view model for the main data model + + + A data model visualization view model containing all data model expansions and modules that expand the main + data model + + + + + Creates a data model visualization view model for the data model of the provided plugin feature + + The modules to create the data model visualization view model for + + Whether or not also to include the main data model (and therefore any modules marked + as ) + + A data model visualization view model containing the data model of the provided feature + + + + Updates the children of the provided main data model visualization, removing disabled children and adding newly + enabled children + + + + + Registers a new data model editor + + The type of the editor + The plugin this editor belongs to + A collection of extra types this editor supports + A registration that can be used to remove the editor + + + + Registers a new data model display + + The type of the display + The plugin this display belongs to + A registration that can be used to remove the display + + + + Removes a data model editor + + + The registration of the editor as returned by + + + + + Removes a data model display + + + The registration of the display as returned by + + + + + Creates the most appropriate display view model for the provided that can display + a value + + The type of data model property to find a display view model for + The description of the data model property + + If , a simple .ToString() display view model will be + returned if nothing else is found + + The most appropriate display view model for the provided + + + + Creates the most appropriate input view model for the provided that allows + inputting a value + + The type of data model property to find a display view model for + The description of the data model property + The initial value to show in the input + A function to call whenever the input was updated (submitted or not) + The most appropriate input view model for the provided + Creates a view model instance of type and shows its corresponding View as a window @@ -895,103 +996,79 @@ The builder that can be used to configure the dialog - + - A service for UI related data model tasks + Creates a content dialog, use the fluent API to configure it + + The builder that can be used to configure the dialog + + + + Gets the current window of the application + + The current window of the application + + + + Represents a class that provides the main window, so that can control the state of + the main window. - + - Gets a read-only list of all registered data model editors + Gets a boolean indicating whether the main window is currently open - + - Gets a read-only list of all registered data model displays + Opens the main window - + - Creates a data model visualization view model for the main data model - - - A data model visualization view model containing all data model expansions and modules that expand the main - data model - - - - - Creates a data model visualization view model for the data model of the provided plugin feature - - The modules to create the data model visualization view model for - - Whether or not also to include the main data model (and therefore any modules marked - as ) - - A data model visualization view model containing the data model of the provided feature - - - - Updates the children of the provided main data model visualization, removing disabled children and adding newly - enabled children + Closes the main window - + - Registers a new data model editor + Occurs when the main window has been opened - The type of the editor - The plugin this editor belongs to - A collection of extra types this editor supports - A registration that can be used to remove the editor - + - Registers a new data model display + Occurs when the main window has been closed - The type of the display - The plugin this display belongs to - A registration that can be used to remove the display - + - Removes a data model editor + Gets a boolean indicating whether the main window is currently open - - The registration of the editor as returned by - - + - Removes a data model display + Sets up the main window provider that controls the state of the main window - - The registration of the display as returned by - + The main window provider to use to control the state of the main window - + - Creates the most appropriate display view model for the provided that can display - a value + Opens the main window if it is not already open - The type of data model property to find a display view model for - The description of the data model property - - If , a simple .ToString() display view model will be - returned if nothing else is found - - The most appropriate display view model for the provided - + - Creates the most appropriate input view model for the provided that allows - inputting a value + Closes the main window if it is not already closed + + + + + Occurs when the main window has been opened + + + + + Occurs when the main window has been closed - The type of data model property to find a display view model for - The description of the data model property - The initial value to show in the input - A function to call whenever the input was updated (submitted or not) - The most appropriate input view model for the provided diff --git a/src/Artemis.UI/Screens/Splash/SplashViewModel.cs b/src/Artemis.UI/Screens/Splash/SplashViewModel.cs index f80543a79..cbcd5ffdb 100644 --- a/src/Artemis.UI/Screens/Splash/SplashViewModel.cs +++ b/src/Artemis.UI/Screens/Splash/SplashViewModel.cs @@ -19,7 +19,7 @@ namespace Artemis.UI.Screens.Splash { _coreService = coreService; _pluginManagementService = pluginManagementService; - Status = "Initializing Core"; + Status = "Initializing Core"; } public string Status diff --git a/src/Avalonia/Artemis.UI.Linux/App.axaml.cs b/src/Avalonia/Artemis.UI.Linux/App.axaml.cs index a054a252d..ea12e7cc6 100644 --- a/src/Avalonia/Artemis.UI.Linux/App.axaml.cs +++ b/src/Avalonia/Artemis.UI.Linux/App.axaml.cs @@ -1,5 +1,4 @@ using Avalonia; -using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; using Avalonia.Threading; using ReactiveUI; @@ -10,17 +9,14 @@ namespace Artemis.UI.Linux { public override void Initialize() { - ArtemisBootstrapper.Bootstrap(); + ArtemisBootstrapper.Bootstrap(this); RxApp.MainThreadScheduler = AvaloniaScheduler.Instance; AvaloniaXamlLoader.Load(this); } public override void OnFrameworkInitializationCompleted() { - if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) - ArtemisBootstrapper.ConfigureApplicationLifetime(desktop); - - base.OnFrameworkInitializationCompleted(); + ArtemisBootstrapper.Initialized(); } } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.MacOS/App.axaml.cs b/src/Avalonia/Artemis.UI.MacOS/App.axaml.cs index 597873852..fefca7aee 100644 --- a/src/Avalonia/Artemis.UI.MacOS/App.axaml.cs +++ b/src/Avalonia/Artemis.UI.MacOS/App.axaml.cs @@ -1,5 +1,4 @@ using Avalonia; -using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; using Avalonia.Threading; using ReactiveUI; @@ -10,17 +9,14 @@ namespace Artemis.UI.MacOS { public override void Initialize() { - ArtemisBootstrapper.Bootstrap(); + ArtemisBootstrapper.Bootstrap(this); RxApp.MainThreadScheduler = AvaloniaScheduler.Instance; AvaloniaXamlLoader.Load(this); } public override void OnFrameworkInitializationCompleted() { - if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) - ArtemisBootstrapper.ConfigureApplicationLifetime(desktop); - - base.OnFrameworkInitializationCompleted(); + ArtemisBootstrapper.Initialized(); } } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/DeviceVisualizer.cs b/src/Avalonia/Artemis.UI.Shared/Controls/DeviceVisualizer.cs index 2e6e832ea..9358e1e61 100644 --- a/src/Avalonia/Artemis.UI.Shared/Controls/DeviceVisualizer.cs +++ b/src/Avalonia/Artemis.UI.Shared/Controls/DeviceVisualizer.cs @@ -88,20 +88,6 @@ namespace Artemis.UI.Shared.Controls /// public event EventHandler? LedClicked; - protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) - { - _deviceImage?.Dispose(); - _deviceImage = null; - - if (Device != null) - { - Device.RgbDevice.PropertyChanged -= DevicePropertyChanged; - Device.DeviceUpdated -= DeviceUpdated; - } - - base.OnDetachedFromVisualTree(e); - } - /// /// Invokes the event /// @@ -216,6 +202,21 @@ namespace Artemis.UI.Shared.Controls #region Lifetime management + /// + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + _deviceImage?.Dispose(); + _deviceImage = null; + + if (Device != null) + { + Device.RgbDevice.PropertyChanged -= DevicePropertyChanged; + Device.DeviceUpdated -= DeviceUpdated; + } + + base.OnDetachedFromVisualTree(e); + } + /// protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) { diff --git a/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/DataModelVisualizationRegistration.cs b/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/DataModelVisualizationRegistration.cs index b67b4fb26..e867a2995 100644 --- a/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/DataModelVisualizationRegistration.cs +++ b/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/DataModelVisualizationRegistration.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using Artemis.Core; using Artemis.UI.Shared.Services; +using Artemis.UI.Shared.Services.Interfaces; namespace Artemis.UI.Shared.DataModelVisualization { diff --git a/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelEventViewModel.cs b/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelEventViewModel.cs index 93a3382c7..c6b3b1478 100644 --- a/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelEventViewModel.cs +++ b/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelEventViewModel.cs @@ -3,6 +3,7 @@ using System.Linq; using Artemis.Core; using Artemis.Core.Modules; using Artemis.UI.Shared.Services; +using Artemis.UI.Shared.Services.Interfaces; using ReactiveUI; namespace Artemis.UI.Shared.DataModelVisualization.Shared diff --git a/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListPropertiesViewModel.cs b/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListPropertiesViewModel.cs index 216c333ef..8bd21ea38 100644 --- a/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListPropertiesViewModel.cs +++ b/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListPropertiesViewModel.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using Artemis.UI.Shared.Services; +using Artemis.UI.Shared.Services.Interfaces; using ReactiveUI; namespace Artemis.UI.Shared.DataModelVisualization.Shared diff --git a/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListPropertyViewModel.cs b/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListPropertyViewModel.cs index 5ba602769..161b76cbe 100644 --- a/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListPropertyViewModel.cs +++ b/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListPropertyViewModel.cs @@ -1,5 +1,6 @@ using System; using Artemis.UI.Shared.Services; +using Artemis.UI.Shared.Services.Interfaces; using ReactiveUI; namespace Artemis.UI.Shared.DataModelVisualization.Shared diff --git a/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListViewModel.cs b/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListViewModel.cs index 71853705a..975a06295 100644 --- a/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListViewModel.cs +++ b/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListViewModel.cs @@ -4,6 +4,7 @@ using System.Collections.ObjectModel; using Artemis.Core; using Artemis.Core.Modules; using Artemis.UI.Shared.Services; +using Artemis.UI.Shared.Services.Interfaces; using ReactiveUI; namespace Artemis.UI.Shared.DataModelVisualization.Shared diff --git a/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelPropertiesViewModel.cs b/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelPropertiesViewModel.cs index 7cc7b4502..e162f0599 100644 --- a/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelPropertiesViewModel.cs +++ b/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelPropertiesViewModel.cs @@ -2,6 +2,7 @@ using Artemis.Core; using Artemis.Core.Modules; using Artemis.UI.Shared.Services; +using Artemis.UI.Shared.Services.Interfaces; using ReactiveUI; namespace Artemis.UI.Shared.DataModelVisualization.Shared diff --git a/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelPropertyViewModel.cs b/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelPropertyViewModel.cs index 004ac7083..f6215d7af 100644 --- a/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelPropertyViewModel.cs +++ b/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelPropertyViewModel.cs @@ -2,6 +2,7 @@ using Artemis.Core; using Artemis.Core.Modules; using Artemis.UI.Shared.Services; +using Artemis.UI.Shared.Services.Interfaces; using ReactiveUI; namespace Artemis.UI.Shared.DataModelVisualization.Shared diff --git a/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelVisualizationViewModel.cs b/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelVisualizationViewModel.cs index 80ba67563..aaa651957 100644 --- a/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelVisualizationViewModel.cs +++ b/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelVisualizationViewModel.cs @@ -7,6 +7,7 @@ using System.Text; using Artemis.Core; using Artemis.Core.Modules; using Artemis.UI.Shared.Services; +using Artemis.UI.Shared.Services.Interfaces; using ReactiveUI; namespace Artemis.UI.Shared.DataModelVisualization.Shared diff --git a/src/Avalonia/Artemis.UI.Shared/Services/DataModelUIService.cs b/src/Avalonia/Artemis.UI.Shared/Services/DataModelUIService.cs index 3afaf0d0d..8a3060d13 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/DataModelUIService.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/DataModelUIService.cs @@ -8,6 +8,7 @@ using Artemis.Core.Services; using Artemis.UI.Shared.DataModelVisualization; using Artemis.UI.Shared.DataModelVisualization.Shared; using Artemis.UI.Shared.DefaultTypes.DataModel.Display; +using Artemis.UI.Shared.Services.Interfaces; using Ninject; using Ninject.Parameters; diff --git a/src/Avalonia/Artemis.UI.Shared/Services/Interfaces/IDataModelUIService.cs b/src/Avalonia/Artemis.UI.Shared/Services/Interfaces/IDataModelUIService.cs index d2c798207..57e9b6214 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/Interfaces/IDataModelUIService.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/Interfaces/IDataModelUIService.cs @@ -4,9 +4,8 @@ using Artemis.Core; using Artemis.Core.Modules; using Artemis.UI.Shared.DataModelVisualization; using Artemis.UI.Shared.DataModelVisualization.Shared; -using Artemis.UI.Shared.Services.Interfaces; -namespace Artemis.UI.Shared.Services +namespace Artemis.UI.Shared.Services.Interfaces { /// /// A service for UI related data model tasks diff --git a/src/Avalonia/Artemis.UI.Shared/Services/Interfaces/IWindowService.cs b/src/Avalonia/Artemis.UI.Shared/Services/Interfaces/IWindowService.cs index 5c63797bb..a844a7c9c 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/Interfaces/IWindowService.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/Interfaces/IWindowService.cs @@ -65,8 +65,16 @@ namespace Artemis.UI.Shared.Services.Interfaces /// The builder that can be used to configure the dialog SaveFileDialogBuilder CreateSaveFileDialog(); + /// + /// Creates a content dialog, use the fluent API to configure it + /// + /// The builder that can be used to configure the dialog ContentDialogBuilder CreateContentDialog(); + /// + /// Gets the current window of the application + /// + /// The current window of the application Window GetCurrentWindow(); } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Services/MainWindowService/IMainWindowProvider.cs b/src/Avalonia/Artemis.UI.Shared/Services/MainWindowService/IMainWindowProvider.cs new file mode 100644 index 000000000..7ad9ef027 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Services/MainWindowService/IMainWindowProvider.cs @@ -0,0 +1,36 @@ +using System; + +namespace Artemis.UI.Shared.Services.MainWindowService +{ + /// + /// Represents a class that provides the main window, so that can control the state of + /// the main window. + /// + public interface IMainWindowProvider + { + /// + /// Gets a boolean indicating whether the main window is currently open + /// + bool IsMainWindowOpen { get; } + + /// + /// Opens the main window + /// + void OpenMainWindow(); + + /// + /// Closes the main window + /// + void CloseMainWindow(); + + /// + /// Occurs when the main window has been opened + /// + public event EventHandler? MainWindowOpened; + + /// + /// Occurs when the main window has been closed + /// + public event EventHandler? MainWindowClosed; + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Services/MainWindowService/IMainWindowService.cs b/src/Avalonia/Artemis.UI.Shared/Services/MainWindowService/IMainWindowService.cs new file mode 100644 index 000000000..8a42d73a9 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Services/MainWindowService/IMainWindowService.cs @@ -0,0 +1,39 @@ +using System; +using Artemis.UI.Shared.Services.Interfaces; + +namespace Artemis.UI.Shared.Services.MainWindowService +{ + public interface IMainWindowService : IArtemisSharedUIService + { + /// + /// Gets a boolean indicating whether the main window is currently open + /// + bool IsMainWindowOpen { get; } + + /// + /// Sets up the main window provider that controls the state of the main window + /// + /// The main window provider to use to control the state of the main window + void ConfigureMainWindowProvider(IMainWindowProvider mainWindowProvider); + + /// + /// Opens the main window if it is not already open + /// + void OpenMainWindow(); + + /// + /// Closes the main window if it is not already closed + /// + void CloseMainWindow(); + + /// + /// Occurs when the main window has been opened + /// + public event EventHandler? MainWindowOpened; + + /// + /// Occurs when the main window has been closed + /// + public event EventHandler? MainWindowClosed; + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Services/MainWindowService/MainWindowService.cs b/src/Avalonia/Artemis.UI.Shared/Services/MainWindowService/MainWindowService.cs new file mode 100644 index 000000000..d2ee398e7 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Services/MainWindowService/MainWindowService.cs @@ -0,0 +1,80 @@ +using System; + +namespace Artemis.UI.Shared.Services.MainWindowService +{ + internal class MainWindowService : IMainWindowService + { + private IMainWindowProvider? _mainWindowManager; + + protected virtual void OnMainWindowOpened() + { + MainWindowOpened?.Invoke(this, EventArgs.Empty); + } + + protected virtual void OnMainWindowClosed() + { + MainWindowClosed?.Invoke(this, EventArgs.Empty); + } + + private void SyncWithManager() + { + if (_mainWindowManager == null) + return; + + if (IsMainWindowOpen && !_mainWindowManager.IsMainWindowOpen) + { + IsMainWindowOpen = false; + OnMainWindowClosed(); + } + + if (!IsMainWindowOpen && _mainWindowManager.IsMainWindowOpen) + { + IsMainWindowOpen = true; + OnMainWindowOpened(); + } + } + + private void HandleMainWindowOpened(object? sender, EventArgs e) + { + SyncWithManager(); + } + + private void HandleMainWindowClosed(object? sender, EventArgs e) + { + SyncWithManager(); + } + + public bool IsMainWindowOpen { get; private set; } + + public void ConfigureMainWindowProvider(IMainWindowProvider mainWindowProvider) + { + if (mainWindowProvider == null) throw new ArgumentNullException(nameof(mainWindowProvider)); + + if (_mainWindowManager != null) + { + _mainWindowManager.MainWindowOpened -= HandleMainWindowOpened; + _mainWindowManager.MainWindowClosed -= HandleMainWindowClosed; + } + + _mainWindowManager = mainWindowProvider; + _mainWindowManager.MainWindowOpened += HandleMainWindowOpened; + _mainWindowManager.MainWindowClosed += HandleMainWindowClosed; + + // Sync up with the new manager's state + SyncWithManager(); + } + + public void OpenMainWindow() + { + _mainWindowManager?.OpenMainWindow(); + } + + public void CloseMainWindow() + { + _mainWindowManager?.CloseMainWindow(); + } + + public event EventHandler? MainWindowOpened; + public event EventHandler? MainWindowClosed; + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Services/WindowService/WindowService.cs b/src/Avalonia/Artemis.UI.Shared/Services/WindowService/WindowService.cs index 4d18cfbd0..8ef62b62c 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/WindowService/WindowService.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/WindowService/WindowService.cs @@ -33,7 +33,7 @@ namespace Artemis.UI.Shared.Services public void ShowWindow(object viewModel) { - Window parent = GetCurrentWindow(); + Window? parent = GetCurrentWindow(); string name = viewModel.GetType().FullName!.Split('`')[0].Replace("ViewModel", "View"); Type? type = viewModel.GetType().Assembly.GetType(name); @@ -50,7 +50,10 @@ namespace Artemis.UI.Shared.Services Window window = (Window) Activator.CreateInstance(type)!; window.DataContext = viewModel; - window.Show(parent); + if (parent != null) + window.Show(parent); + else + window.Show(); } public async Task ShowDialogAsync(params (string name, object value)[] parameters) where TViewModel : DialogViewModelBase @@ -132,14 +135,14 @@ namespace Artemis.UI.Shared.Services return new SaveFileDialogBuilder(GetCurrentWindow()); } - public Window GetCurrentWindow() + public Window? GetCurrentWindow() { if (Application.Current.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime classic) { throw new ArtemisSharedUIException("Can't show a dialog when application lifetime is not IClassicDesktopStyleApplicationLifetime."); } - Window parent = classic.Windows.FirstOrDefault(w => w.IsActive) ?? classic.MainWindow; + Window? parent = classic.Windows.FirstOrDefault(w => w.IsActive) ?? classic.MainWindow; return parent; } } diff --git a/src/Avalonia/Artemis.UI.Windows/App.axaml.cs b/src/Avalonia/Artemis.UI.Windows/App.axaml.cs index cc0592702..471e65589 100644 --- a/src/Avalonia/Artemis.UI.Windows/App.axaml.cs +++ b/src/Avalonia/Artemis.UI.Windows/App.axaml.cs @@ -2,7 +2,6 @@ using Avalonia; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; using Avalonia.Threading; -using FluentAvalonia.Styling; using Ninject; using ReactiveUI; @@ -10,27 +9,23 @@ namespace Artemis.UI.Windows { public class App : Application { - private StandardKernel _kernel; - private ApplicationStateManager _stateManager; + // ReSharper disable NotAccessedField.Local + private StandardKernel? _kernel; + private ApplicationStateManager? _applicationStateManager; + // ReSharper restore NotAccessedField.Local public override void Initialize() { - _kernel = ArtemisBootstrapper.Bootstrap(); + _kernel = ArtemisBootstrapper.Bootstrap(this); RxApp.MainThreadScheduler = AvaloniaScheduler.Instance; AvaloniaXamlLoader.Load(this); } public override void OnFrameworkInitializationCompleted() { + ArtemisBootstrapper.Initialized(); if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) - { - ArtemisBootstrapper.ConfigureApplicationLifetime(desktop); - AvaloniaLocator.Current.GetService().ForceNativeTitleBarToTheme(desktop.MainWindow, "Dark"); - - _stateManager = new ApplicationStateManager(_kernel, desktop.Args); - } - - base.OnFrameworkInitializationCompleted(); + _applicationStateManager = new ApplicationStateManager(_kernel!, desktop.Args); } } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/ArtemisBootstrapper.cs b/src/Avalonia/Artemis.UI/ArtemisBootstrapper.cs index 18f78e866..15275c559 100644 --- a/src/Avalonia/Artemis.UI/ArtemisBootstrapper.cs +++ b/src/Avalonia/Artemis.UI/ArtemisBootstrapper.cs @@ -3,6 +3,8 @@ using Artemis.UI.Exceptions; using Artemis.UI.Ninject; using Artemis.UI.Screens.Root; using Artemis.UI.Shared.Ninject; +using Avalonia; +using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Ninject; using Splat.Ninject; @@ -12,12 +14,14 @@ namespace Artemis.UI public static class ArtemisBootstrapper { private static StandardKernel? _kernel; + private static Application? _application; - public static StandardKernel Bootstrap() + public static StandardKernel Bootstrap(Application application) { - if (_kernel != null) + if (_application != null || _kernel != null) throw new ArtemisUIException("UI already bootstrapped"); - + + _application = application; _kernel = new StandardKernel(); _kernel.Settings.InjectNonPublic = true; @@ -30,12 +34,19 @@ namespace Artemis.UI return _kernel; } - public static void ConfigureApplicationLifetime(IClassicDesktopStyleApplicationLifetime applicationLifetime) + public static void Initialized() { - if (_kernel == null) + if (_application == null || _kernel == null) throw new ArtemisUIException("UI not yet bootstrapped"); + if (_application.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop) + return; - applicationLifetime.MainWindow = new MainWindow {DataContext = _kernel.Get()}; + // Don't shut down when the last window closes, we might still be active in the tray + desktop.ShutdownMode = ShutdownMode.OnExplicitShutdown; + // Create the root view model that drives the UI + RootViewModel rootViewModel = _kernel.Get(); + // Apply the root view model to the data context of the application so that tray icon commands work + _application.DataContext = rootViewModel; } } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/ArtemisTrayIcon.axaml b/src/Avalonia/Artemis.UI/ArtemisTrayIcon.axaml new file mode 100644 index 000000000..2e2530293 --- /dev/null +++ b/src/Avalonia/Artemis.UI/ArtemisTrayIcon.axaml @@ -0,0 +1,12 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs b/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs index 736298c31..878b09a36 100644 --- a/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs +++ b/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs @@ -34,7 +34,7 @@ namespace Artemis.UI.Ninject.Factories public interface ISidebarVmFactory : IVmFactory { - SidebarViewModel SidebarViewModel(IScreen hostScreen); + SidebarViewModel? SidebarViewModel(IScreen hostScreen); SidebarCategoryViewModel SidebarCategoryViewModel(ProfileCategory profileCategory); SidebarProfileConfigurationViewModel SidebarProfileConfigurationViewModel(ProfileConfiguration profileConfiguration); } diff --git a/src/Avalonia/Artemis.UI/Ninject/UIModule.cs b/src/Avalonia/Artemis.UI/Ninject/UIModule.cs index 30f910279..4461c6a66 100644 --- a/src/Avalonia/Artemis.UI/Ninject/UIModule.cs +++ b/src/Avalonia/Artemis.UI/Ninject/UIModule.cs @@ -3,6 +3,8 @@ using Artemis.UI.Ninject.Factories; using Artemis.UI.Screens; using Artemis.UI.Services.Interfaces; using Artemis.UI.Shared; +using Avalonia.Platform; +using Avalonia.Shared.PlatformSupport; using Ninject.Extensions.Conventions; using Ninject.Modules; using Ninject.Planning.Bindings.Resolvers; @@ -17,6 +19,7 @@ namespace Artemis.UI.Ninject throw new ArgumentNullException("Kernel shouldn't be null here."); Kernel.Components.Add(); + Kernel.Bind().ToConstant(new AssetLoader()); Kernel.Bind(x => { diff --git a/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/DataModel/DataModelDebugViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/DataModel/DataModelDebugViewModel.cs index 734634975..71dac014d 100644 --- a/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/DataModel/DataModelDebugViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/DataModel/DataModelDebugViewModel.cs @@ -11,6 +11,7 @@ using Artemis.Core.Services; using Artemis.UI.Shared; using Artemis.UI.Shared.DataModelVisualization.Shared; using Artemis.UI.Shared.Services; +using Artemis.UI.Shared.Services.Interfaces; using DynamicData; using ReactiveUI; diff --git a/src/Avalonia/Artemis.UI/Screens/Root/RootViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Root/RootViewModel.cs index 8e6bdda11..a702c091e 100644 --- a/src/Avalonia/Artemis.UI/Screens/Root/RootViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Root/RootViewModel.cs @@ -1,30 +1,172 @@ -using Artemis.Core.Services; +using System; +using System.Linq; +using System.Threading.Tasks; +using Artemis.Core; +using Artemis.Core.Services; using Artemis.UI.Ninject.Factories; using Artemis.UI.Screens.Root.Sidebar; using Artemis.UI.Services.Interfaces; using Artemis.UI.Shared; +using Artemis.UI.Shared.Services.Interfaces; +using Artemis.UI.Shared.Services.MainWindowService; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Platform; +using Avalonia.Threading; using ReactiveUI; namespace Artemis.UI.Screens.Root { - public class RootViewModel : ActivatableViewModelBase, IScreen + public class RootViewModel : ActivatableViewModelBase, IScreen, IMainWindowProvider { + private readonly IClassicDesktopStyleApplicationLifetime _lifeTime; private readonly ICoreService _coreService; + private readonly ISettingsService _settingsService; + private readonly IWindowService _windowService; + private readonly IAssetLoader _assetLoader; + private readonly ISidebarVmFactory _sidebarVmFactory; + private SidebarViewModel? _sidebarViewModel; + private TrayIcon? _trayIcon; + private TrayIcons? _trayIcons; - public RootViewModel(ICoreService coreService, IRegistrationService registrationService, ISidebarVmFactory sidebarVmFactory) + public RootViewModel(ICoreService coreService, + ISettingsService settingsService, + IRegistrationService registrationService, + IWindowService windowService, + IMainWindowService mainWindowService, + IAssetLoader assetLoader, + ISidebarVmFactory sidebarVmFactory) { Router = new RoutingState(); - SidebarViewModel = sidebarVmFactory.SidebarViewModel(this); _coreService = coreService; - _coreService.Initialize(); + _settingsService = settingsService; + _windowService = windowService; + _assetLoader = assetLoader; + _sidebarVmFactory = sidebarVmFactory; + _lifeTime = (IClassicDesktopStyleApplicationLifetime) Application.Current.ApplicationLifetime; + coreService.StartupArguments = _lifeTime.Args.ToList(); + mainWindowService.ConfigureMainWindowProvider(this); registrationService.RegisterProviders(); + + DisplayAccordingToSettings(); + Task.Run(coreService.Initialize); } - public SidebarViewModel SidebarViewModel { get; } + public SidebarViewModel? SidebarViewModel + { + get => _sidebarViewModel; + set => this.RaiseAndSetIfChanged(ref _sidebarViewModel, value); + } /// public RoutingState Router { get; } + + public async Task Exit() + { + // Don't freeze the UI right after clicking + await Task.Delay(200); + Utilities.Shutdown(); + } + + private void CurrentMainWindowOnClosed(object? sender, EventArgs e) + { + _lifeTime.MainWindow = null; + SidebarViewModel = null; + + OnMainWindowClosed(); + } + + private void DisplayAccordingToSettings() + { + bool autoRunning = _coreService.StartupArguments.Contains("--autorun"); + bool minimized = _coreService.StartupArguments.Contains("--minimized"); + bool showOnAutoRun = _settingsService.GetSetting("UI.ShowOnStartup", true).Value; + + // Always show the tray icon if ShowOnStartup is false or the user has no way to open the main window + bool showTrayIcon = !showOnAutoRun || _settingsService.GetSetting("UI.ShowTrayIcon", true).Value; + + if (showTrayIcon) + ShowTrayIcon(); + + if (autoRunning && !showOnAutoRun || minimized) + { + // TODO: Auto-update + } + else + { + ShowSplashScreen(); + _coreService.Initialized += (_, _) => Dispatcher.UIThread.InvokeAsync(OpenMainWindow); + } + } + + private void ShowSplashScreen() + { + _windowService.ShowWindow(); + } + + private void ShowTrayIcon() + { + _trayIcon = new TrayIcon {Icon = new WindowIcon(_assetLoader.Open(new Uri("avares://Artemis.UI/Assets/Images/Logo/bow.ico")))}; + _trayIcon.Menu = (NativeMenu?) Application.Current.FindResource("TrayIconMenu"); + _trayIcons = new TrayIcons {_trayIcon}; + TrayIcon.SetIcons(Application.Current, _trayIcons); + } + + private void HideTrayIcon() + { + _trayIcon?.Dispose(); + TrayIcon.SetIcons(Application.Current, null!); + + _trayIcon = null; + _trayIcons = null; + } + + #region Implementation of IMainWindowProvider + + /// + public bool IsMainWindowOpen => _lifeTime.MainWindow != null; + + /// + public void OpenMainWindow() + { + if (_lifeTime.MainWindow == null) + { + SidebarViewModel = _sidebarVmFactory.SidebarViewModel(this); + _lifeTime.MainWindow = new MainWindow {DataContext = this}; + _lifeTime.MainWindow.Show(); + _lifeTime.MainWindow.Closed += CurrentMainWindowOnClosed; + } + + _lifeTime.MainWindow.Activate(); + + OnMainWindowOpened(); + } + + /// + public void CloseMainWindow() + { + _lifeTime.MainWindow?.Close(); + } + + /// + public event EventHandler? MainWindowOpened; + + /// + public event EventHandler? MainWindowClosed; + + protected virtual void OnMainWindowOpened() + { + MainWindowOpened?.Invoke(this, EventArgs.Empty); + } + + protected virtual void OnMainWindowClosed() + { + MainWindowClosed?.Invoke(this, EventArgs.Empty); + } + + #endregion } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/Root/SplashView.axaml b/src/Avalonia/Artemis.UI/Screens/Root/SplashView.axaml new file mode 100644 index 000000000..37211a912 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/Root/SplashView.axaml @@ -0,0 +1,32 @@ + + + + + + + + + Artemis is initializing... + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/Root/SplashView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/Root/SplashView.axaml.cs new file mode 100644 index 000000000..b3fed1a81 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/Root/SplashView.axaml.cs @@ -0,0 +1,33 @@ +using System; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using Avalonia; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; +using Avalonia.Threading; +using ReactiveUI; + +namespace Artemis.UI.Screens.Root +{ + public class SplashView : ReactiveWindow + { + public SplashView() + { + InitializeComponent(); +#if DEBUG + this.AttachDevTools(); +#endif + this.WhenActivated(disposables => + { + Observable.FromEventPattern(x => ViewModel!.CoreService.Initialized += x, x => ViewModel!.CoreService.Initialized -= x) + .Subscribe(_ => Dispatcher.UIThread.Post(Close)) + .DisposeWith(disposables); + }); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/Root/SplashViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Root/SplashViewModel.cs new file mode 100644 index 000000000..e12ab63d6 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/Root/SplashViewModel.cs @@ -0,0 +1,71 @@ +using System; +using Artemis.Core; +using Artemis.Core.Services; +using Artemis.UI.Shared; +using Humanizer; +using ReactiveUI; + +namespace Artemis.UI.Screens.Root +{ + public class SplashViewModel : ViewModelBase + { + private string _status; + + public SplashViewModel(ICoreService coreService, IPluginManagementService pluginManagementService) + { + CoreService = coreService; + _status = "Initializing Core"; + + pluginManagementService.CopyingBuildInPlugins += OnPluginManagementServiceOnCopyingBuildInPluginsManagement; + pluginManagementService.PluginLoading += OnPluginManagementServiceOnPluginManagementLoading; + pluginManagementService.PluginLoaded += OnPluginManagementServiceOnPluginManagementLoaded; + pluginManagementService.PluginEnabling += PluginManagementServiceOnPluginManagementEnabling; + pluginManagementService.PluginEnabled += PluginManagementServiceOnPluginManagementEnabled; + pluginManagementService.PluginFeatureEnabling += PluginManagementServiceOnPluginFeatureEnabling; + pluginManagementService.PluginFeatureEnabled += PluginManagementServiceOnPluginFeatureEnabled; + } + + public ICoreService CoreService { get; } + + public string Status + { + get => _status; + set => this.RaiseAndSetIfChanged(ref _status, value); + } + + private void OnPluginManagementServiceOnPluginManagementLoaded(object? sender, PluginEventArgs args) + { + Status = "Initializing UI"; + } + + private void OnPluginManagementServiceOnPluginManagementLoading(object? sender, PluginEventArgs args) + { + Status = "Loading plugin: " + args.Plugin.Info.Name; + } + + private void PluginManagementServiceOnPluginManagementEnabled(object? sender, PluginEventArgs args) + { + Status = "Initializing UI"; + } + + private void PluginManagementServiceOnPluginManagementEnabling(object? sender, PluginEventArgs args) + { + Status = "Enabling plugin: " + args.Plugin.Info.Name; + } + + private void PluginManagementServiceOnPluginFeatureEnabling(object? sender, PluginFeatureEventArgs e) + { + Status = "Enabling: " + e.PluginFeature.GetType().Name.Humanize(); + } + + private void PluginManagementServiceOnPluginFeatureEnabled(object? sender, PluginFeatureEventArgs e) + { + Status = "Initializing UI"; + } + + private void OnPluginManagementServiceOnCopyingBuildInPluginsManagement(object? sender, EventArgs args) + { + Status = "Updating built-in plugins"; + } + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Styles/Artemis.axaml b/src/Avalonia/Artemis.UI/Styles/Artemis.axaml index daa01349c..c40b34e96 100644 --- a/src/Avalonia/Artemis.UI/Styles/Artemis.axaml +++ b/src/Avalonia/Artemis.UI/Styles/Artemis.axaml @@ -1,7 +1,14 @@  - + + + + + + Yellow + +