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 - + diff --git a/src/Artemis.UI/Screens/Plugins/PluginSettingsWindowView.axaml b/src/Artemis.UI/Screens/Plugins/PluginSettingsWindowView.axaml index 7943954b6..66dcbb1c0 100644 --- a/src/Artemis.UI/Screens/Plugins/PluginSettingsWindowView.axaml +++ b/src/Artemis.UI/Screens/Plugins/PluginSettingsWindowView.axaml @@ -4,6 +4,9 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:windowing="clr-namespace:FluentAvalonia.UI.Windowing;assembly=FluentAvalonia" xmlns:plugins="clr-namespace:Artemis.UI.Screens.Plugins" + xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" + xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" + xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.Plugins.PluginSettingsWindowView" x:DataType="plugins:PluginSettingsWindowViewModel" @@ -11,9 +14,63 @@ Title="{CompiledBinding DisplayName}" Width="800" Height="800" + MaxWidth="800" WindowStartupLocation="CenterOwner"> - - + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Plugins/PluginSettingsWindowViewModel.cs b/src/Artemis.UI/Screens/Plugins/PluginSettingsWindowViewModel.cs index 401deb4c0..2ef07a933 100644 --- a/src/Artemis.UI/Screens/Plugins/PluginSettingsWindowViewModel.cs +++ b/src/Artemis.UI/Screens/Plugins/PluginSettingsWindowViewModel.cs @@ -16,4 +16,5 @@ public class PluginSettingsWindowViewModel : ActivatableViewModelBase public PluginConfigurationViewModel ConfigurationViewModel { get; } public Plugin Plugin { get; } + public string LicenseButtonText => Plugin.Info.LicenseName ?? "View license"; } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Plugins/PluginView.axaml b/src/Artemis.UI/Screens/Plugins/PluginView.axaml index f35e54630..b9dc83c55 100644 --- a/src/Artemis.UI/Screens/Plugins/PluginView.axaml +++ b/src/Artemis.UI/Screens/Plugins/PluginView.axaml @@ -12,7 +12,6 @@ - + - - + + + + + + + + +