diff --git a/README.md b/README.md
index 754d9301c..6a9775308 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
Artemis adds highly configurable support for several games to a range of RGB keyboards, mice and headsets.
### Check out our [Wiki](https://wiki.artemis-rgb.com) and more specifically, the [getting started guide](https://wiki.artemis-rgb.com/en/guides/user).
-**Pre-release download**: https://github.com/SpoinkyNL/Artemis/releases (pre-release means your profiles may break at any given time!)
+**Pre-release download**: https://artemis-rgb.com/
**Plugin documentation**: https://artemis-rgb.com/docs/
**Please note that even though we have plugins for each brand supported by RGB.NET, they have not been thoroughly tested due to a lack of hardware. If you run into any issues please let us know on Discord.**
diff --git a/src/Artemis.Core/Plugins/PluginInfo.cs b/src/Artemis.Core/Plugins/PluginInfo.cs
index 1eebd03c6..91e49eeab 100644
--- a/src/Artemis.Core/Plugins/PluginInfo.cs
+++ b/src/Artemis.Core/Plugins/PluginInfo.cs
@@ -29,6 +29,8 @@ public class PluginInfo : CorePropertyChanged, IPrerequisitesSubject
private Uri? _website;
private Uri? _helpPage;
private bool _hotReloadSupported;
+ private Uri? _license;
+ private string? _licenseName;
internal PluginInfo()
{
@@ -103,6 +105,26 @@ public class PluginInfo : CorePropertyChanged, IPrerequisitesSubject
get => _helpPage;
set => SetAndNotify(ref _helpPage, value);
}
+
+ ///
+ /// Gets or sets the help page of this plugin
+ ///
+ [JsonProperty]
+ public Uri? License
+ {
+ get => _license;
+ set => SetAndNotify(ref _license, value);
+ }
+
+ ///
+ /// Gets or sets the author of this plugin
+ ///
+ [JsonProperty]
+ public string? LicenseName
+ {
+ get => _licenseName;
+ set => SetAndNotify(ref _licenseName, value);
+ }
///
/// The plugins display icon that's shown in the settings see for
diff --git a/src/Artemis.UI.Shared/Controls/ArtemisIcon.axaml.cs b/src/Artemis.UI.Shared/Controls/ArtemisIcon.axaml.cs
index 6e174235d..7aadc91bf 100644
--- a/src/Artemis.UI.Shared/Controls/ArtemisIcon.axaml.cs
+++ b/src/Artemis.UI.Shared/Controls/ArtemisIcon.axaml.cs
@@ -43,30 +43,18 @@ public partial class ArtemisIcon : UserControl
// If it's a string there are several options
else if (Icon is string iconString)
{
+ // An URI pointing to an image
+ if (ImageRegex.IsMatch(iconString))
+ {
+ Image image = new() {Source = new Bitmap(iconString), VerticalAlignment = VerticalAlignment.Stretch, HorizontalAlignment = HorizontalAlignment.Stretch};
+ RenderOptions.SetBitmapInterpolationMode(image, BitmapInterpolationMode.HighQuality);
+ Content = image;
+ }
// An enum defined as a string
- if (Enum.TryParse(iconString, true, out MaterialIconKind parsedIcon))
+ else if (Enum.TryParse(iconString, true, out MaterialIconKind parsedIcon))
{
Content = new MaterialIcon {Kind = parsedIcon, Width = Bounds.Width, Height = Bounds.Height};
}
- // An URI pointing to an image
- else if (ImageRegex.IsMatch(iconString))
- {
- if (!Fill)
- Content = new Image
- {
- Source = new Bitmap(iconString),
- VerticalAlignment = VerticalAlignment.Stretch,
- HorizontalAlignment = HorizontalAlignment.Stretch
- };
- else
- Content = new Border
- {
- Background = TextElement.GetForeground(this),
- VerticalAlignment = VerticalAlignment.Stretch,
- HorizontalAlignment = HorizontalAlignment.Stretch,
- OpacityMask = new ImageBrush(new Bitmap(iconString))
- };
- }
else
{
Content = new MaterialIcon {Kind = MaterialIconKind.QuestionMark, Width = Bounds.Width, Height = Bounds.Height};
@@ -87,10 +75,10 @@ public partial class ArtemisIcon : UserControl
contentControl.Height = Bounds.Height;
}
}
-
+
private void OnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
- if (e.Property == IconProperty || e.Property == FillProperty)
+ if (e.Property == IconProperty)
Update();
}
@@ -119,21 +107,5 @@ public partial class ArtemisIcon : UserControl
set => SetValue(IconProperty, value);
}
- ///
- /// Gets or sets a boolean indicating whether or not the icon should be filled in with the primary text color of the
- /// theme
- ///
- public static readonly StyledProperty FillProperty = AvaloniaProperty.Register(nameof(Icon));
-
- ///
- /// Gets or sets a boolean indicating whether or not the icon should be filled in with the primary text color of the
- /// theme
- ///
- public bool Fill
- {
- get => GetValue(FillProperty);
- set => SetValue(FillProperty, value);
- }
-
#endregion
}
\ No newline at end of file
diff --git a/src/Artemis.UI.Shared/Controls/Flyouts/MaterialIconPickerFlyout.cs b/src/Artemis.UI.Shared/Controls/Flyouts/MaterialIconPickerFlyout.cs
new file mode 100644
index 000000000..99321b5e0
--- /dev/null
+++ b/src/Artemis.UI.Shared/Controls/Flyouts/MaterialIconPickerFlyout.cs
@@ -0,0 +1,38 @@
+using Avalonia.Controls;
+using FluentAvalonia.UI.Controls;
+
+namespace Artemis.UI.Shared.Flyouts;
+
+///
+/// Defines a flyout that hosts a data model picker.
+///
+public sealed class MaterialIconPickerFlyout : Flyout
+{
+ private MaterialIconPicker.MaterialIconPicker? _picker;
+
+ ///
+ /// Gets the data model picker that the flyout hosts.
+ ///
+ public MaterialIconPicker.MaterialIconPicker MaterialIconPicker => _picker ??= new MaterialIconPicker.MaterialIconPicker();
+
+ ///
+ protected override Control CreatePresenter()
+ {
+ _picker ??= new MaterialIconPicker.MaterialIconPicker();
+ _picker.Flyout = this;
+ FlyoutPresenter presenter = new() {Content = MaterialIconPicker};
+ return presenter;
+ }
+
+ #region Overrides of FlyoutBase
+
+ ///
+ protected override void OnClosed()
+ {
+ if (_picker != null)
+ _picker.Flyout = null;
+ base.OnClosed();
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/src/Artemis.UI.Shared/Controls/MaterialIconPicker/MaterialIconPicker.cs b/src/Artemis.UI.Shared/Controls/MaterialIconPicker/MaterialIconPicker.cs
new file mode 100644
index 000000000..5777b21de
--- /dev/null
+++ b/src/Artemis.UI.Shared/Controls/MaterialIconPicker/MaterialIconPicker.cs
@@ -0,0 +1,147 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Reactive.Linq;
+using System.Text.RegularExpressions;
+using System.Windows.Input;
+using Artemis.UI.Shared.Flyouts;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.Data;
+using Avalonia.LogicalTree;
+using DynamicData;
+using DynamicData.Binding;
+using Material.Icons;
+using ReactiveUI;
+
+namespace Artemis.UI.Shared.MaterialIconPicker;
+
+///
+/// Represents a Material icon picker picker that can be used to search and select a Material icon.
+///
+public partial class MaterialIconPicker : TemplatedControl
+{
+ ///
+ /// Gets or sets the current Material icon.
+ ///
+ public static readonly StyledProperty ValueProperty =
+ AvaloniaProperty.Register(nameof(Value), defaultBindingMode: BindingMode.TwoWay);
+
+ ///
+ /// Gets the command to execute when deleting stops.
+ ///
+ public static readonly DirectProperty SelectIconProperty =
+ AvaloniaProperty.RegisterDirect(nameof(SelectIcon), g => g.SelectIcon);
+
+ private readonly ICommand _selectIcon;
+ private ItemsRepeater? _iconsContainer;
+
+ private SourceList? _iconsSource;
+ private TextBox? _searchBox;
+ private IDisposable? _sub;
+ private readonly Dictionary _enumNames;
+
+ ///
+ public MaterialIconPicker()
+ {
+ _selectIcon = ReactiveCommand.Create(i =>
+ {
+ Value = i;
+ Flyout?.Hide();
+ });
+
+ // Build a list of enum names and values, this is required because a value may have more than one name
+ _enumNames = new Dictionary();
+ MaterialIconKind[] values = Enum.GetValues();
+ string[] names = Enum.GetNames();
+ for (int index = 0; index < names.Length; index++)
+ _enumNames[names[index]] = values[index];
+ }
+
+ ///
+ /// Gets or sets the current Material icon.
+ ///
+ public MaterialIconKind? Value
+ {
+ get => GetValue(ValueProperty);
+ set => SetValue(ValueProperty, value);
+ }
+
+ ///
+ /// Gets the command to execute when deleting stops.
+ ///
+ public ICommand SelectIcon
+ {
+ get => _selectIcon;
+ private init => SetAndRaise(SelectIconProperty, ref _selectIcon, value);
+ }
+
+ internal MaterialIconPickerFlyout? Flyout { get; set; }
+
+ private void Setup()
+ {
+ if (_searchBox == null || _iconsContainer == null)
+ return;
+
+ // Build a list of values, they are not unique because a value with multiple names occurs once per name
+ _iconsSource = new SourceList();
+ _iconsSource.AddRange(Enum.GetValues().Distinct());
+ _sub = _iconsSource.Connect()
+ .Filter(_searchBox.WhenAnyValue(s => s.Text).Throttle(TimeSpan.FromMilliseconds(100)).Select(CreatePredicate))
+ .Sort(SortExpressionComparer.Descending(p => p.ToString()))
+ .Bind(out ReadOnlyObservableCollection icons)
+ .Subscribe();
+ _iconsContainer.ItemsSource = icons;
+ }
+
+ #region Overrides of TemplatedControl
+
+ ///
+ protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
+ {
+ _searchBox = e.NameScope.Find("SearchBox");
+ _iconsContainer = e.NameScope.Find("IconsContainer");
+
+ Setup();
+ base.OnApplyTemplate(e);
+ }
+
+ ///
+ protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
+ {
+ Setup();
+ base.OnAttachedToLogicalTree(e);
+ }
+
+ ///
+ protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
+ {
+ _iconsSource?.Dispose();
+ _iconsSource = null;
+ _sub?.Dispose();
+ _sub = null;
+
+ if (_searchBox != null)
+ _searchBox.Text = "";
+ base.OnDetachedFromLogicalTree(e);
+ }
+
+ private Func CreatePredicate(string? text)
+ {
+ if (string.IsNullOrWhiteSpace(text))
+ return _ => true;
+
+ // Strip out whitespace and find all matching enum values
+ text = StripWhiteSpaceRegex().Replace(text, "");
+ HashSet values = _enumNames.Where(n => n.Key.Contains(text, StringComparison.OrdinalIgnoreCase)).Select(n => n.Value).ToHashSet();
+ // Only show those that matched
+ return data => values.Contains(data);
+ }
+
+ [GeneratedRegex("\\s+")]
+ private static partial Regex StripWhiteSpaceRegex();
+
+ #endregion
+}
\ No newline at end of file
diff --git a/src/Artemis.UI.Shared/Controls/MaterialIconPicker/MaterialIconPickerButton.cs b/src/Artemis.UI.Shared/Controls/MaterialIconPicker/MaterialIconPickerButton.cs
new file mode 100644
index 000000000..53c751ac3
--- /dev/null
+++ b/src/Artemis.UI.Shared/Controls/MaterialIconPicker/MaterialIconPickerButton.cs
@@ -0,0 +1,137 @@
+using System;
+using Artemis.UI.Shared.Flyouts;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.Data;
+using Avalonia.Interactivity;
+using FluentAvalonia.Core;
+using Material.Icons;
+
+namespace Artemis.UI.Shared.MaterialIconPicker;
+
+///
+/// Represents a button that can be used to pick a data model path in a flyout.
+///
+public class MaterialIconPickerButton : TemplatedControl
+{
+ ///
+ /// Gets or sets the placeholder to show when nothing is selected.
+ ///
+ public static readonly StyledProperty PlaceholderProperty =
+ AvaloniaProperty.Register(nameof(Placeholder), "Click to select");
+
+ ///
+ /// Gets or sets the current Material icon.
+ ///
+ public static readonly StyledProperty ValueProperty =
+ AvaloniaProperty.Register(nameof(Value), defaultBindingMode: BindingMode.TwoWay);
+
+ ///
+ /// Gets or sets the desired flyout placement.
+ ///
+ public static readonly StyledProperty PlacementProperty =
+ AvaloniaProperty.Register(nameof(Placement), PlacementMode.BottomEdgeAlignedLeft);
+
+ private Button? _button;
+ private MaterialIconPickerFlyout? _flyout;
+ private bool _flyoutActive;
+
+ ///
+ /// Gets or sets the placeholder to show when nothing is selected.
+ ///
+ public string Placeholder
+ {
+ get => GetValue(PlaceholderProperty);
+ set => SetValue(PlaceholderProperty, value);
+ }
+
+ ///
+ /// Gets or sets the current Material icon.
+ ///
+ public MaterialIconKind? Value
+ {
+ get => GetValue(ValueProperty);
+ set => SetValue(ValueProperty, value);
+ }
+
+ ///
+ /// Gets or sets the desired flyout placement.
+ ///
+ public PlacementMode Placement
+ {
+ get => GetValue(PlacementProperty);
+ set => SetValue(PlacementProperty, value);
+ }
+
+ ///
+ /// Raised when the flyout opens.
+ ///
+ public event TypedEventHandler? FlyoutOpened;
+
+ ///
+ /// Raised when the flyout closes.
+ ///
+ public event TypedEventHandler? FlyoutClosed;
+
+ private void OnButtonClick(object? sender, RoutedEventArgs e)
+ {
+ if (_flyout == null)
+ return;
+
+ MaterialIconPickerFlyout flyout = new();
+ flyout.FlyoutPresenterClasses.Add("material-icon-picker-presenter");
+ flyout.MaterialIconPicker.Value = Value;
+ flyout.Placement = Placement;
+
+ flyout.Closed += FlyoutOnClosed;
+ flyout.ShowAt(this);
+ FlyoutOpened?.Invoke(this, EventArgs.Empty);
+
+ void FlyoutOnClosed(object? closedSender, EventArgs closedEventArgs)
+ {
+ Value = flyout.MaterialIconPicker.Value;
+ flyout.Closed -= FlyoutOnClosed;
+ FlyoutClosed?.Invoke(this, EventArgs.Empty);
+ }
+ }
+
+ #region Overrides of TemplatedControl
+
+ ///
+ protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
+ {
+ if (_button != null)
+ _button.Click -= OnButtonClick;
+ base.OnApplyTemplate(e);
+ _button = e.NameScope.Find
+
+
+