From 0b905cca2e6831eb25b84aa0a07088c1157d0a7e Mon Sep 17 00:00:00 2001 From: Robert Date: Mon, 10 Jan 2022 00:07:06 +0100 Subject: [PATCH] Profile editor - Ported most of the property tree --- .../Artemis.UI.Avalonia.Shared.xml | 1582 ----------------- .../Artemis.UI.Shared.csproj | 4 +- .../Artemis.UI.Shared.csproj.DotSettings | 2 + .../BrushConfigurationViewModel.cs | 57 + .../LayerBrushConfigurationDialog.cs | 45 + .../EffectConfigurationViewModel.cs | 57 + .../LayerEffectConfigurationDialog.cs | 46 + .../ScriptEditorViewModel.cs | 56 + .../IMainWindowProvider.cs | 2 +- .../IMainWindowService.cs | 2 +- .../MainWindowService.cs | 2 +- .../Commands/AddProfileElement.cs | 60 + .../Commands/RemoveProfileElement.cs | 65 + .../Commands/UpdateLayerProperty.cs | 56 + .../ProfileEditor/IProfileEditorCommand.cs | 2 +- .../ProfileEditor/IProfileEditorService.cs | 12 +- .../ProfileEditor/ProfileEditorHistory.cs | 2 +- .../ProfileEditor/ProfileEditorService.cs | 94 + .../PropertyInputRegistration.cs | 52 + .../PropertyInput/PropertyInputService.cs | 100 ++ .../PropertyInput/PropertyInputViewModel.cs | 213 +++ .../ExceptionDialogView.axaml | 0 .../ExceptionDialogView.axaml.cs | 0 .../ExceptionDialogViewModel.cs | 0 .../WindowService.cs | 0 src/Avalonia/Artemis.UI/Artemis.UI.csproj | 6 + .../Converters/PropertyTreeMarginConverter.cs | 28 + .../Ninject/Factories/IVMFactory.cs | 17 + .../Commands/AddProfileElement.cs | 56 - .../Commands/RemoveProfileElement.cs | 63 - .../Panels/MenuBar/MenuBarView.axaml | 2 +- .../Panels/MenuBar/MenuBarView.axaml.cs | 4 +- .../Panels/MenuBar/MenuBarViewModel.cs | 4 +- .../ProfileElementPropertiesView.axaml | 15 + .../ProfileElementPropertiesView.axaml.cs | 18 + .../ProfileElementPropertiesViewModel.cs | 156 ++ .../ProfileElementPropertyGroupViewModel.cs | 40 + .../ProfileElementPropertyViewModel.cs | 20 + .../Tree/TreeGroupView.axaml | 183 ++ .../Tree/TreeGroupView.axaml.cs | 18 + .../Tree/TreeGroupViewModel.cs | 164 ++ .../Tree/TreePropertyView.axaml | 8 + .../Tree/TreePropertyView.axaml.cs | 19 + .../Tree/TreePropertyViewModel.cs | 19 + .../BrushConfigurationWindowView.axaml | 16 + .../BrushConfigurationWindowView.axaml.cs | 27 + .../BrushConfigurationWindowViewModel.cs | 32 + .../EffectConfigurationWindowView.axaml | 16 + .../EffectConfigurationWindowView.axaml.cs | 27 + .../EffectConfigurationWindowViewModel.cs | 31 + .../ProfileTree/FolderTreeItemViewModel.cs | 2 +- .../ProfileTree/LayerTreeItemViewModel.cs | 2 +- .../ProfileTree/ProfileTreeViewModel.cs | 2 +- .../Panels/ProfileTree/TreeItemViewModel.cs | 4 +- .../VisualEditor/VisualEditorViewModel.cs | 2 +- .../ProfileEditorTitleBarViewModel.cs | 2 +- .../ProfileEditor/ProfileEditorView.axaml | 2 +- .../ProfileEditor/ProfileEditorViewModel.cs | 32 +- .../Artemis.UI/Screens/Root/RootViewModel.cs | 2 +- .../Sidebar/SidebarCategoryViewModel.cs | 2 +- .../Screens/Sidebar/SidebarViewModel.cs | 2 +- .../ProfileEditor/ProfileEditorService.cs | 58 - 62 files changed, 1807 insertions(+), 1805 deletions(-) delete mode 100644 src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.xml create mode 100644 src/Avalonia/Artemis.UI.Shared/Plugins/LayerBrushes/BrushConfigurationViewModel.cs create mode 100644 src/Avalonia/Artemis.UI.Shared/Plugins/LayerBrushes/LayerBrushConfigurationDialog.cs create mode 100644 src/Avalonia/Artemis.UI.Shared/Plugins/LayerEffects/EffectConfigurationViewModel.cs create mode 100644 src/Avalonia/Artemis.UI.Shared/Plugins/LayerEffects/LayerEffectConfigurationDialog.cs create mode 100644 src/Avalonia/Artemis.UI.Shared/Plugins/ScriptingProviders/ScriptEditorViewModel.cs rename src/Avalonia/Artemis.UI.Shared/Services/{MainWindowService => MainWindow}/IMainWindowProvider.cs (94%) rename src/Avalonia/Artemis.UI.Shared/Services/{MainWindowService => MainWindow}/IMainWindowService.cs (96%) rename src/Avalonia/Artemis.UI.Shared/Services/{MainWindowService => MainWindow}/MainWindowService.cs (97%) create mode 100644 src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/AddProfileElement.cs create mode 100644 src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/RemoveProfileElement.cs create mode 100644 src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/UpdateLayerProperty.cs rename src/Avalonia/{Artemis.UI => Artemis.UI.Shared}/Services/ProfileEditor/IProfileEditorCommand.cs (90%) rename src/Avalonia/{Artemis.UI => Artemis.UI.Shared}/Services/ProfileEditor/IProfileEditorService.cs (59%) rename src/Avalonia/{Artemis.UI => Artemis.UI.Shared}/Services/ProfileEditor/ProfileEditorHistory.cs (98%) create mode 100644 src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs create mode 100644 src/Avalonia/Artemis.UI.Shared/Services/PropertyInput/PropertyInputRegistration.cs create mode 100644 src/Avalonia/Artemis.UI.Shared/Services/PropertyInput/PropertyInputService.cs create mode 100644 src/Avalonia/Artemis.UI.Shared/Services/PropertyInput/PropertyInputViewModel.cs rename src/Avalonia/Artemis.UI.Shared/Services/{WindowService => Window}/ExceptionDialogView.axaml (100%) rename src/Avalonia/Artemis.UI.Shared/Services/{WindowService => Window}/ExceptionDialogView.axaml.cs (100%) rename src/Avalonia/Artemis.UI.Shared/Services/{WindowService => Window}/ExceptionDialogViewModel.cs (100%) rename src/Avalonia/Artemis.UI.Shared/Services/{WindowService => Window}/WindowService.cs (100%) create mode 100644 src/Avalonia/Artemis.UI/Converters/PropertyTreeMarginConverter.cs delete mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Commands/AddProfileElement.cs delete mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Commands/RemoveProfileElement.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertiesView.axaml create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertiesView.axaml.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertiesViewModel.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertyGroupViewModel.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertyViewModel.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreeGroupView.axaml create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreeGroupView.axaml.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreeGroupViewModel.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreePropertyView.axaml create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreePropertyView.axaml.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreePropertyViewModel.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Windows/BrushConfigurationWindowView.axaml create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Windows/BrushConfigurationWindowView.axaml.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Windows/BrushConfigurationWindowViewModel.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Windows/EffectConfigurationWindowView.axaml create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Windows/EffectConfigurationWindowView.axaml.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Windows/EffectConfigurationWindowViewModel.cs delete mode 100644 src/Avalonia/Artemis.UI/Services/ProfileEditor/ProfileEditorService.cs diff --git a/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.xml b/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.xml deleted file mode 100644 index 555a46173..000000000 --- a/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.xml +++ /dev/null @@ -1,1582 +0,0 @@ - - - - Artemis.UI.Shared - - - - - Represents a control that can display an arbitrary kind of icon. - - - - - Creates a new instance of the class. - - - - - Gets or sets the currently displayed icon as either a or an - pointing to an SVG - - - - - Gets or sets the currently displayed icon as either a or an - pointing to an SVG - - - - - Visualizes an with optional per-LED colors - - - - - - - - - - - Occurs when a LED of the device has been clicked - - - - - Invokes the event - - - - - - Gets or sets the to display - - - - - Gets or sets the to display - - - - - Gets or sets boolean indicating whether or not to show per-LED colors - - - - - Gets or sets a boolean indicating whether or not to show per-LED colors - - - - - Gets or sets a list of LEDs to highlight - - - - - Gets or sets a list of LEDs to highlight - - - - - - - - - - - - - - - - - Represents a combobox that can display the values of an enum. - - - - - Gets or sets the currently selected value - - - - - Creates a new instance of the class. - - - - - Gets or sets the currently selected value - - - - - - - - - - - Represents a control that can be used to display or edit instances. - - - - - Creates a new instance of the class - - - - - Gets or sets the currently displayed icon as either a or an - pointing to an SVG - - - - - Gets or sets the watermark of the hotkey box when it is empty. - - - - - Gets or sets a boolean indicating whether the watermark should float above the hotkey box when it is not empty. - - - - - Gets or sets the currently displayed icon as either a or an - pointing to an SVG - - - - - Gets or sets the watermark of the hotkey box when it is empty. - - - - - Gets or sets a boolean indicating whether the watermark should float above the hotkey box when it is not empty. - - - - - - - - - - - Represents a control that can display the icon of a specific . - - - - - Creates a new instance of the class. - - - - - Gets or sets the to display - - - - - Gets or sets the to display - - - - - Visualizes an with optional per-LED colors - - - - - Defines the property. - - - - - Defines the property. - - - - - Defines the property. - - - - - Defines the property. - - - - - Defines the property. - - - - - - - - Gets or sets a brush used to paint the control's background. - - - - - Gets or sets a brush used to paint the control's border - - - - - Gets or sets the width of the control's border - - - - - Gets or sets the radius of the control's border - - - - - Gets or sets the element that captures input for the selection rectangle. - - - - - Occurs when the selection rect is being updated, indicating the user is dragging. - - - - - Occurs when the selection has finished, indicating the user stopped dragging. - - - - - Invokes the event - - - - - - Invokes the event - - - - - - - - - - - - - - - Converts into . - - - - - - - - - - - Converts an enum into a boolean. - - - - - - - - - - - Converts into . - - - - - - - - - - - Converts into . - - - - - - - - - - - Represents a display view model - - The type of the data model - - - - Gets or sets value that the view model must display - - - - - - - - Occurs when the display value is updated - - - - - For internal use only, implement instead. - - - - - Gets the property description of this value - - - - - Prevents this type being implemented directly, implement instead. - - - - - Updates the display value - - The value to set - - - - Represents a input view model - - The type of the data model - - - - Creates a new instance of the class - - The description of the property this input VM is representing - The initial value to set the input value to - - - - Gets or sets the value shown in the input - - - - - Gets the description of the property this input VM is representing - - - - - - - - - - - For internal use only, implement instead. - - - - - Prevents this type being implemented directly, implement instead. - - - - - Gets the types this input view model can support through type conversion. This list is defined when registering the - view model. - - - - - Submits the input value and removes this view model. - This is called automatically when the user presses enter or clicks outside the view - - - - - Discards changes to the input value and removes this view model. - This is called automatically when the user presses escape - - - - - Called before the current value is submitted - - - - - Called before the current value is discarded - - - - - Represents a layer brush registered through - or - - - - - - Gets the type of registration, either a display or an input - - - - - Gets the plugin that registered the visualization - - - - - Gets the type supported by the visualization - - - - - Gets the view model type of the visualization - - - - - Gets a read only collection of types this visualization can convert to and from - - - - - Represents a type of data model visualization registration - - - - - A visualization used for displaying values - - - - - A visualization used for inputting values - - - - - Represents a view model that visualizes an event data model property - - - - - Gets the type of event arguments this event triggers and that must be displayed as children - - - - - - - - Always returns for data model events - - - - - - - - Represents a view model that visualizes a single data model property contained in a - - - - - - Gets the view model used to display the display value - - - - - Gets the index of the element within the list - - - - - Gets the type of elements contained in the list - - - - - Gets the value of the property that is being visualized - - - - - - - - - - - - - - Represents a view model that visualizes a list data model property - - - - - Gets the instance of the list that is being visualized - - - - - Gets amount of elements in the list that is being visualized - - - - - Gets the type of elements this list contains and that must be displayed as children - - - - - Gets a human readable display count - - - - - Gets a list of child view models that visualize the elements in the list - - - - - - - - - - - Represents a view model that visualizes a class (POCO) data model property containing child properties - - - - - Gets the type of the property that is being visualized - - - - - Gets the value of the property that is being visualized - - - - - - - - - - - - - - Represents a view model that visualizes a single data model property contained in a - - - - - - Gets the value of the property that is being visualized - - - - - Gets the type of the property that is being visualized - - - - - Gets the view model used to display the display value - - - - - - - - - - - Represents a configuration to use while updating a - - - - - Creates a new instance of the class - - A boolean indicating whether or not event children should be created - - - - Gets a boolean indicating whether or not event children should be created - - - - - Represents a base class for a view model that visualizes a part of the data model - - - - - Gets a boolean indicating whether this view model is at the root of the data model - - - - - Gets the data model path to the property this view model is visualizing - - - - - Gets a string representation of the path backing this model - - - - - Gets the property depth of the view model - - - - - Gets the data model backing this view model - - - - - Gets the property description of the property this view model is visualizing - - - - - Gets the parent of this view model - - - - - Gets or sets an observable collection containing the children of this view model - - - - - Gets a boolean indicating whether the property being visualized matches the types last provided to - - - - - - Gets or sets a boolean indicating whether the visualization is expanded, exposing the - - - - - Gets a user-friendly representation of the - - - - - Updates the datamodel and if in an parent, any children - - The data model UI service used during update - The configuration to apply while updating - - - - Gets the current value of the property being visualized - - The current value of the property being visualized - - - - Determines whether the provided types match the type of the property being visualized and sets the result in - - - Whether the type may be a loose match, meaning it can be cast or converted - The types to filter - - - - Occurs when an update to the property this view model visualizes is requested - - - - - Invokes the event - - - - - Releases the unmanaged resources used by the object and optionally releases the managed resources. - - - to release both managed and unmanaged resources; - to release only unmanaged resources. - - - - - - - - Represents a default data model display view. - - - - - Creates a new instance of the class. - - - - - Represents the default data model display view model that is used when no display viewmodel specific for the type - is registered - - - - - Provides data on LED click events raised by the device visualizer - - - - - The device that was clicked - - - - - The LED that was clicked - - - - - Provides data on profile related events raised by the profile editor - - - - - Gets the profile the event was raised for - - - - - If applicable, the previous active profile before the event was raised - - - - - Provides data on profile element related events raised by the profile editor - - - - - Gets the profile element the event was raised for - - - - - If applicable, the previous active profile element before the event was raised - - - - - Provides data on selection events raised by the . - - - - - Creates a new instance of the class. - - - - - Gets the rectangle that was selected when the event occurred. - - - - - Gets the key modifiers that where pressed when the event occurred. - - - - - Represents errors that occur within the Artemis Shared UI library - - - - - Provides extension methods for Avalonia's type - - - - - Clears all data validation errors on the given control and any of it's logical siblings - - The target control - - - - The main of the Artemis Shared UI toolkit that binds all services - - - - - - - - - - - - - - Describes a configuration dialog for a specific plugin - - - - - - - - Represents a view model for a plugin configuration window - - - - - Creates a new instance of the class - - - - - - Gets the plugin this configuration view model is associated with - - - - - Closes the window hosting the view model - - - - - Called when the the window hosting the view model should close - - - - - Occurs when the the window hosting the view model should close - - - - - Represents a builder that can be used to create Fluent UI dialogs. - - - - - Changes the title of the dialog. - - The new title. - The builder that can be used to further build the dialog. - - - - Changes the content of the dialog. - - The new content. - The builder that can be used to further build the dialog. - - - - Changes the default button of the dialog that is pressed on enter. - - The default button. - The builder that can be used to further build the dialog. - - - - Changes the primary button of the dialog. - - An action to configure the button. - The builder that can be used to further build the dialog. - - - - Changes the secondary button of the dialog. - - An action to configure the button. - The builder that can be used to further build the dialog. - - - - Changes the text of the close button of the dialog. - - The new text. - The builder that can be used to further build the dialog. - - - - Changes the view model of the content dialog, hosting it inside the dialog. - - The type of the view model to host. - The resulting view model. - Optional parameters to pass to the constructor of the view model, case and order sensitive. - The builder that can be used to further build the dialog. - - - - Asynchronously shows the content dialog. - - A task containing the result of the content dialog. - Thrown when the parent window does not contain a panel at its root. - - - - Represents a content dialog button. - - - - - No button. - - - - - The primary button. - - - - - The secondary button. - - - - - The close button. - - - - - Represents a builder that can be used to create buttons inside content dialogs. - - - - - Changes text message of the button. - - The new text. - The notification builder that can be used to further build the button. - - - - Changes action that is called when the button is clicked. - - The action to call when the button is clicked. - The builder that can be used to further build the button. - - - - Changes command that is called when the button is clicked. - - The command to call when the button is clicked. - The builder that can be used to further build the button. - - - - Changes parameter of the command that is called when the button is clicked. - - The parameter of the command to call when the button is clicked. - The builder that can be used to further build the button. - - - - Represents a builder that can create a . - - - - - Sets the name of the filter - - - - - Adds the provided extension to the filter - - - - - Represents a builder that can be used to create notifications. - - - - - Creates a new instance of the class. - - The parent window that will host the notification. - - - - Changes the title of the notification. - - The new title. - The notification builder that can be used to further build the notification. - - - - Changes the message of the notification. - - The new message. - The notification builder that can be used to further build the notification. - - - - Changes the timeout of the notification after which it disappears automatically. - - The timeout of the notification after which it disappears automatically. - The notification builder that can be used to further build the notification. - - - - Changes the vertical position of the notification inside the parent window. - - The vertical position of the notification inside the parent window. - The notification builder that can be used to further build the notification. - - - - Changes the horizontal position of the notification inside the parent window. - - The horizontal position of the notification inside the parent window. - The notification builder that can be used to further build the notification. - - - - Changes the severity (color) of the notification. - - The severity (color) of the notification. - The notification builder that can be used to further build the notification. - - - - Changes the action button of the notification. - - An action to configure the button. - The notification builder that can be used to further build the notification. - - - - Shows the notification. - - - - - Represents a builder that can be used to create buttons inside notifications. - - - - - Changes text message of the button. - - The new text. - The notification builder that can be used to further build the button. - - - - Changes action that is called when the button is clicked. - - The action to call when the button is clicked. - The builder that can be used to further build the button. - - - - Changes command that is called when the button is clicked. - - The command to call when the button is clicked. - The builder that can be used to further build the button. - - - - Changes parameter of the command that is called when the button is clicked. - - The parameter of the command to call when the button is clicked. - The builder that can be used to further build the button. - - - - Represents a severity of a notification. - - - - - A severity for informational messages. - - - - - A severity for success messages. - - - - - A severity for warning messages. - - - - - A severity for error messages. - - - - - Represents a builder that can create a . - - - - - Creates a new instance of the class. - - The parent window that will host the dialog. - - - - Indicate that the user can select multiple files. - - - - - Set the title of the dialog - - - - - Set the initial directory of the dialog - - - - - Set the initial file name of the dialog - - - - - Add a filter to the dialog - - - - - Asynchronously shows the file dialog. - - - A task that on completion returns an array containing the full path to the selected - files, or null if the dialog was canceled. - - - - - Represents a builder that can create a . - - - - - Creates a new instance of the class. - - The parent window that will host the notification. - - - - Set the title of the dialog - - - - - Set the initial directory of the dialog - - - - - Set the initial file name of the dialog - - - - - Set the default extension of the dialog - - - - - Add a filter to the dialog - - - - - Asynchronously shows the save file dialog. - - - A task that on completion contains the full path of the save location, or null if the - dialog was canceled. - - - - - 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 - - - - A service that can be used to create notifications in either the application or on the desktop. - - - - - Creates an in-app notification using a builder. - - A builder used to configure and show the notification. - - - - A service that can be used to show windows and dialogs. - - - - - Creates a view model instance of type and shows its corresponding View as a - window - - The type of view model to create - The created view model - - - - Given a ViewModel, show its corresponding View as a window - - ViewModel to show the View for - - - - Shows a dialog displaying the given exception - - The title of the dialog - The exception to display - - - - Given an existing ViewModel, show its corresponding View as a Dialog - - The return type - ViewModel to show the View for - A task containing the return value of type - - - - Creates a view model instance of type and shows its corresponding View as a - Dialog - - The view model type - The return type - A task containing the return value of type - - - - Shows a content dialog asking the user to confirm an action - - The title of the dialog - The message of the dialog - The text of the confirm button - The text of the cancel button, if the cancel button will not be shown - - A task containing the result of the dialog, if confirmed; otherwise - - - - - - Creates an open file dialog, use the fluent API to configure it - - The builder that can be used to configure the dialog - - - - Creates a save file dialog, use the fluent API to configure it - - The builder that can be used to configure the dialog - - - - 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 boolean indicating whether the main window is currently open - - - - - Opens the main window - - - - - Closes the main window - - - - - Occurs when the main window has been opened - - - - - Occurs when the main window has been closed - - - - - A service that can be used to manage the state of the main window. - - - - - Gets a boolean indicating whether the main window is currently open - - - - - 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 - - - - Opens the main window if it is not already open - - - - - 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 - - - - - Represents the base class for Artemis view models - - - - - Gets the content dialog that hosts the view model - - - - - - - - Releases the unmanaged resources used by the object and optionally releases the managed resources. - - - to release both managed and unmanaged resources; - to release only unmanaged resources. - - - - - - - - Represents the base class for Artemis view models - - - - - Gets or sets the display name of the view model - - - - - Represents the base class for Artemis view models - - - - - Gets or sets the display name of the view model - - - - - Represents the base class for Artemis view models that are interested in the activated event - - - - - - - - Releases the unmanaged resources used by the object and optionally releases the managed resources. - - - to release both managed and unmanaged resources; - to release only unmanaged resources. - - - - - - - - - - - Represents the base class for Artemis view models used to drive dialogs - - - - - Closes the dialog with the given - - The result of the dialog - - - - Closes the dialog without a result - - - - diff --git a/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj b/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj index 1217094b2..1cd6e870b 100644 --- a/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj +++ b/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj @@ -9,7 +9,7 @@ x64 - C:\Repos\Artemis\src\Artemis.UI.Avalonia.Shared\Artemis.UI.Avalonia.Shared.xml + bin\Artemis.UI.Avalonia.Shared.xml @@ -43,7 +43,7 @@ HotkeyBox.axaml - + %(Filename) diff --git a/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj.DotSettings b/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj.DotSettings index 179c6a80b..22557dc59 100644 --- a/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj.DotSettings +++ b/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj.DotSettings @@ -1,4 +1,6 @@  True True + True + True True \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Plugins/LayerBrushes/BrushConfigurationViewModel.cs b/src/Avalonia/Artemis.UI.Shared/Plugins/LayerBrushes/BrushConfigurationViewModel.cs new file mode 100644 index 000000000..73f045f94 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Plugins/LayerBrushes/BrushConfigurationViewModel.cs @@ -0,0 +1,57 @@ +using System; +using System.Threading.Tasks; +using Artemis.Core.LayerBrushes; + +namespace Artemis.UI.Shared.LayerBrushes +{ + /// + /// Represents a view model for a brush configuration window + /// + public abstract class BrushConfigurationViewModel : ActivatableViewModelBase + { + /// + /// Creates a new instance of the class + /// + /// + protected BrushConfigurationViewModel(BaseLayerBrush layerBrush) + { + LayerBrush = layerBrush; + } + + /// + /// Gets the layer brush this view model is associated with + /// + public BaseLayerBrush LayerBrush { get; } + + /// + /// Closes the dialog + /// + public void RequestClose() + { + CloseRequested?.Invoke(this, EventArgs.Empty); + } + + /// + /// Called when the window wants to close, returning will cause the window to stay open. + /// + /// if the window may close; otherwise . + public virtual bool CanClose() + { + return true; + } + + /// + /// Called when the window wants to close, returning will cause the window to stay open. + /// + /// A task if the window may close; otherwise . + public virtual Task CanCloseAsync() + { + return Task.FromResult(true); + } + + /// + /// Occurs when a close was requested + /// + public event EventHandler? CloseRequested; + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Plugins/LayerBrushes/LayerBrushConfigurationDialog.cs b/src/Avalonia/Artemis.UI.Shared/Plugins/LayerBrushes/LayerBrushConfigurationDialog.cs new file mode 100644 index 000000000..7f7b94fa8 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Plugins/LayerBrushes/LayerBrushConfigurationDialog.cs @@ -0,0 +1,45 @@ +using System; +using Artemis.Core.LayerBrushes; + +namespace Artemis.UI.Shared.LayerBrushes +{ + /// + public class LayerBrushConfigurationDialog : LayerBrushConfigurationDialog where T : BrushConfigurationViewModel + { + /// + public LayerBrushConfigurationDialog() + { + } + + /// + public LayerBrushConfigurationDialog(int dialogWidth, int dialogHeight) + { + DialogWidth = dialogWidth; + DialogHeight = dialogHeight; + } + + /// + public override Type Type => typeof(T); + } + + /// + /// Describes a UI tab for a layer brush + /// + public abstract class LayerBrushConfigurationDialog : ILayerBrushConfigurationDialog + { + /// + /// The default width of the dialog + /// + public int DialogWidth { get; set; } = 800; + + /// + /// The default height of the dialog + /// + public int DialogHeight { get; set; } = 800; + + /// + /// The type of view model the dialog contains + /// + public abstract Type Type { get; } + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Plugins/LayerEffects/EffectConfigurationViewModel.cs b/src/Avalonia/Artemis.UI.Shared/Plugins/LayerEffects/EffectConfigurationViewModel.cs new file mode 100644 index 000000000..2645faddc --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Plugins/LayerEffects/EffectConfigurationViewModel.cs @@ -0,0 +1,57 @@ +using System; +using System.Threading.Tasks; +using Artemis.Core.LayerEffects; +using Avalonia.Threading; + +namespace Artemis.UI.Shared.LayerEffects; + +/// +/// Represents a view model for an effect configuration window +/// +public abstract class EffectConfigurationViewModel : ActivatableViewModelBase +{ + /// + /// Creates a new instance of the class + /// + /// + protected EffectConfigurationViewModel(BaseLayerEffect layerEffect) + { + LayerEffect = layerEffect; + } + + /// + /// Gets the layer effect this view model is associated with + /// + public BaseLayerEffect LayerEffect { get; } + + /// + /// Closes the dialog + /// + public void RequestClose() + { + CloseRequested?.Invoke(this, EventArgs.Empty); + } + + /// + /// Called when the window wants to close, returning will cause the window to stay open. + /// + /// if the window may close; otherwise . + public virtual bool CanClose() + { + return true; + } + + /// + /// Called when the window wants to close, returning will cause the window to stay open. + /// + /// A task if the window may close; otherwise . + public virtual Task CanCloseAsync() + { + return Task.FromResult(true); + } + + /// + /// Occurs when a close was requested + /// + public event EventHandler? CloseRequested; +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Plugins/LayerEffects/LayerEffectConfigurationDialog.cs b/src/Avalonia/Artemis.UI.Shared/Plugins/LayerEffects/LayerEffectConfigurationDialog.cs new file mode 100644 index 000000000..f5810e74c --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Plugins/LayerEffects/LayerEffectConfigurationDialog.cs @@ -0,0 +1,46 @@ +using System; +using Artemis.Core.LayerEffects; + +namespace Artemis.UI.Shared.LayerEffects +{ + /// + public class LayerEffectConfigurationDialog : LayerEffectConfigurationDialog where T : EffectConfigurationViewModel + { + + /// + public LayerEffectConfigurationDialog() + { + } + + /// + public LayerEffectConfigurationDialog(int dialogWidth, int dialogHeight) + { + DialogWidth = dialogWidth; + DialogHeight = dialogHeight; + } + + /// + public override Type Type => typeof(T); + } + + /// + /// Describes a UI tab for a specific layer effect + /// + public abstract class LayerEffectConfigurationDialog : ILayerEffectConfigurationDialog + { + /// + /// The default width of the dialog + /// + public int DialogWidth { get; set; } = 800; + + /// + /// The default height of the dialog + /// + public int DialogHeight { get; set; } = 800; + + /// + /// The type of view model the dialog contains + /// + public abstract Type Type { get; } + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Plugins/ScriptingProviders/ScriptEditorViewModel.cs b/src/Avalonia/Artemis.UI.Shared/Plugins/ScriptingProviders/ScriptEditorViewModel.cs new file mode 100644 index 000000000..65e1fc9b1 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Plugins/ScriptingProviders/ScriptEditorViewModel.cs @@ -0,0 +1,56 @@ +using Artemis.Core.ScriptingProviders; +using ReactiveUI; + +namespace Artemis.UI.Shared.ScriptingProviders +{ + /// + /// Represents a Stylet view model containing a script editor + /// + public class ScriptEditorViewModel : ActivatableViewModelBase, IScriptEditorViewModel + { + private Script? _script; + + /// + /// Creates a new instance of + /// + /// The script type this view model was created for + public ScriptEditorViewModel(ScriptType scriptType) + { + ScriptType = scriptType; + } + + /// + /// Called just before the script is changed to a different one + /// + /// The script to display or if no script is to be displayed + protected virtual void OnScriptChanging(Script? script) + { + } + + /// + /// Called after the script was changed to a different one + /// + /// The script to display or if no script is to be displayed + protected virtual void OnScriptChanged(Script? script) + { + } + + /// + public ScriptType ScriptType { get; } + + /// + public Script? Script + { + get => _script; + internal set => this.RaiseAndSetIfChanged(ref _script, value); + } + + /// + public void ChangeScript(Script? script) + { + OnScriptChanging(script); + Script = script; + OnScriptChanged(script); + } + } +} \ 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/MainWindow/IMainWindowProvider.cs similarity index 94% rename from src/Avalonia/Artemis.UI.Shared/Services/MainWindowService/IMainWindowProvider.cs rename to src/Avalonia/Artemis.UI.Shared/Services/MainWindow/IMainWindowProvider.cs index 7ad9ef027..5f31ae42c 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/MainWindowService/IMainWindowProvider.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/MainWindow/IMainWindowProvider.cs @@ -1,6 +1,6 @@ using System; -namespace Artemis.UI.Shared.Services.MainWindowService +namespace Artemis.UI.Shared.Services.MainWindow { /// /// Represents a class that provides the main window, so that can control the state of diff --git a/src/Avalonia/Artemis.UI.Shared/Services/MainWindowService/IMainWindowService.cs b/src/Avalonia/Artemis.UI.Shared/Services/MainWindow/IMainWindowService.cs similarity index 96% rename from src/Avalonia/Artemis.UI.Shared/Services/MainWindowService/IMainWindowService.cs rename to src/Avalonia/Artemis.UI.Shared/Services/MainWindow/IMainWindowService.cs index 1022f39cb..0ab536c14 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/MainWindowService/IMainWindowService.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/MainWindow/IMainWindowService.cs @@ -1,7 +1,7 @@ using System; using Artemis.UI.Shared.Services.Interfaces; -namespace Artemis.UI.Shared.Services.MainWindowService +namespace Artemis.UI.Shared.Services.MainWindow { /// /// A service that can be used to manage the state of the main window. diff --git a/src/Avalonia/Artemis.UI.Shared/Services/MainWindowService/MainWindowService.cs b/src/Avalonia/Artemis.UI.Shared/Services/MainWindow/MainWindowService.cs similarity index 97% rename from src/Avalonia/Artemis.UI.Shared/Services/MainWindowService/MainWindowService.cs rename to src/Avalonia/Artemis.UI.Shared/Services/MainWindow/MainWindowService.cs index d2ee398e7..ac87db953 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/MainWindowService/MainWindowService.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/MainWindow/MainWindowService.cs @@ -1,6 +1,6 @@ using System; -namespace Artemis.UI.Shared.Services.MainWindowService +namespace Artemis.UI.Shared.Services.MainWindow { internal class MainWindowService : IMainWindowService { diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/AddProfileElement.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/AddProfileElement.cs new file mode 100644 index 000000000..eab84bd7f --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/AddProfileElement.cs @@ -0,0 +1,60 @@ +using System; +using Artemis.Core; + +namespace Artemis.UI.Shared.Services.ProfileEditor.Commands; + +/// +/// Represents a profile editor command that can be used to add a profile element. +/// +public class AddProfileElement : IProfileEditorCommand, IDisposable +{ + private readonly int _index; + private readonly RenderProfileElement _subject; + private readonly ProfileElement _target; + private bool _isAdded; + + /// + /// Creates a new instance of the class. + /// + public AddProfileElement(RenderProfileElement subject, ProfileElement target, int index) + { + _subject = subject; + _target = target; + _index = index; + + DisplayName = subject switch + { + Layer => "Add layer", + Folder => "Add folder", + _ => throw new ArgumentException("Type of subject is not supported") + }; + } + + /// + public void Dispose() + { + if (!_isAdded) + _subject.Dispose(); + } + + #region Implementation of IProfileEditorCommand + + /// + public string DisplayName { get; } + + /// + public void Execute() + { + _isAdded = true; + _target.AddChild(_subject, _index); + } + + /// + public void Undo() + { + _isAdded = false; + _target.RemoveChild(_subject); + } + + #endregion +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/RemoveProfileElement.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/RemoveProfileElement.cs new file mode 100644 index 000000000..ddeab19aa --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/RemoveProfileElement.cs @@ -0,0 +1,65 @@ +using System; +using Artemis.Core; + +namespace Artemis.UI.Shared.Services.ProfileEditor.Commands; + +/// +/// Represents a profile editor command that can be used to remove a profile element. +/// +public class RemoveProfileElement : IProfileEditorCommand, IDisposable +{ + private readonly int _index; + private readonly RenderProfileElement _subject; + private readonly ProfileElement _target; + private bool _isRemoved; + + /// + /// Creates a new instance of the class. + /// + public RemoveProfileElement(RenderProfileElement subject) + { + if (subject.Parent == null) + throw new ArtemisSharedUIException("Can't remove a subject that has no parent"); + + _subject = subject; + _target = _subject.Parent; + _index = _subject.Children.IndexOf(_subject); + + DisplayName = subject switch + { + Layer => "Remove layer", + Folder => "Remove folder", + _ => throw new ArgumentException("Type of subject is not supported") + }; + } + + /// + public void Dispose() + { + if (_isRemoved) + _subject.Dispose(); + } + + #region Implementation of IProfileEditorCommand + + /// + public string DisplayName { get; } + + /// + public void Execute() + { + _isRemoved = true; + _target.RemoveChild(_subject); + _subject.Deactivate(); + } + + /// + public void Undo() + { + _isRemoved = false; + _subject.Activate(); + _target.AddChild(_subject, _index); + } + + #endregion +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/UpdateLayerProperty.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/UpdateLayerProperty.cs new file mode 100644 index 000000000..9eaf43424 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/UpdateLayerProperty.cs @@ -0,0 +1,56 @@ +using System; +using Artemis.Core; + +namespace Artemis.UI.Shared.Services.ProfileEditor.Commands; + +/// +/// Represents a profile editor command that can be used to update a layer property of type . +/// +public class UpdateLayerProperty : IProfileEditorCommand +{ + private readonly LayerProperty _layerProperty; + private readonly T _newValue; + private readonly T _originalValue; + private readonly TimeSpan? _time; + + /// + /// Creates a new instance of the class. + /// + public UpdateLayerProperty(LayerProperty layerProperty, T newValue, TimeSpan? time) + { + _layerProperty = layerProperty; + _originalValue = layerProperty.CurrentValue; + _newValue = newValue; + _time = time; + } + + /// + /// Creates a new instance of the class. + /// + public UpdateLayerProperty(LayerProperty layerProperty, T newValue, T originalValue, TimeSpan? time) + { + _layerProperty = layerProperty; + _originalValue = originalValue; + _newValue = newValue; + _time = time; + } + + #region Implementation of IProfileEditorCommand + + /// + public string DisplayName => $"Update {_layerProperty.PropertyDescription.Name ?? "property"}"; + + /// + public void Execute() + { + _layerProperty.SetCurrentValue(_newValue, _time); + } + + /// + public void Undo() + { + _layerProperty.SetCurrentValue(_originalValue, _time); + } + + #endregion +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Services/ProfileEditor/IProfileEditorCommand.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorCommand.cs similarity index 90% rename from src/Avalonia/Artemis.UI/Services/ProfileEditor/IProfileEditorCommand.cs rename to src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorCommand.cs index 83b896a55..900d1f50c 100644 --- a/src/Avalonia/Artemis.UI/Services/ProfileEditor/IProfileEditorCommand.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorCommand.cs @@ -1,4 +1,4 @@ -namespace Artemis.UI.Services.ProfileEditor +namespace Artemis.UI.Shared.Services.ProfileEditor { /// /// Represents a command that can be executed and if needed, undone diff --git a/src/Avalonia/Artemis.UI/Services/ProfileEditor/IProfileEditorService.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs similarity index 59% rename from src/Avalonia/Artemis.UI/Services/ProfileEditor/IProfileEditorService.cs rename to src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs index f799df525..f1f5e2260 100644 --- a/src/Avalonia/Artemis.UI/Services/ProfileEditor/IProfileEditorService.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs @@ -1,17 +1,23 @@ using System; +using System.Threading.Tasks; using Artemis.Core; -using Artemis.UI.Services.Interfaces; +using Artemis.UI.Shared.Services.Interfaces; -namespace Artemis.UI.Services.ProfileEditor +namespace Artemis.UI.Shared.Services.ProfileEditor { - public interface IProfileEditorService : IArtemisUIService + public interface IProfileEditorService : IArtemisSharedUIService { IObservable ProfileConfiguration { get; } IObservable ProfileElement { get; } IObservable History { get; } + IObservable Time { get; } void ChangeCurrentProfileConfiguration(ProfileConfiguration? profileConfiguration); void ChangeCurrentProfileElement(RenderProfileElement? renderProfileElement); + void ChangeTime(TimeSpan time); + void ExecuteCommand(IProfileEditorCommand command); + void SaveProfile(); + Task SaveProfileAsync(); } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Services/ProfileEditor/ProfileEditorHistory.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorHistory.cs similarity index 98% rename from src/Avalonia/Artemis.UI/Services/ProfileEditor/ProfileEditorHistory.cs rename to src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorHistory.cs index ccdc7b133..be9bd2254 100644 --- a/src/Avalonia/Artemis.UI/Services/ProfileEditor/ProfileEditorHistory.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorHistory.cs @@ -7,7 +7,7 @@ using System.Reactive.Subjects; using Artemis.Core; using ReactiveUI; -namespace Artemis.UI.Services.ProfileEditor +namespace Artemis.UI.Shared.Services.ProfileEditor { public class ProfileEditorHistory { diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs new file mode 100644 index 000000000..c82ae0597 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using System.Threading.Tasks; +using Artemis.Core; +using Artemis.Core.Services; +using Artemis.UI.Shared.Services.Interfaces; + +namespace Artemis.UI.Shared.Services.ProfileEditor; + +internal class ProfileEditorService : IProfileEditorService +{ + private readonly BehaviorSubject _profileConfigurationSubject = new(null); + private readonly Dictionary _profileEditorHistories = new(); + private readonly BehaviorSubject _profileElementSubject = new(null); + private readonly BehaviorSubject _timeSubject = new(TimeSpan.Zero); + private readonly IProfileService _profileService; + private readonly IWindowService _windowService; + + public ProfileEditorService(IProfileService profileService, IWindowService windowService) + { + _profileService = profileService; + _windowService = windowService; + ProfileConfiguration = _profileConfigurationSubject.AsObservable().DistinctUntilChanged(); + ProfileElement = _profileElementSubject.AsObservable().DistinctUntilChanged(); + History = Observable.Defer(() => Observable.Return(GetHistory(_profileConfigurationSubject.Value))).Concat(ProfileConfiguration.Select(GetHistory)); + } + + private ProfileEditorHistory? GetHistory(ProfileConfiguration? profileConfiguration) + { + if (profileConfiguration == null) + return null; + if (_profileEditorHistories.TryGetValue(profileConfiguration, out ProfileEditorHistory? history)) + return history; + + ProfileEditorHistory newHistory = new(profileConfiguration); + _profileEditorHistories.Add(profileConfiguration, newHistory); + return newHistory; + } + + public IObservable ProfileConfiguration { get; } + public IObservable ProfileElement { get; } + public IObservable History { get; } + public IObservable Time { get; } + + public void ChangeCurrentProfileConfiguration(ProfileConfiguration? profileConfiguration) + { + _profileConfigurationSubject.OnNext(profileConfiguration); + } + + public void ChangeCurrentProfileElement(RenderProfileElement? renderProfileElement) + { + _profileElementSubject.OnNext(renderProfileElement); + } + + public void ChangeTime(TimeSpan time) + { + _timeSubject.OnNext(time); + } + + public void ExecuteCommand(IProfileEditorCommand command) + { + try + { + ProfileEditorHistory? history = GetHistory(_profileConfigurationSubject.Value); + if (history == null) + throw new ArtemisSharedUIException("Can't execute a command when there's no active profile configuration"); + + history.Execute.Execute(command).Subscribe(); + } + catch (Exception e) + { + _windowService.ShowExceptionDialog("Editor command failed", e); + throw; + } + } + + /// + public void SaveProfile() + { + Profile? profile = _profileConfigurationSubject.Value?.Profile; + if (profile == null) + return; + + _profileService.SaveProfile(profile, true); + } + + /// + public async Task SaveProfileAsync() + { + await Task.Run(SaveProfile); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Services/PropertyInput/PropertyInputRegistration.cs b/src/Avalonia/Artemis.UI.Shared/Services/PropertyInput/PropertyInputRegistration.cs new file mode 100644 index 000000000..40951a2c2 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Services/PropertyInput/PropertyInputRegistration.cs @@ -0,0 +1,52 @@ +using System; +using Artemis.Core; +using Artemis.UI.Shared.Services.ProfileEditor; + +namespace Artemis.UI.Shared.Services.PropertyInput +{ + /// + /// Represents a property input registration registered through + /// + public class PropertyInputRegistration + { + private readonly IPropertyInputService _propertyInputService; + + internal PropertyInputRegistration(IPropertyInputService propertyInputService, Plugin plugin, Type supportedType, Type viewModelType) + { + _propertyInputService = propertyInputService; + Plugin = plugin; + SupportedType = supportedType; + ViewModelType = viewModelType; + + if (Plugin != Constants.CorePlugin) + Plugin.Disabled += InstanceOnDisabled; + } + + /// + /// Gets the plugin that registered the property input + /// + public Plugin Plugin { get; } + + /// + /// Gets the type supported by the property input + /// + public Type SupportedType { get; } + + /// + /// Gets the view model type of the property input + /// + public Type ViewModelType { get; } + + internal void Unsubscribe() + { + if (Plugin != Constants.CorePlugin) + Plugin.Disabled -= InstanceOnDisabled; + } + + private void InstanceOnDisabled(object? sender, EventArgs e) + { + // Profile editor service will call Unsubscribe + _propertyInputService.RemovePropertyInput(this); + } + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Services/PropertyInput/PropertyInputService.cs b/src/Avalonia/Artemis.UI.Shared/Services/PropertyInput/PropertyInputService.cs new file mode 100644 index 000000000..528d7e531 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Services/PropertyInput/PropertyInputService.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Artemis.Core; +using Artemis.UI.Shared.Services.Interfaces; + +namespace Artemis.UI.Shared.Services.PropertyInput +{ + internal class PropertyInputService : IPropertyInputService + { + private readonly List _registeredPropertyEditors; + + public PropertyInputService() + { + _registeredPropertyEditors = new List(); + RegisteredPropertyEditors = new ReadOnlyCollection(_registeredPropertyEditors); + } + + /// + public ReadOnlyCollection RegisteredPropertyEditors { get; } + + /// + public PropertyInputRegistration RegisterPropertyInput(Plugin plugin) where T : PropertyInputViewModel + { + throw new NotImplementedException(); + } + + /// + public PropertyInputRegistration RegisterPropertyInput(Type viewModelType, Plugin plugin) + { + throw new NotImplementedException(); + } + + /// + public void RemovePropertyInput(PropertyInputRegistration registration) + { + throw new NotImplementedException(); + } + + /// + public bool CanCreatePropertyInputViewModel(ILayerProperty layerProperty) + { + throw new NotImplementedException(); + } + + /// + public PropertyInputViewModel? CreatePropertyInputViewModel(LayerProperty layerProperty) + { + throw new NotImplementedException(); + } + } + + public interface IPropertyInputService : IArtemisSharedUIService + { + /// + /// Gets a read-only collection of all registered property editors + /// + ReadOnlyCollection RegisteredPropertyEditors { get; } + + /// + /// Registers a new property input view model used in the profile editor for the generic type defined in + /// + /// Note: DataBindingProperty will remove itself on plugin disable so you don't have to + /// + /// + /// + PropertyInputRegistration RegisterPropertyInput(Plugin plugin) where T : PropertyInputViewModel; + + /// + /// Registers a new property input view model used in the profile editor for the generic type defined in + /// + /// Note: DataBindingProperty will remove itself on plugin disable so you don't have to + /// + /// + /// + /// + PropertyInputRegistration RegisterPropertyInput(Type viewModelType, Plugin plugin); + + /// + /// Removes the property input view model + /// + /// + void RemovePropertyInput(PropertyInputRegistration registration); + + /// + /// Determines if there is a matching registration for the provided layer property + /// + /// The layer property to try to find a view model for + bool CanCreatePropertyInputViewModel(ILayerProperty layerProperty); + + /// + /// If a matching registration is found, creates a new supporting + /// + /// + PropertyInputViewModel? CreatePropertyInputViewModel(LayerProperty layerProperty); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Services/PropertyInput/PropertyInputViewModel.cs b/src/Avalonia/Artemis.UI.Shared/Services/PropertyInput/PropertyInputViewModel.cs new file mode 100644 index 000000000..be92954d3 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Services/PropertyInput/PropertyInputViewModel.cs @@ -0,0 +1,213 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Reactive.Linq; +using Artemis.Core; +using Artemis.UI.Shared.Services.ProfileEditor; +using Artemis.UI.Shared.Services.ProfileEditor.Commands; +using Avalonia.Controls.Mixins; +using ReactiveUI; + +namespace Artemis.UI.Shared.Services.PropertyInput; + +/// +/// Represents the base class for a property input view model that is used to edit layer properties +/// +/// The type of property this input view model supports +public abstract class PropertyInputViewModel : PropertyInputViewModel +{ + [AllowNull] + private T _inputValue; + private bool _inputDragging; + private T _dragStartValue; + private TimeSpan _time; + + /// + /// Creates a new instance of the class + /// + protected PropertyInputViewModel(LayerProperty layerProperty, IProfileEditorService profileEditorService, IPropertyInputService propertyInputService) + { + LayerProperty = layerProperty; + ProfileEditorService = profileEditorService; + PropertyInputService = propertyInputService; + + _inputValue = default!; + _dragStartValue = default!; + + this.WhenActivated(d => + { + ProfileEditorService.Time.Subscribe(t => _time = t).DisposeWith(d); + UpdateInputValue(); + + Observable.FromEventPattern(x => LayerProperty.Updated += x, x => LayerProperty.Updated -= x) + .Subscribe(_ => UpdateInputValue()) + .DisposeWith(d); + Observable.FromEventPattern(x => LayerProperty.CurrentValueSet += x, x => LayerProperty.CurrentValueSet -= x) + .Subscribe(_ => UpdateInputValue()) + .DisposeWith(d); + Observable.FromEventPattern(x => LayerProperty.DataBinding.DataBindingEnabled += x, x => LayerProperty.DataBinding.DataBindingEnabled -= x) + .Subscribe(_ => UpdateDataBinding()) + .DisposeWith(d); + Observable.FromEventPattern(x => LayerProperty.DataBinding.DataBindingDisabled += x, x => LayerProperty.DataBinding.DataBindingDisabled -= x) + .Subscribe(_ => UpdateDataBinding()) + .DisposeWith(d); + }); + } + + /// + /// Gets the layer property this view model is editing + /// + public LayerProperty LayerProperty { get; } + + /// + /// Gets a boolean indicating whether the layer property should be enabled + /// + public bool IsEnabled => !LayerProperty.HasDataBinding; + + /// + /// Gets the profile editor service + /// + public IProfileEditorService ProfileEditorService { get; } + + /// + /// Gets the property input service + /// + public IPropertyInputService PropertyInputService { get; } + + /// + /// Gets or sets a boolean indicating whether the input is currently being dragged + /// + /// Only applicable when using something like a , see + /// and + /// + /// + public bool InputDragging + { + get => _inputDragging; + private set => this.RaiseAndSetIfChanged(ref _inputDragging, value); + } + + /// + /// Gets or sets the input value + /// + [AllowNull] + public T InputValue + { + get => _inputValue; + set + { + this.RaiseAndSetIfChanged(ref _inputValue, value); + ApplyInputValue(); + } + } + + internal override object InternalGuard { get; } = new(); + + /// + /// Called by the view input drag has started + /// + /// To use, add the following to DraggableFloat in your xaml: DragStarted="{s:Action InputDragStarted}" + /// + /// + public void InputDragStarted(object sender, EventArgs e) + { + InputDragging = true; + _dragStartValue = GetDragStartValue(); + } + + /// + /// Called by the view when input drag has ended + /// + /// To use, add the following to DraggableFloat in your xaml: DragEnded="{s:Action InputDragEnded}" + /// + /// + public void InputDragEnded(object sender, EventArgs e) + { + InputDragging = false; + ProfileEditorService.ExecuteCommand(new UpdateLayerProperty(LayerProperty, _inputValue, _dragStartValue, _time)); + } + + /// + /// Called when the input value has been applied to the layer property + /// + protected virtual void OnInputValueApplied() + { + } + + /// + /// Called when the input value has changed + /// + protected virtual void OnInputValueChanged() + { + } + + /// + /// Called when data bindings have been enabled or disabled on the layer property + /// + protected virtual void OnDataBindingsChanged() + { + } + + protected virtual T GetDragStartValue() + { + return InputValue; + } + + /// + /// Applies the input value to the layer property + /// + protected void ApplyInputValue() + { + OnInputValueChanged(); + LayerProperty.SetCurrentValue(_inputValue, _time); + OnInputValueApplied(); + + if (InputDragging) + ProfileEditorService.ChangeTime(_time); + else + ProfileEditorService.ExecuteCommand(new UpdateLayerProperty(LayerProperty, _inputValue, _time)); + } + + private void UpdateInputValue() + { + // Avoid unnecessary UI updates and validator cycles + if (_inputValue != null && _inputValue.Equals(LayerProperty.CurrentValue) || _inputValue == null && LayerProperty.CurrentValue == null) + return; + + // Override the input value + _inputValue = LayerProperty.CurrentValue; + + // Notify a change in the input value + OnInputValueChanged(); + this.RaisePropertyChanged(nameof(InputValue)); + } + + private void UpdateDataBinding() + { + this.RaisePropertyChanged(nameof(IsEnabled)); + OnDataBindingsChanged(); + } + + private void LayerPropertyOnUpdated(object? sender, EventArgs e) + { + UpdateInputValue(); + } + + private void OnDataBindingChange(object? sender, DataBindingEventArgs e) + { + this.RaisePropertyChanged(nameof(IsEnabled)); + OnDataBindingsChanged(); + } +} + +/// +/// For internal use only, implement instead. +/// +public abstract class PropertyInputViewModel : ActivatableViewModelBase +{ + /// + /// Prevents this type being implemented directly, implement + /// instead. + /// + // ReSharper disable once UnusedMember.Global + internal abstract object InternalGuard { get; } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Services/WindowService/ExceptionDialogView.axaml b/src/Avalonia/Artemis.UI.Shared/Services/Window/ExceptionDialogView.axaml similarity index 100% rename from src/Avalonia/Artemis.UI.Shared/Services/WindowService/ExceptionDialogView.axaml rename to src/Avalonia/Artemis.UI.Shared/Services/Window/ExceptionDialogView.axaml diff --git a/src/Avalonia/Artemis.UI.Shared/Services/WindowService/ExceptionDialogView.axaml.cs b/src/Avalonia/Artemis.UI.Shared/Services/Window/ExceptionDialogView.axaml.cs similarity index 100% rename from src/Avalonia/Artemis.UI.Shared/Services/WindowService/ExceptionDialogView.axaml.cs rename to src/Avalonia/Artemis.UI.Shared/Services/Window/ExceptionDialogView.axaml.cs diff --git a/src/Avalonia/Artemis.UI.Shared/Services/WindowService/ExceptionDialogViewModel.cs b/src/Avalonia/Artemis.UI.Shared/Services/Window/ExceptionDialogViewModel.cs similarity index 100% rename from src/Avalonia/Artemis.UI.Shared/Services/WindowService/ExceptionDialogViewModel.cs rename to src/Avalonia/Artemis.UI.Shared/Services/Window/ExceptionDialogViewModel.cs diff --git a/src/Avalonia/Artemis.UI.Shared/Services/WindowService/WindowService.cs b/src/Avalonia/Artemis.UI.Shared/Services/Window/WindowService.cs similarity index 100% rename from src/Avalonia/Artemis.UI.Shared/Services/WindowService/WindowService.cs rename to src/Avalonia/Artemis.UI.Shared/Services/Window/WindowService.cs diff --git a/src/Avalonia/Artemis.UI/Artemis.UI.csproj b/src/Avalonia/Artemis.UI/Artemis.UI.csproj index 4bd007991..01e3602d7 100644 --- a/src/Avalonia/Artemis.UI/Artemis.UI.csproj +++ b/src/Avalonia/Artemis.UI/Artemis.UI.csproj @@ -51,6 +51,12 @@ DebugSettingsView.axaml + + ProfileElementPropertiesView.axaml + + + BrushConfigurationWindowView.axaml + SidebarCategoryEditView.axaml diff --git a/src/Avalonia/Artemis.UI/Converters/PropertyTreeMarginConverter.cs b/src/Avalonia/Artemis.UI/Converters/PropertyTreeMarginConverter.cs new file mode 100644 index 000000000..71aad6a8f --- /dev/null +++ b/src/Avalonia/Artemis.UI/Converters/PropertyTreeMarginConverter.cs @@ -0,0 +1,28 @@ +using System; +using System.Globalization; +using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree; +using Avalonia; +using Avalonia.Data.Converters; + +namespace Artemis.UI.Converters; + +public class PropertyTreeMarginConverter : IValueConverter +{ + public double Length { get; set; } + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is TreeGroupViewModel treeGroupViewModel) + return new Thickness(Length * treeGroupViewModel.GetDepth(), 0, 0, 0); + // TODO + // if (value is ITreePropertyViewModel treePropertyViewModel) + // return new Thickness(Length * treePropertyViewModel.GetDepth(), 0, 0, 0); + + return new Thickness(0); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} \ 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 7d26bec62..0e699c8f6 100644 --- a/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs +++ b/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs @@ -3,11 +3,14 @@ using Artemis.Core; using Artemis.UI.Screens.Device; using Artemis.UI.Screens.Plugins; using Artemis.UI.Screens.ProfileEditor; +using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties; +using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree; using Artemis.UI.Screens.ProfileEditor.ProfileTree; using Artemis.UI.Screens.Settings; using Artemis.UI.Screens.Sidebar; using Artemis.UI.Screens.SurfaceEditor; using Artemis.UI.Services; +using DynamicData.Binding; using ReactiveUI; namespace Artemis.UI.Ninject.Factories @@ -58,4 +61,18 @@ namespace Artemis.UI.Ninject.Factories FolderTreeItemViewModel FolderTreeItemViewModel(TreeItemViewModel? parent, Folder folder); LayerTreeItemViewModel LayerTreeItemViewModel(TreeItemViewModel? parent, Layer layer); } + + public interface ILayerPropertyVmFactory : IVmFactory + { + ProfileElementPropertyViewModel ProfileElementPropertyViewModel(ILayerProperty layerProperty); + ProfileElementPropertyGroupViewModel ProfileElementPropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup); + + TreeGroupViewModel TreeGroupViewModel(ProfileElementPropertyGroupViewModel profileElementPropertyGroupViewModel); + // TimelineGroupViewModel TimelineGroupViewModel(ProfileElementPropertiesViewModel profileElementPropertiesViewModel); + + // TreeViewModel TreeViewModel(ProfileElementPropertiesViewModel profileElementPropertiesViewModel, IObservableCollection profileElementPropertyGroups); + // EffectsViewModel EffectsViewModel(ProfileElementPropertiesViewModel profileElementPropertiesViewModel); + // TimelineViewModel TimelineViewModel(ProfileElementPropertiesViewModel profileElementPropertiesViewModel, IObservableCollection profileElementPropertyGroups); + // TimelineSegmentViewModel TimelineSegmentViewModel(SegmentViewModelType segment, IObservableCollection profileElementPropertyGroups); + } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Commands/AddProfileElement.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Commands/AddProfileElement.cs deleted file mode 100644 index dced97951..000000000 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Commands/AddProfileElement.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using Artemis.Core; -using Artemis.UI.Services.ProfileEditor; - -namespace Artemis.UI.Screens.ProfileEditor.Commands -{ - public class AddProfileElement : IProfileEditorCommand, IDisposable - { - private readonly int _index; - private readonly RenderProfileElement _subject; - private readonly ProfileElement _target; - private bool _isAdded; - - public AddProfileElement(RenderProfileElement subject, ProfileElement target, int index) - { - _subject = subject; - _target = target; - _index = index; - - DisplayName = subject switch - { - Layer => "Add layer", - Folder => "Add folder", - _ => throw new ArgumentException("Type of subject is not supported") - }; - } - - /// - public void Dispose() - { - if (!_isAdded) - _subject.Dispose(); - } - - #region Implementation of IProfileEditorCommand - - /// - public string DisplayName { get; } - - /// - public void Execute() - { - _isAdded = true; - _target.AddChild(_subject, _index); - } - - /// - public void Undo() - { - _isAdded = false; - _target.RemoveChild(_subject); - } - - #endregion - } -} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Commands/RemoveProfileElement.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Commands/RemoveProfileElement.cs deleted file mode 100644 index 6bd983906..000000000 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Commands/RemoveProfileElement.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using Artemis.Core; -using Artemis.UI.Exceptions; -using Artemis.UI.Services.ProfileEditor; - -namespace Artemis.UI.Screens.ProfileEditor.Commands -{ - public class RemoveProfileElement : IProfileEditorCommand, IDisposable - { - private readonly int _index; - private readonly RenderProfileElement _subject; - private readonly ProfileElement _target; - private bool _isRemoved; - - public RemoveProfileElement(RenderProfileElement subject) - { - if (subject.Parent == null) - throw new ArtemisUIException("Can't remove a subject that has no parent"); - - _subject = subject; - _target = _subject.Parent; - _index = _subject.Children.IndexOf(_subject); - - DisplayName = subject switch - { - Layer => "Remove layer", - Folder => "Remove folder", - _ => throw new ArgumentException("Type of subject is not supported") - }; - } - - /// - public void Dispose() - { - if (_isRemoved) - _subject.Dispose(); - } - - #region Implementation of IProfileEditorCommand - - /// - public string DisplayName { get; } - - /// - public void Execute() - { - _isRemoved = true; - _target.RemoveChild(_subject); - _subject.Deactivate(); - } - - /// - public void Undo() - { - _isRemoved = false; - _subject.Activate(); - _target.AddChild(_subject, _index); - - } - - #endregion - } -} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarView.axaml index 98a327fe4..568caaa3c 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarView.axaml @@ -4,7 +4,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Screens.ProfileEditor.Panels.MenuBar.MenuBarView"> + x:Class="Artemis.UI.Screens.ProfileEditor.MenuBar.MenuBarView"> diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarView.axaml.cs index 153fe97f5..469ca47de 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarView.axaml.cs @@ -1,10 +1,8 @@ -using Avalonia; -using Avalonia.Controls; using Avalonia.Interactivity; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; -namespace Artemis.UI.Screens.ProfileEditor.Panels.MenuBar +namespace Artemis.UI.Screens.ProfileEditor.MenuBar { public partial class MenuBarView : ReactiveUserControl { diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs index a4927ff55..84456eac9 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs @@ -1,10 +1,10 @@ using System; using System.Reactive.Disposables; -using Artemis.UI.Services.ProfileEditor; using Artemis.UI.Shared; +using Artemis.UI.Shared.Services.ProfileEditor; using ReactiveUI; -namespace Artemis.UI.Screens.ProfileEditor.Panels.MenuBar +namespace Artemis.UI.Screens.ProfileEditor.MenuBar { public class MenuBarViewModel : ActivatableViewModelBase { diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertiesView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertiesView.axaml new file mode 100644 index 000000000..ead36d35f --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertiesView.axaml @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertiesView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertiesView.axaml.cs new file mode 100644 index 000000000..ae979f444 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertiesView.axaml.cs @@ -0,0 +1,18 @@ +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties +{ + public partial class ProfileElementPropertiesView : ReactiveUserControl + { + public ProfileElementPropertiesView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertiesViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertiesViewModel.cs new file mode 100644 index 000000000..ba22901ab --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertiesViewModel.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reactive; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using Artemis.Core; +using Artemis.Core.LayerEffects; +using Artemis.UI.Ninject.Factories; +using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Services.ProfileEditor; +using ReactiveUI; + +namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties; + +public class ProfileElementPropertiesViewModel : ActivatableViewModelBase +{ + private readonly ILayerPropertyVmFactory _layerPropertyVmFactory; + private readonly IProfileEditorService _profileEditorService; + private ProfileElementPropertyGroupViewModel? _brushPropertyGroup; + private ObservableAsPropertyHelper? _profileElement; + + /// + public ProfileElementPropertiesViewModel(IProfileEditorService profileEditorService, ILayerPropertyVmFactory layerPropertyVmFactory) + { + _profileEditorService = profileEditorService; + _layerPropertyVmFactory = layerPropertyVmFactory; + PropertyGroupViewModels = new ObservableCollection(); + + // Subscribe to events of the latest selected profile element - borrowed from https://stackoverflow.com/a/63950940 + this.WhenAnyValue(x => x.ProfileElement) + .Select(p => p is Layer l + ? Observable.FromEventPattern(x => l.LayerBrushUpdated += x, x => l.LayerBrushUpdated -= x) + : Observable.Never>()) + .Switch() + .Subscribe(_ => ApplyEffects()); + this.WhenAnyValue(x => x.ProfileElement) + .Select(p => p != null + ? Observable.FromEventPattern(x => p.LayerEffectsUpdated += x, x => p.LayerEffectsUpdated -= x) + : Observable.Never>()) + .Switch() + .Subscribe(_ => ApplyLayerBrush()); + + // React to service profile element changes as long as the VM is active + this.WhenActivated(d => + { + _profileElement = _profileEditorService.ProfileElement.ToProperty(this, vm => vm.ProfileElement).DisposeWith(d); + _profileEditorService.ProfileElement.Subscribe(p => PopulateProperties(p)).DisposeWith(d); + }); + } + + public RenderProfileElement? ProfileElement => _profileElement?.Value; + public Layer? Layer => _profileElement?.Value as Layer; + public ObservableCollection PropertyGroupViewModels { get; } + + private void PopulateProperties(RenderProfileElement? renderProfileElement) + { + PropertyGroupViewModels.Clear(); + _brushPropertyGroup = null; + + if (ProfileElement == null) + return; + + // Add layer root groups + if (Layer != null) + { + PropertyGroupViewModels.Add(_layerPropertyVmFactory.ProfileElementPropertyGroupViewModel(Layer.General)); + PropertyGroupViewModels.Add(_layerPropertyVmFactory.ProfileElementPropertyGroupViewModel(Layer.Transform)); + ApplyLayerBrush(false); + } + + ApplyEffects(); + } + + private void ApplyLayerBrush(bool sortProperties = true) + { + if (Layer == null) + return; + + bool hideRenderRelatedProperties = Layer.LayerBrush != null && Layer.LayerBrush.SupportsTransformation; + + Layer.General.ShapeType.IsHidden = hideRenderRelatedProperties; + Layer.General.BlendMode.IsHidden = hideRenderRelatedProperties; + Layer.Transform.IsHidden = hideRenderRelatedProperties; + + if (_brushPropertyGroup != null) + { + PropertyGroupViewModels.Remove(_brushPropertyGroup); + _brushPropertyGroup = null; + } + + if (Layer.LayerBrush?.BaseProperties != null) + { + _brushPropertyGroup = _layerPropertyVmFactory.ProfileElementPropertyGroupViewModel(Layer.LayerBrush.BaseProperties); + PropertyGroupViewModels.Add(_brushPropertyGroup); + } + + if (sortProperties) + SortProperties(); + } + + private void ApplyEffects(bool sortProperties = true) + { + if (ProfileElement == null) + return; + + // Remove VMs of effects no longer applied on the layer + List toRemove = PropertyGroupViewModels + .Where(l => l.LayerPropertyGroup.LayerEffect != null && !ProfileElement.LayerEffects.Contains(l.LayerPropertyGroup.LayerEffect)) + .ToList(); + foreach (ProfileElementPropertyGroupViewModel profileElementPropertyGroupViewModel in toRemove) + PropertyGroupViewModels.Remove(profileElementPropertyGroupViewModel); + + foreach (BaseLayerEffect layerEffect in ProfileElement.LayerEffects) + { + if (PropertyGroupViewModels.Any(l => l.LayerPropertyGroup.LayerEffect == layerEffect) || layerEffect.BaseProperties == null) + continue; + + PropertyGroupViewModels.Add(_layerPropertyVmFactory.ProfileElementPropertyGroupViewModel(layerEffect.BaseProperties)); + } + + if (sortProperties) + SortProperties(); + } + + private void SortProperties() + { + // Get all non-effect properties + List nonEffectProperties = PropertyGroupViewModels + .Where(l => l.TreeGroupViewModel.GroupType != LayerPropertyGroupType.LayerEffectRoot) + .ToList(); + // Order the effects + List effectProperties = PropertyGroupViewModels + .Where(l => l.TreeGroupViewModel.GroupType == LayerPropertyGroupType.LayerEffectRoot && l.LayerPropertyGroup.LayerEffect != null) + .OrderBy(l => l.LayerPropertyGroup.LayerEffect?.Order) + .ToList(); + + // Put the non-effect properties in front + for (int index = 0; index < nonEffectProperties.Count; index++) + { + ProfileElementPropertyGroupViewModel layerPropertyGroupViewModel = nonEffectProperties[index]; + if (PropertyGroupViewModels.IndexOf(layerPropertyGroupViewModel) != index) + PropertyGroupViewModels.Move(PropertyGroupViewModels.IndexOf(layerPropertyGroupViewModel), index); + } + + // Put the effect properties after, sorted by their order + for (int index = 0; index < effectProperties.Count; index++) + { + ProfileElementPropertyGroupViewModel layerPropertyGroupViewModel = effectProperties[index]; + if (PropertyGroupViewModels.IndexOf(layerPropertyGroupViewModel) != index + nonEffectProperties.Count) + PropertyGroupViewModels.Move(PropertyGroupViewModels.IndexOf(layerPropertyGroupViewModel), index + nonEffectProperties.Count); + } + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertyGroupViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertyGroupViewModel.cs new file mode 100644 index 000000000..bd7e5ea2c --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertyGroupViewModel.cs @@ -0,0 +1,40 @@ +using System.Collections.ObjectModel; +using Artemis.Core; +using Artemis.UI.Ninject.Factories; +using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree; +using Artemis.UI.Shared; +using ReactiveUI; + +namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties; + +public class ProfileElementPropertyGroupViewModel : ViewModelBase +{ + private bool _isVisible; + private bool _isExpanded; + + public ProfileElementPropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, ILayerPropertyVmFactory layerPropertyVmFactory) + { + Children = new ObservableCollection(); + LayerPropertyGroup = layerPropertyGroup; + TreeGroupViewModel = layerPropertyVmFactory.TreeGroupViewModel(this); + + IsVisible = !LayerPropertyGroup.IsHidden; + // TODO: Update visiblity on change, can't do it atm because not sure how to unsubscribe from the event + } + + public ObservableCollection Children { get; } + public LayerPropertyGroup LayerPropertyGroup { get; } + public TreeGroupViewModel TreeGroupViewModel { get; } + + public bool IsVisible + { + get => _isVisible; + set => this.RaiseAndSetIfChanged(ref _isVisible, value); + } + + public bool IsExpanded + { + get => _isExpanded; + set => this.RaiseAndSetIfChanged(ref _isExpanded, value); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertyViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertyViewModel.cs new file mode 100644 index 000000000..da3519ae9 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertyViewModel.cs @@ -0,0 +1,20 @@ +using Artemis.Core; +using Artemis.UI.Ninject.Factories; +using Artemis.UI.Shared.Services.ProfileEditor; + +namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties; + +public class ProfileElementPropertyViewModel +{ + private readonly ILayerPropertyVmFactory _layerPropertyVmFactory; + private readonly IProfileEditorService _profileEditorService; + + public ProfileElementPropertyViewModel(ILayerProperty layerProperty, IProfileEditorService profileEditorService, ILayerPropertyVmFactory layerPropertyVmFactory) + { + LayerProperty = layerProperty; + _profileEditorService = profileEditorService; + _layerPropertyVmFactory = layerPropertyVmFactory; + } + + public ILayerProperty LayerProperty { get; } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreeGroupView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreeGroupView.axaml new file mode 100644 index 000000000..af78df0b0 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreeGroupView.axaml @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + General + + + + + + Transform + + + + + + + Brush -  + + + + + Extra options available! + + + + + + + + + + + + + + + Effect + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreeGroupView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreeGroupView.axaml.cs new file mode 100644 index 000000000..4a9482d73 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreeGroupView.axaml.cs @@ -0,0 +1,18 @@ +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree +{ + public partial class TreeGroupView : ReactiveUserControl + { + public TreeGroupView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreeGroupViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreeGroupViewModel.cs new file mode 100644 index 000000000..dbdd25b59 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreeGroupViewModel.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Linq; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Artemis.Core; +using Artemis.Core.LayerBrushes; +using Artemis.Core.LayerEffects; +using Artemis.UI.Exceptions; +using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Windows; +using Artemis.UI.Shared; +using Artemis.UI.Shared.LayerBrushes; +using Artemis.UI.Shared.LayerEffects; +using Artemis.UI.Shared.Services.Interfaces; +using Artemis.UI.Shared.Services.ProfileEditor; +using Ninject; +using Ninject.Parameters; +using ReactiveUI; + +namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree; + +public class TreeGroupViewModel : ActivatableViewModelBase +{ + private readonly IWindowService _windowService; + private readonly IProfileEditorService _profileEditorService; + private BrushConfigurationWindowViewModel? _brushConfigurationWindowViewModel; + private EffectConfigurationWindowViewModel? _effectConfigurationWindowViewModel; + + public TreeGroupViewModel(ProfileElementPropertyGroupViewModel profileElementPropertyGroupViewModel, IWindowService windowService, IProfileEditorService profileEditorService) + { + _windowService = windowService; + _profileEditorService = profileEditorService; + ProfileElementPropertyGroupViewModel = profileElementPropertyGroupViewModel; + DetermineGroupType(); + + this.WhenActivated(d => + { + ProfileElementPropertyGroupViewModel.WhenAnyValue(vm => vm.IsExpanded).Subscribe(_ => this.RaisePropertyChanged(nameof(Children))).DisposeWith(d); + Disposable.Create(CloseViewModels).DisposeWith(d); + }); + } + + + public ProfileElementPropertyGroupViewModel ProfileElementPropertyGroupViewModel { get; } + public LayerPropertyGroup LayerPropertyGroup => ProfileElementPropertyGroupViewModel.LayerPropertyGroup; + public ObservableCollection? Children => ProfileElementPropertyGroupViewModel.IsExpanded ? ProfileElementPropertyGroupViewModel.Children : null; + + public LayerPropertyGroupType GroupType { get; private set; } + + public async Task OpenBrushSettings() + { + BaseLayerBrush? layerBrush = LayerPropertyGroup.LayerBrush; + if (layerBrush?.ConfigurationDialog is not LayerBrushConfigurationDialog configurationViewModel) + return; + + try + { + // Limit to one constructor, there's no need to have more and it complicates things anyway + ConstructorInfo[] constructors = configurationViewModel.Type.GetConstructors(); + if (constructors.Length != 1) + throw new ArtemisUIException("Brush configuration dialogs must have exactly one constructor"); + + // Find the BaseLayerBrush parameter, it is required by the base constructor so its there for sure + ParameterInfo brushParameter = constructors.First().GetParameters().First(p => typeof(BaseLayerBrush).IsAssignableFrom(p.ParameterType)); + ConstructorArgument argument = new(brushParameter.Name!, layerBrush); + BrushConfigurationViewModel viewModel = (BrushConfigurationViewModel) layerBrush.Descriptor.Provider.Plugin.Kernel!.Get(configurationViewModel.Type, argument); + + _brushConfigurationWindowViewModel = new BrushConfigurationWindowViewModel(viewModel, configurationViewModel); + await _windowService.ShowDialogAsync(_brushConfigurationWindowViewModel); + + // Save changes after the dialog closes + await _profileEditorService.SaveProfileAsync(); + } + catch (Exception e) + { + _windowService.ShowExceptionDialog("An exception occurred while trying to show the brush's settings window", e); + } + } + + public async Task OpenEffectSettings() + { + BaseLayerEffect? layerEffect = LayerPropertyGroup.LayerEffect; + if (layerEffect?.ConfigurationDialog is not LayerEffectConfigurationDialog configurationViewModel) + return; + + try + { + // Limit to one constructor, there's no need to have more and it complicates things anyway + ConstructorInfo[] constructors = configurationViewModel.Type.GetConstructors(); + if (constructors.Length != 1) + throw new ArtemisUIException("Effect configuration dialogs must have exactly one constructor"); + + // Find the BaseLayerEffect parameter, it is required by the base constructor so its there for sure + ParameterInfo effectParameter = constructors.First().GetParameters().First(p => typeof(BaseLayerEffect).IsAssignableFrom(p.ParameterType)); + ConstructorArgument argument = new(effectParameter.Name!, layerEffect); + EffectConfigurationViewModel viewModel = (EffectConfigurationViewModel)layerEffect.Descriptor.Provider.Plugin.Kernel!.Get(configurationViewModel.Type, argument); + + _effectConfigurationWindowViewModel = new EffectConfigurationWindowViewModel(viewModel, configurationViewModel); + await _windowService.ShowDialogAsync(_effectConfigurationWindowViewModel); + + // Save changes after the dialog closes + await _profileEditorService.SaveProfileAsync(); + } + catch (Exception e) + { + _windowService.ShowExceptionDialog("An exception occurred while trying to show the effect's settings window", e); + } + } + + public async Task RenameEffect() + { + await _windowService.ShowConfirmContentDialog("Not yet implemented", "Try again later :p"); + } + + public async Task DeleteEffect() + { + await _windowService.ShowConfirmContentDialog("Not yet implemented", "Try again later :p"); + } + + public double GetDepth() + { + int depth = 0; + LayerPropertyGroup? current = LayerPropertyGroup.Parent; + while (current != null) + { + depth++; + current = current.Parent; + } + + return depth; + } + + private void CloseViewModels() + { + _effectConfigurationWindowViewModel?.Close(null); + _brushConfigurationWindowViewModel?.Close(null); + } + + private void DetermineGroupType() + { + if (LayerPropertyGroup is LayerGeneralProperties) + GroupType = LayerPropertyGroupType.General; + else if (LayerPropertyGroup is LayerTransformProperties) + GroupType = LayerPropertyGroupType.Transform; + else if (LayerPropertyGroup.Parent == null && LayerPropertyGroup.LayerBrush != null) + GroupType = LayerPropertyGroupType.LayerBrushRoot; + else if (LayerPropertyGroup.Parent == null && LayerPropertyGroup.LayerEffect != null) + GroupType = LayerPropertyGroupType.LayerEffectRoot; + else + GroupType = LayerPropertyGroupType.None; + } +} + +public enum LayerPropertyGroupType +{ + General, + Transform, + LayerBrushRoot, + LayerEffectRoot, + None +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreePropertyView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreePropertyView.axaml new file mode 100644 index 000000000..83248e419 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreePropertyView.axaml @@ -0,0 +1,8 @@ + + Welcome to Avalonia! + diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreePropertyView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreePropertyView.axaml.cs new file mode 100644 index 000000000..9dbc4d083 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreePropertyView.axaml.cs @@ -0,0 +1,19 @@ +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; +using ReactiveUI; + +namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree +{ + public partial class TreePropertyView : ReactiveUserControl + { + public TreePropertyView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreePropertyViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreePropertyViewModel.cs new file mode 100644 index 000000000..0ca6424eb --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreePropertyViewModel.cs @@ -0,0 +1,19 @@ +using Artemis.Core; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Services.PropertyInput; + +namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree; + +internal class TreePropertyViewModel : ActivatableViewModelBase +{ + public TreePropertyViewModel(LayerProperty layerProperty, ProfileElementPropertyViewModel layerPropertyViewModel, IPropertyInputService propertyInputService) + { + LayerProperty = layerProperty; + LayerPropertyViewModel = layerPropertyViewModel; + PropertyInputViewModel = propertyInputService.CreatePropertyInputViewModel(LayerProperty); + } + + public LayerProperty LayerProperty { get; } + public ProfileElementPropertyViewModel LayerPropertyViewModel { get; } + public PropertyInputViewModel? PropertyInputViewModel { get; } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Windows/BrushConfigurationWindowView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Windows/BrushConfigurationWindowView.axaml new file mode 100644 index 000000000..353d93d61 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Windows/BrushConfigurationWindowView.axaml @@ -0,0 +1,16 @@ + + + + + diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Windows/BrushConfigurationWindowView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Windows/BrushConfigurationWindowView.axaml.cs new file mode 100644 index 000000000..000f65596 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Windows/BrushConfigurationWindowView.axaml.cs @@ -0,0 +1,27 @@ +using System.ComponentModel; +using Avalonia; +using Avalonia.Markup.Xaml; + +namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Windows; + +public class BrushConfigurationWindowView : ReactiveCoreWindow +{ + public BrushConfigurationWindowView() + { + InitializeComponent(); +#if DEBUG + this.AttachDevTools(); +#endif + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + Closing += OnClosing; + } + + private void OnClosing(object? sender, CancelEventArgs e) + { + e.Cancel = ViewModel?.CanClose() ?? true; + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Windows/BrushConfigurationWindowViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Windows/BrushConfigurationWindowViewModel.cs new file mode 100644 index 000000000..51d8c380a --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Windows/BrushConfigurationWindowViewModel.cs @@ -0,0 +1,32 @@ +using System; +using Artemis.UI.Shared; +using Artemis.UI.Shared.LayerBrushes; +using Artemis.UI.Shared.LayerEffects; +using Avalonia.Threading; + +namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Windows; + +public class BrushConfigurationWindowViewModel : DialogViewModelBase +{ + public BrushConfigurationWindowViewModel(BrushConfigurationViewModel configurationViewModel, LayerBrushConfigurationDialog configuration) + { + ConfigurationViewModel = configurationViewModel; + Configuration = configuration; + + ConfigurationViewModel.CloseRequested += ConfigurationViewModelOnCloseRequested; + } + + public BrushConfigurationViewModel ConfigurationViewModel { get; } + public LayerBrushConfigurationDialog Configuration { get; } + + public bool CanClose() + { + return ConfigurationViewModel.CanClose() && Dispatcher.UIThread.InvokeAsync(async () => await ConfigurationViewModel.CanCloseAsync()).GetAwaiter().GetResult(); + } + + private void ConfigurationViewModelOnCloseRequested(object? sender, EventArgs e) + { + if (CanClose()) + Close(null); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Windows/EffectConfigurationWindowView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Windows/EffectConfigurationWindowView.axaml new file mode 100644 index 000000000..ed573d41a --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Windows/EffectConfigurationWindowView.axaml @@ -0,0 +1,16 @@ + + + + + diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Windows/EffectConfigurationWindowView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Windows/EffectConfigurationWindowView.axaml.cs new file mode 100644 index 000000000..2e03a2339 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Windows/EffectConfigurationWindowView.axaml.cs @@ -0,0 +1,27 @@ +using System.ComponentModel; +using Avalonia; +using Avalonia.Markup.Xaml; + +namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Windows; + +public class EffectConfigurationWindowView : ReactiveCoreWindow +{ + public EffectConfigurationWindowView() + { + InitializeComponent(); +#if DEBUG + this.AttachDevTools(); +#endif + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + Closing += OnClosing; + } + + private void OnClosing(object? sender, CancelEventArgs e) + { + e.Cancel = ViewModel?.CanClose() ?? true; + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Windows/EffectConfigurationWindowViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Windows/EffectConfigurationWindowViewModel.cs new file mode 100644 index 000000000..f9289f450 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Windows/EffectConfigurationWindowViewModel.cs @@ -0,0 +1,31 @@ +using System; +using Artemis.UI.Shared; +using Artemis.UI.Shared.LayerEffects; +using Avalonia.Threading; + +namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Windows; + +public class EffectConfigurationWindowViewModel : DialogViewModelBase +{ + public EffectConfigurationWindowViewModel(EffectConfigurationViewModel configurationViewModel, LayerEffectConfigurationDialog configuration) + { + ConfigurationViewModel = configurationViewModel; + Configuration = configuration; + + ConfigurationViewModel.CloseRequested += ConfigurationViewModelOnCloseRequested; + } + + public EffectConfigurationViewModel ConfigurationViewModel { get; } + public LayerEffectConfigurationDialog Configuration { get; } + + public bool CanClose() + { + return ConfigurationViewModel.CanClose() && Dispatcher.UIThread.InvokeAsync(async () => await ConfigurationViewModel.CanCloseAsync()).GetAwaiter().GetResult(); + } + + private void ConfigurationViewModelOnCloseRequested(object? sender, EventArgs e) + { + if (CanClose()) + Close(null); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemViewModel.cs index a2f4627b9..5055f6d35 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemViewModel.cs @@ -1,7 +1,7 @@ using Artemis.Core; using Artemis.UI.Ninject.Factories; -using Artemis.UI.Services.ProfileEditor; using Artemis.UI.Shared.Services.Interfaces; +using Artemis.UI.Shared.Services.ProfileEditor; namespace Artemis.UI.Screens.ProfileEditor.ProfileTree { diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemViewModel.cs index 69908ad6a..b7bb5f087 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemViewModel.cs @@ -1,7 +1,7 @@ using Artemis.Core; using Artemis.UI.Ninject.Factories; -using Artemis.UI.Services.ProfileEditor; using Artemis.UI.Shared.Services.Interfaces; +using Artemis.UI.Shared.Services.ProfileEditor; namespace Artemis.UI.Screens.ProfileEditor.ProfileTree { diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeViewModel.cs index 80a6bcf8d..d4e730438 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeViewModel.cs @@ -5,8 +5,8 @@ using System.Linq; using System.Reactive.Disposables; using Artemis.Core; using Artemis.UI.Ninject.Factories; -using Artemis.UI.Services.ProfileEditor; using Artemis.UI.Shared.Services.Interfaces; +using Artemis.UI.Shared.Services.ProfileEditor; using ReactiveUI; namespace Artemis.UI.Screens.ProfileEditor.ProfileTree diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/TreeItemViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/TreeItemViewModel.cs index 980563191..e3662b8ca 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/TreeItemViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/TreeItemViewModel.cs @@ -8,10 +8,10 @@ using System.Reactive.Linq; using System.Threading.Tasks; using Artemis.Core; using Artemis.UI.Ninject.Factories; -using Artemis.UI.Screens.ProfileEditor.Commands; -using Artemis.UI.Services.ProfileEditor; using Artemis.UI.Shared; using Artemis.UI.Shared.Services.Interfaces; +using Artemis.UI.Shared.Services.ProfileEditor; +using Artemis.UI.Shared.Services.ProfileEditor.Commands; using ReactiveUI; namespace Artemis.UI.Screens.ProfileEditor.ProfileTree diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorViewModel.cs index 2931181fb..9889a534f 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorViewModel.cs @@ -3,8 +3,8 @@ using System.Collections.ObjectModel; using System.Reactive.Disposables; using Artemis.Core; using Artemis.Core.Services; -using Artemis.UI.Services.ProfileEditor; using Artemis.UI.Shared; +using Artemis.UI.Shared.Services.ProfileEditor; using ReactiveUI; namespace Artemis.UI.Screens.ProfileEditor.VisualEditor diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorTitleBarViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorTitleBarViewModel.cs index c30c92144..699b17c36 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorTitleBarViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorTitleBarViewModel.cs @@ -1,4 +1,4 @@ -using Artemis.UI.Screens.ProfileEditor.Panels.MenuBar; +using Artemis.UI.Screens.ProfileEditor.MenuBar; using Artemis.UI.Services.Interfaces; using Artemis.UI.Shared; diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.axaml index 288d7d170..37f8b5d90 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.axaml @@ -63,7 +63,7 @@ - Properties/timeline + diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs index b535f9419..00cfb1863 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs @@ -1,10 +1,11 @@ using System; using System.Reactive.Disposables; using Artemis.Core; -using Artemis.UI.Screens.ProfileEditor.Panels.MenuBar; +using Artemis.UI.Screens.ProfileEditor.MenuBar; +using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties; using Artemis.UI.Screens.ProfileEditor.ProfileTree; using Artemis.UI.Screens.ProfileEditor.VisualEditor; -using Artemis.UI.Services.ProfileEditor; +using Artemis.UI.Shared.Services.ProfileEditor; using Ninject; using ReactiveUI; @@ -12,8 +13,8 @@ namespace Artemis.UI.Screens.ProfileEditor { public class ProfileEditorViewModel : MainScreenViewModel { - private ProfileConfiguration? _profileConfiguration; - private ProfileEditorHistory? _history; + private ObservableAsPropertyHelper? _profileConfiguration; + private ObservableAsPropertyHelper? _history; /// public ProfileEditorViewModel(IScreen hostScreen, @@ -22,37 +23,30 @@ namespace Artemis.UI.Screens.ProfileEditor VisualEditorViewModel visualEditorViewModel, ProfileTreeViewModel profileTreeViewModel, ProfileEditorTitleBarViewModel profileEditorTitleBarViewModel, - MenuBarViewModel menuBarViewModel) + MenuBarViewModel menuBarViewModel, + ProfileElementPropertiesViewModel profileElementPropertiesViewModel) : base(hostScreen, "profile-editor") { VisualEditorViewModel = visualEditorViewModel; ProfileTreeViewModel = profileTreeViewModel; + ProfileElementPropertiesViewModel = profileElementPropertiesViewModel; if (OperatingSystem.IsWindows()) TitleBarViewModel = profileEditorTitleBarViewModel; else MenuBarViewModel = menuBarViewModel; - - this.WhenActivated(d => profileEditorService.ProfileConfiguration.WhereNotNull().Subscribe(p => ProfileConfiguration = p).DisposeWith(d)); - this.WhenActivated(d => profileEditorService.History.Subscribe(history => History = history).DisposeWith(d)); + this.WhenActivated(d => _profileConfiguration = profileEditorService.ProfileConfiguration.ToProperty(this, vm => vm.ProfileConfiguration).DisposeWith(d)); + this.WhenActivated(d => _history = profileEditorService.History.ToProperty(this, vm => vm.History).DisposeWith(d)); } public VisualEditorViewModel VisualEditorViewModel { get; } public ProfileTreeViewModel ProfileTreeViewModel { get; } public MenuBarViewModel? MenuBarViewModel { get; } + public ProfileElementPropertiesViewModel ProfileElementPropertiesViewModel { get; } - public ProfileConfiguration? ProfileConfiguration - { - get => _profileConfiguration; - set => this.RaiseAndSetIfChanged(ref _profileConfiguration, value); - } - - public ProfileEditorHistory? History - { - get => _history; - set => this.RaiseAndSetIfChanged(ref _history, value); - } + public ProfileConfiguration? ProfileConfiguration => _profileConfiguration?.Value; + public ProfileEditorHistory? History => _history?.Value; public void OpenUrl(string url) { diff --git a/src/Avalonia/Artemis.UI/Screens/Root/RootViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Root/RootViewModel.cs index 72e4f7674..197bb922c 100644 --- a/src/Avalonia/Artemis.UI/Screens/Root/RootViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Root/RootViewModel.cs @@ -9,7 +9,7 @@ using Artemis.UI.Screens.Sidebar; using Artemis.UI.Services.Interfaces; using Artemis.UI.Shared; using Artemis.UI.Shared.Services.Interfaces; -using Artemis.UI.Shared.Services.MainWindowService; +using Artemis.UI.Shared.Services.MainWindow; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; diff --git a/src/Avalonia/Artemis.UI/Screens/Sidebar/SidebarCategoryViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Sidebar/SidebarCategoryViewModel.cs index a18d9afb8..cc87bfda5 100644 --- a/src/Avalonia/Artemis.UI/Screens/Sidebar/SidebarCategoryViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Sidebar/SidebarCategoryViewModel.cs @@ -6,10 +6,10 @@ using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.Ninject.Factories; -using Artemis.UI.Services.ProfileEditor; using Artemis.UI.Shared; using Artemis.UI.Shared.Services.Builders; using Artemis.UI.Shared.Services.Interfaces; +using Artemis.UI.Shared.Services.ProfileEditor; using ReactiveUI; namespace Artemis.UI.Screens.Sidebar diff --git a/src/Avalonia/Artemis.UI/Screens/Sidebar/SidebarViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Sidebar/SidebarViewModel.cs index 96777e626..aa72a7b5e 100644 --- a/src/Avalonia/Artemis.UI/Screens/Sidebar/SidebarViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Sidebar/SidebarViewModel.cs @@ -11,10 +11,10 @@ using Artemis.UI.Screens.ProfileEditor; using Artemis.UI.Screens.Settings; using Artemis.UI.Screens.SurfaceEditor; using Artemis.UI.Screens.Workshop; -using Artemis.UI.Services.ProfileEditor; using Artemis.UI.Shared; using Artemis.UI.Shared.Services.Builders; using Artemis.UI.Shared.Services.Interfaces; +using Artemis.UI.Shared.Services.ProfileEditor; using Material.Icons; using Ninject; using ReactiveUI; diff --git a/src/Avalonia/Artemis.UI/Services/ProfileEditor/ProfileEditorService.cs b/src/Avalonia/Artemis.UI/Services/ProfileEditor/ProfileEditorService.cs deleted file mode 100644 index 2a84b223d..000000000 --- a/src/Avalonia/Artemis.UI/Services/ProfileEditor/ProfileEditorService.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Reactive.Linq; -using System.Reactive.Subjects; -using Artemis.Core; -using Artemis.UI.Exceptions; - -namespace Artemis.UI.Services.ProfileEditor -{ - public class ProfileEditorService : IProfileEditorService - { - private readonly Dictionary _profileEditorHistories = new(); - private readonly BehaviorSubject _profileConfigurationSubject = new(null); - private readonly BehaviorSubject _profileElementSubject = new(null); - - public ProfileEditorService() - { - ProfileConfiguration = _profileConfigurationSubject.AsObservable().DistinctUntilChanged(); - ProfileElement = _profileElementSubject.AsObservable().DistinctUntilChanged(); - History = Observable.Defer(() => Observable.Return(GetHistory(_profileConfigurationSubject.Value))).Concat(ProfileConfiguration.Select(GetHistory)); - } - - private ProfileEditorHistory? GetHistory(ProfileConfiguration? profileConfiguration) - { - if (profileConfiguration == null) - return null; - if (_profileEditorHistories.TryGetValue(profileConfiguration, out ProfileEditorHistory? history)) - return history; - - ProfileEditorHistory newHistory = new(profileConfiguration); - _profileEditorHistories.Add(profileConfiguration, newHistory); - return newHistory; - } - - public IObservable ProfileConfiguration { get; } - public IObservable ProfileElement { get; } - public IObservable History { get; } - - public void ChangeCurrentProfileConfiguration(ProfileConfiguration? profileConfiguration) - { - _profileConfigurationSubject.OnNext(profileConfiguration); - } - - public void ChangeCurrentProfileElement(RenderProfileElement? renderProfileElement) - { - _profileElementSubject.OnNext(renderProfileElement); - } - - public void ExecuteCommand(IProfileEditorCommand command) - { - ProfileEditorHistory? history = GetHistory(_profileConfigurationSubject.Value); - if (history == null) - throw new ArtemisUIException("Can't execute a command when there's no active profile configuration"); - - history.Execute.Execute(command).Subscribe(); - } - } -} \ No newline at end of file