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 @@
TrueTrue
+ True
+ TrueTrue
\ 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">