diff --git a/src/Artemis.Core/Models/Profile/Profile.cs b/src/Artemis.Core/Models/Profile/Profile.cs index c511f9ec9..2a0d96a23 100644 --- a/src/Artemis.Core/Models/Profile/Profile.cs +++ b/src/Artemis.Core/Models/Profile/Profile.cs @@ -17,13 +17,12 @@ public sealed class Profile : ProfileElement private readonly ObservableCollection _scriptConfigurations; private readonly ObservableCollection _scripts; private bool _isFreshImport; - private ProfileElement? _lastSelectedProfileElement; internal Profile(ProfileConfiguration configuration, ProfileEntity profileEntity) : base(null!) { _scripts = new ObservableCollection(); _scriptConfigurations = new ObservableCollection(); - + Opacity = 0d; ShouldDisplay = true; Configuration = configuration; @@ -67,15 +66,6 @@ public sealed class Profile : ProfileElement set => SetAndNotify(ref _isFreshImport, value); } - /// - /// Gets or sets the last selected profile element of this profile - /// - public ProfileElement? LastSelectedProfileElement - { - get => _lastSelectedProfileElement; - set => SetAndNotify(ref _lastSelectedProfileElement, value); - } - /// /// Gets the profile entity this profile uses for persistent storage /// @@ -105,7 +95,7 @@ public sealed class Profile : ProfileElement profileScript.OnProfileUpdated(deltaTime); const double OPACITY_PER_SECOND = 1; - + if (ShouldDisplay && Opacity < 1) Opacity = Math.Clamp(Opacity + OPACITY_PER_SECOND * deltaTime, 0d, 1d); if (!ShouldDisplay && Opacity > 0) @@ -123,14 +113,14 @@ public sealed class Profile : ProfileElement foreach (ProfileScript profileScript in Scripts) profileScript.OnProfileRendering(canvas, canvas.LocalClipBounds); - + SKPaint? opacityPaint = null; bool applyOpacityLayer = Configuration.FadeInAndOut && Opacity < 1; - + if (applyOpacityLayer) { opacityPaint = new SKPaint(); - opacityPaint.Color = new SKColor(0, 0, 0, (byte)(255d * Easings.CubicEaseInOut(Opacity))); + opacityPaint.Color = new SKColor(0, 0, 0, (byte) (255d * Easings.CubicEaseInOut(Opacity))); canvas.SaveLayer(opacityPaint); } @@ -242,20 +232,13 @@ public sealed class Profile : ProfileElement AddChild(new Folder(this, this, rootFolder)); } - List renderElements = GetAllRenderElements(); - - if (ProfileEntity.LastSelectedProfileElement != Guid.Empty) - LastSelectedProfileElement = renderElements.FirstOrDefault(f => f.EntityId == ProfileEntity.LastSelectedProfileElement); - else - LastSelectedProfileElement = null; - while (_scriptConfigurations.Any()) RemoveScriptConfiguration(_scriptConfigurations[0]); foreach (ScriptConfiguration scriptConfiguration in ProfileEntity.ScriptConfigurations.Select(e => new ScriptConfiguration(e))) AddScriptConfiguration(scriptConfiguration); // Load node scripts last since they may rely on the profile structure being in place - foreach (RenderProfileElement renderProfileElement in renderElements) + foreach (RenderProfileElement renderProfileElement in GetAllRenderElements()) renderProfileElement.LoadNodeScript(); } @@ -312,7 +295,6 @@ public sealed class Profile : ProfileElement ProfileEntity.Id = EntityId; ProfileEntity.Name = Configuration.Name; ProfileEntity.IsFreshImport = IsFreshImport; - ProfileEntity.LastSelectedProfileElement = LastSelectedProfileElement?.EntityId ?? Guid.Empty; foreach (ProfileElement profileElement in Children) profileElement.Save(); diff --git a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs index 06a2090d3..b0dec1d29 100644 --- a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs +++ b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs @@ -147,16 +147,7 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable get => _activationConditionMet; private set => SetAndNotify(ref _activationConditionMet, value); } - - /// - /// Gets or sets a boolean indicating whether this profile configuration is being edited - /// - public bool IsBeingEdited - { - get => _isBeingEdited; - set => SetAndNotify(ref _isBeingEdited, value); - } - + /// /// Gets the profile of this profile configuration /// @@ -243,8 +234,6 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable { if (_disposed) throw new ObjectDisposedException("ProfileConfiguration"); - if (IsBeingEdited) - return true; if (Category.IsSuspended || IsSuspended || IsMissingModule) return false; diff --git a/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs b/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs index 98e0bc322..4d2907af4 100644 --- a/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs +++ b/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs @@ -26,19 +26,26 @@ public interface IProfileService : IArtemisService ReadOnlyCollection ProfileConfigurations { get; } /// - /// Gets or sets a boolean indicating whether hotkeys are enabled. + /// Gets or sets the focused profile configuration which is rendered exclusively. /// - bool HotkeysEnabled { get; set; } + ProfileConfiguration? FocusProfile { get; set; } /// - /// Gets or sets a boolean indicating whether rendering should only be done for profiles being edited. + /// Gets or sets the profile element which is rendered exclusively. /// - bool RenderForEditor { get; set; } + ProfileElement? FocusProfileElement { get; set; } /// - /// Gets or sets the profile element to focus on while rendering for the editor. + /// Gets or sets a value indicating whether the currently focused profile should receive updates. /// - ProfileElement? EditorFocus { get; set; } + bool UpdateFocusProfile { get; set; } + + /// + /// Creates a copy of the provided profile configuration. + /// + /// The profile configuration to clone. + /// The resulting clone. + ProfileConfiguration CloneProfileConfiguration(ProfileConfiguration profileConfiguration); /// /// Activates the profile of the given with the currently active surface. diff --git a/src/Artemis.Core/Services/Storage/ProfileService.cs b/src/Artemis.Core/Services/Storage/ProfileService.cs index ba1f496e7..103c1c2f1 100644 --- a/src/Artemis.Core/Services/Storage/ProfileService.cs +++ b/src/Artemis.Core/Services/Storage/ProfileService.cs @@ -15,14 +15,14 @@ namespace Artemis.Core.Services; internal class ProfileService : IProfileService { private readonly ILogger _logger; + private readonly IRgbService _rgbService; + private readonly IProfileCategoryRepository _profileCategoryRepository; + private readonly IPluginManagementService _pluginManagementService; private readonly List _pendingKeyboardEvents = new(); - private readonly IPluginManagementService _pluginManagementService; private readonly List _profileCategories; - private readonly IProfileCategoryRepository _profileCategoryRepository; private readonly IProfileRepository _profileRepository; private readonly List _renderExceptions = new(); - private readonly IRgbService _rgbService; private readonly List _updateExceptions = new(); private DateTime _lastRenderExceptionLog; private DateTime _lastUpdateExceptionLog; @@ -45,7 +45,6 @@ internal class ProfileService : IProfileService _pluginManagementService.PluginFeatureEnabled += PluginManagementServiceOnPluginFeatureToggled; _pluginManagementService.PluginFeatureDisabled += PluginManagementServiceOnPluginFeatureToggled; - HotkeysEnabled = true; inputService.KeyboardKeyUp += InputServiceOnKeyboardKeyUp; if (!_profileCategories.Any()) @@ -53,140 +52,21 @@ internal class ProfileService : IProfileService UpdateModules(); } - private void InputServiceOnKeyboardKeyUp(object? sender, ArtemisKeyboardKeyEventArgs e) - { - if (!HotkeysEnabled) - return; - - lock (_profileCategories) - { - _pendingKeyboardEvents.Add(e); - } - } - - /// - /// Populates all missing LEDs on all currently active profiles - /// - private void ActiveProfilesPopulateLeds() - { - foreach (ProfileConfiguration profileConfiguration in ProfileConfigurations) - { - if (profileConfiguration.Profile == null) continue; - profileConfiguration.Profile.PopulateLeds(_rgbService.EnabledDevices); - - if (!profileConfiguration.Profile.IsFreshImport) continue; - _logger.Debug("Profile is a fresh import, adapting to surface - {profile}", profileConfiguration.Profile); - AdaptProfile(profileConfiguration.Profile); - } - } - - private void UpdateModules() - { - lock (_profileRepository) - { - List modules = _pluginManagementService.GetFeaturesOfType(); - foreach (ProfileCategory profileCategory in _profileCategories) - { - foreach (ProfileConfiguration profileConfiguration in profileCategory.ProfileConfigurations) - profileConfiguration.LoadModules(modules); - } - } - } - - private void RgbServiceOnLedsChanged(object? sender, EventArgs e) - { - ActiveProfilesPopulateLeds(); - } - - private void PluginManagementServiceOnPluginFeatureToggled(object? sender, PluginFeatureEventArgs e) - { - if (e.PluginFeature is Module) - UpdateModules(); - } - - private void ProcessPendingKeyEvents(ProfileConfiguration profileConfiguration) - { - if (profileConfiguration.HotkeyMode == ProfileConfigurationHotkeyMode.None) - return; - - bool before = profileConfiguration.IsSuspended; - foreach (ArtemisKeyboardKeyEventArgs e in _pendingKeyboardEvents) - { - if (profileConfiguration.HotkeyMode == ProfileConfigurationHotkeyMode.Toggle) - { - if (profileConfiguration.EnableHotkey != null && profileConfiguration.EnableHotkey.MatchesEventArgs(e)) - profileConfiguration.IsSuspended = !profileConfiguration.IsSuspended; - } - else - { - if (profileConfiguration.IsSuspended && profileConfiguration.EnableHotkey != null && profileConfiguration.EnableHotkey.MatchesEventArgs(e)) - profileConfiguration.IsSuspended = false; - else if (!profileConfiguration.IsSuspended && profileConfiguration.DisableHotkey != null && profileConfiguration.DisableHotkey.MatchesEventArgs(e)) - profileConfiguration.IsSuspended = true; - } - } - - // If suspension was changed, save the category - if (before != profileConfiguration.IsSuspended) - SaveProfileCategory(profileConfiguration.Category); - } - - private void CreateDefaultProfileCategories() - { - foreach (DefaultCategoryName defaultCategoryName in Enum.GetValues()) - CreateProfileCategory(defaultCategoryName.ToString()); - } - - private void LogProfileUpdateExceptions() - { - // Only log update exceptions every 10 seconds to avoid spamming the logs - if (DateTime.Now - _lastUpdateExceptionLog < TimeSpan.FromSeconds(10)) - return; - _lastUpdateExceptionLog = DateTime.Now; - - if (!_updateExceptions.Any()) - return; - - // Group by stack trace, that should gather up duplicate exceptions - foreach (IGrouping exceptions in _updateExceptions.GroupBy(e => e.StackTrace)) - { - _logger.Warning(exceptions.First(), - "Exception was thrown {count} times during profile update in the last 10 seconds", - exceptions.Count()); - } - - // When logging is finished start with a fresh slate - _updateExceptions.Clear(); - } - - private void LogProfileRenderExceptions() - { - // Only log update exceptions every 10 seconds to avoid spamming the logs - if (DateTime.Now - _lastRenderExceptionLog < TimeSpan.FromSeconds(10)) - return; - _lastRenderExceptionLog = DateTime.Now; - - if (!_renderExceptions.Any()) - return; - - // Group by stack trace, that should gather up duplicate exceptions - foreach (IGrouping exceptions in _renderExceptions.GroupBy(e => e.StackTrace)) - { - _logger.Warning(exceptions.First(), - "Exception was thrown {count} times during profile render in the last 10 seconds", - exceptions.Count()); - } - - // When logging is finished start with a fresh slate - _renderExceptions.Clear(); - } - - public bool HotkeysEnabled { get; set; } - public bool RenderForEditor { get; set; } - public ProfileElement? EditorFocus { get; set; } + public ProfileConfiguration? FocusProfile { get; set; } + public ProfileElement? FocusProfileElement { get; set; } + public bool UpdateFocusProfile { get; set; } + /// public void UpdateProfiles(double deltaTime) { + // If there is a focus profile update only that, and only if UpdateFocusProfile is true + if (FocusProfile != null) + { + if (UpdateFocusProfile) + FocusProfile.Profile?.Update(deltaTime); + return; + } + lock (_profileCategories) { // Iterate the children in reverse because the first category must be rendered last to end up on top @@ -200,16 +80,11 @@ internal class ProfileService : IProfileService // Process hotkeys that where pressed since this profile last updated ProcessPendingKeyEvents(profileConfiguration); - // Profiles being edited are updated at their own leisure - if (profileConfiguration.IsBeingEdited && RenderForEditor) - continue; - bool shouldBeActive = profileConfiguration.ShouldBeActive(false); if (shouldBeActive) { profileConfiguration.Update(); - if (!profileConfiguration.IsBeingEdited) - shouldBeActive = profileConfiguration.ActivationConditionMet; + shouldBeActive = profileConfiguration.ActivationConditionMet; } try @@ -243,20 +118,18 @@ internal class ProfileService : IProfileService } } + /// public void RenderProfiles(SKCanvas canvas) { + // If there is a focus profile, render only that + if (FocusProfile != null) + { + FocusProfile.Profile?.Render(canvas, SKPointI.Empty, FocusProfileElement); + return; + } + lock (_profileCategories) { - ProfileConfiguration? editedProfileConfiguration = _profileCategories.SelectMany(c => c.ProfileConfigurations).FirstOrDefault(p => p.IsBeingEdited); - if (editedProfileConfiguration != null) - { - editedProfileConfiguration.Profile?.Render(canvas, SKPointI.Empty, RenderForEditor ? EditorFocus : null); - return; - } - - if (RenderForEditor) - return; - // Iterate the children in reverse because the first category must be rendered last to end up on top for (int i = _profileCategories.Count - 1; i > -1; i--) { @@ -282,6 +155,7 @@ internal class ProfileService : IProfileService } } + /// public ReadOnlyCollection ProfileCategories { get @@ -293,6 +167,7 @@ internal class ProfileService : IProfileService } } + /// public ReadOnlyCollection ProfileConfigurations { get @@ -304,6 +179,7 @@ internal class ProfileService : IProfileService } } + /// public void LoadProfileConfigurationIcon(ProfileConfiguration profileConfiguration) { if (profileConfiguration.Icon.IconType == ProfileConfigurationIconType.MaterialIcon) @@ -314,6 +190,7 @@ internal class ProfileService : IProfileService profileConfiguration.Icon.SetIconByStream(profileConfiguration.Entity.IconOriginalFileName, stream); } + /// public void SaveProfileConfigurationIcon(ProfileConfiguration profileConfiguration) { if (profileConfiguration.Icon.IconType == ProfileConfigurationIconType.MaterialIcon) @@ -327,6 +204,13 @@ internal class ProfileService : IProfileService } } + /// + public ProfileConfiguration CloneProfileConfiguration(ProfileConfiguration profileConfiguration) + { + return new ProfileConfiguration(profileConfiguration.Category, profileConfiguration.Entity); + } + + /// public Profile ActivateProfile(ProfileConfiguration profileConfiguration) { if (profileConfiguration.Profile != null) @@ -364,9 +248,10 @@ internal class ProfileService : IProfileService return profile; } + /// public void DeactivateProfile(ProfileConfiguration profileConfiguration) { - if (profileConfiguration.IsBeingEdited) + if (FocusProfile == profileConfiguration) throw new ArtemisCoreException("Cannot disable a profile that is being edited, that's rude"); if (profileConfiguration.Profile == null) return; @@ -378,9 +263,10 @@ internal class ProfileService : IProfileService OnProfileDeactivated(new ProfileConfigurationEventArgs(profileConfiguration)); } + /// public void RequestDeactivation(ProfileConfiguration profileConfiguration) { - if (profileConfiguration.IsBeingEdited) + if (FocusProfile == profileConfiguration) throw new ArtemisCoreException("Cannot disable a profile that is being edited, that's rude"); if (profileConfiguration.Profile == null) return; @@ -388,6 +274,7 @@ internal class ProfileService : IProfileService profileConfiguration.Profile.ShouldDisplay = false; } + /// public void DeleteProfile(ProfileConfiguration profileConfiguration) { DeactivateProfile(profileConfiguration); @@ -401,6 +288,7 @@ internal class ProfileService : IProfileService SaveProfileCategory(profileConfiguration.Category); } + /// public ProfileCategory CreateProfileCategory(string name) { ProfileCategory profileCategory; @@ -415,6 +303,7 @@ internal class ProfileService : IProfileService return profileCategory; } + /// public void DeleteProfileCategory(ProfileCategory profileCategory) { List profileConfigurations = profileCategory.ProfileConfigurations.ToList(); @@ -430,6 +319,7 @@ internal class ProfileService : IProfileService OnProfileCategoryRemoved(new ProfileCategoryEventArgs(profileCategory)); } + /// public ProfileConfiguration CreateProfileConfiguration(ProfileCategory category, string name, string icon) { ProfileConfiguration configuration = new(category, name, icon); @@ -441,6 +331,7 @@ internal class ProfileService : IProfileService return configuration; } + /// public void RemoveProfileConfiguration(ProfileConfiguration profileConfiguration) { profileConfiguration.Category.RemoveProfileConfiguration(profileConfiguration); @@ -454,6 +345,7 @@ internal class ProfileService : IProfileService profileConfiguration.Dispose(); } + /// public void SaveProfileCategory(ProfileCategory profileCategory) { profileCategory.Save(); @@ -465,6 +357,7 @@ internal class ProfileService : IProfileService } } + /// public void SaveProfile(Profile profile, bool includeChildren) { _logger.Debug("Updating profile - Saving {Profile}", profile); @@ -480,6 +373,7 @@ internal class ProfileService : IProfileService _profileRepository.Save(profile.ProfileEntity); } + /// public ProfileConfigurationExportModel ExportProfile(ProfileConfiguration profileConfiguration) { // The profile may not be active and in that case lets activate it real quick @@ -493,6 +387,7 @@ internal class ProfileService : IProfileService }; } + /// public ProfileConfiguration ImportProfile(ProfileCategory category, ProfileConfigurationExportModel exportModel, bool makeUnique, bool markAsFreshImport, string? nameAffix) { @@ -565,6 +460,131 @@ internal class ProfileService : IProfileService _profileRepository.Save(profile.ProfileEntity); } + private void InputServiceOnKeyboardKeyUp(object? sender, ArtemisKeyboardKeyEventArgs e) + { + lock (_profileCategories) + { + _pendingKeyboardEvents.Add(e); + } + } + + /// + /// Populates all missing LEDs on all currently active profiles + /// + private void ActiveProfilesPopulateLeds() + { + foreach (ProfileConfiguration profileConfiguration in ProfileConfigurations) + { + if (profileConfiguration.Profile == null) continue; + profileConfiguration.Profile.PopulateLeds(_rgbService.EnabledDevices); + + if (!profileConfiguration.Profile.IsFreshImport) continue; + _logger.Debug("Profile is a fresh import, adapting to surface - {profile}", profileConfiguration.Profile); + AdaptProfile(profileConfiguration.Profile); + } + } + + private void UpdateModules() + { + lock (_profileRepository) + { + List modules = _pluginManagementService.GetFeaturesOfType(); + foreach (ProfileCategory profileCategory in _profileCategories) + { + foreach (ProfileConfiguration profileConfiguration in profileCategory.ProfileConfigurations) + profileConfiguration.LoadModules(modules); + } + } + } + + private void RgbServiceOnLedsChanged(object? sender, EventArgs e) + { + ActiveProfilesPopulateLeds(); + } + + private void PluginManagementServiceOnPluginFeatureToggled(object? sender, PluginFeatureEventArgs e) + { + if (e.PluginFeature is Module) + UpdateModules(); + } + + private void ProcessPendingKeyEvents(ProfileConfiguration profileConfiguration) + { + if (profileConfiguration.HotkeyMode == ProfileConfigurationHotkeyMode.None) + return; + + bool before = profileConfiguration.IsSuspended; + foreach (ArtemisKeyboardKeyEventArgs e in _pendingKeyboardEvents) + { + if (profileConfiguration.HotkeyMode == ProfileConfigurationHotkeyMode.Toggle) + { + if (profileConfiguration.EnableHotkey != null && profileConfiguration.EnableHotkey.MatchesEventArgs(e)) + profileConfiguration.IsSuspended = !profileConfiguration.IsSuspended; + } + else + { + if (profileConfiguration.IsSuspended && profileConfiguration.EnableHotkey != null && profileConfiguration.EnableHotkey.MatchesEventArgs(e)) + profileConfiguration.IsSuspended = false; + else if (!profileConfiguration.IsSuspended && profileConfiguration.DisableHotkey != null && profileConfiguration.DisableHotkey.MatchesEventArgs(e)) + profileConfiguration.IsSuspended = true; + } + } + + // If suspension was changed, save the category + if (before != profileConfiguration.IsSuspended) + SaveProfileCategory(profileConfiguration.Category); + } + + private void CreateDefaultProfileCategories() + { + foreach (DefaultCategoryName defaultCategoryName in Enum.GetValues()) + CreateProfileCategory(defaultCategoryName.ToString()); + } + + private void LogProfileUpdateExceptions() + { + // Only log update exceptions every 10 seconds to avoid spamming the logs + if (DateTime.Now - _lastUpdateExceptionLog < TimeSpan.FromSeconds(10)) + return; + _lastUpdateExceptionLog = DateTime.Now; + + if (!_updateExceptions.Any()) + return; + + // Group by stack trace, that should gather up duplicate exceptions + foreach (IGrouping exceptions in _updateExceptions.GroupBy(e => e.StackTrace)) + { + _logger.Warning(exceptions.First(), + "Exception was thrown {count} times during profile update in the last 10 seconds", + exceptions.Count()); + } + + // When logging is finished start with a fresh slate + _updateExceptions.Clear(); + } + + private void LogProfileRenderExceptions() + { + // Only log update exceptions every 10 seconds to avoid spamming the logs + if (DateTime.Now - _lastRenderExceptionLog < TimeSpan.FromSeconds(10)) + return; + _lastRenderExceptionLog = DateTime.Now; + + if (!_renderExceptions.Any()) + return; + + // Group by stack trace, that should gather up duplicate exceptions + foreach (IGrouping exceptions in _renderExceptions.GroupBy(e => e.StackTrace)) + { + _logger.Warning(exceptions.First(), + "Exception was thrown {count} times during profile render in the last 10 seconds", + exceptions.Count()); + } + + // When logging is finished start with a fresh slate + _renderExceptions.Clear(); + } + #region Events public event EventHandler? ProfileActivated; diff --git a/src/Artemis.Storage/Entities/Profile/ProfileEntity.cs b/src/Artemis.Storage/Entities/Profile/ProfileEntity.cs index 8a8616ce7..da5bbb903 100644 --- a/src/Artemis.Storage/Entities/Profile/ProfileEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/ProfileEntity.cs @@ -18,7 +18,6 @@ public class ProfileEntity public string Name { get; set; } public bool IsFreshImport { get; set; } - public Guid LastSelectedProfileElement { get; set; } public List Folders { get; set; } public List Layers { get; set; } diff --git a/src/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs b/src/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs index 42390dd3e..f0c4cf085 100644 --- a/src/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs +++ b/src/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs @@ -10,7 +10,6 @@ using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.Shared.Services.MainWindow; using Artemis.UI.Shared.Services.ProfileEditor.Commands; -using Avalonia.Threading; using DynamicData; using Serilog; @@ -140,14 +139,14 @@ internal class ProfileEditorService : IProfileEditorService private void ApplyFocusMode() { if (_suspendedEditingSubject.Value) - _profileService.EditorFocus = null; + _profileService.FocusProfileElement = null; - _profileService.EditorFocus = _focusModeSubject.Value switch + _profileService.FocusProfileElement = _focusModeSubject.Value switch { ProfileEditorFocusMode.None => null, ProfileEditorFocusMode.Folder => _profileElementSubject.Value?.Parent, ProfileEditorFocusMode.Selection => _profileElementSubject.Value, - _ => _profileService.EditorFocus + _ => _profileService.FocusProfileElement }; } @@ -164,52 +163,38 @@ internal class ProfileEditorService : IProfileEditorService public async Task ChangeCurrentProfileConfiguration(ProfileConfiguration? profileConfiguration) { - if (ReferenceEquals(_profileConfigurationSubject.Value, profileConfiguration)) + ProfileConfiguration? previous = _profileConfigurationSubject.Value; + if (ReferenceEquals(previous, profileConfiguration)) return; _logger.Verbose("ChangeCurrentProfileConfiguration {profile}", profileConfiguration); // Stop playing and save the current profile Pause(); - if (_profileConfigurationSubject.Value?.Profile != null) - { - _profileConfigurationSubject.Value.Profile.Reset(); - _profileConfigurationSubject.Value.Profile.LastSelectedProfileElement = _profileElementSubject.Value; - } - await SaveProfileAsync(); - // No need to deactivate the profile, if needed it will be deactivated next update - if (_profileConfigurationSubject.Value != null) - _profileConfigurationSubject.Value.IsBeingEdited = false; - // Deselect whatever profile element was active ChangeCurrentProfileElement(null); + ChangeSuspendedEditing(false); // Close the command scope if one was open _profileEditorHistoryScope?.Dispose(); - // The new profile may need activation - if (profileConfiguration != null) + // Activate the profile and it's mode off of the UI thread + await Task.Run(() => { - await Task.Run(() => - { - profileConfiguration.IsBeingEdited = true; - _moduleService.SetActivationOverride(profileConfiguration.Module); + // Activate the profile if one was provided + if (profileConfiguration != null) _profileService.ActivateProfile(profileConfiguration); - _profileService.RenderForEditor = true; - }); - if (profileConfiguration.Profile?.LastSelectedProfileElement is RenderProfileElement renderProfileElement) - ChangeCurrentProfileElement(renderProfileElement); - } - else - { - _moduleService.SetActivationOverride(null); - _profileService.RenderForEditor = false; - } + // If there is no profile configuration or module, deliberately set the override to null + _moduleService.SetActivationOverride(profileConfiguration?.Module); + }); + _profileService.FocusProfile = profileConfiguration; _profileConfigurationSubject.OnNext(profileConfiguration); + ChangeTime(TimeSpan.Zero); + previous?.Profile?.Reset(); } public void ChangeCurrentProfileElement(RenderProfileElement? renderProfileElement) @@ -238,23 +223,23 @@ internal class ProfileEditorService : IProfileEditorService if (_suspendedEditingSubject.Value == suspend) return; - _suspendedEditingSubject.OnNext(suspend); if (suspend) { Pause(); - _profileService.RenderForEditor = false; + _profileService.UpdateFocusProfile = true; _profileConfigurationSubject.Value?.Profile?.Reset(); } else { if (_profileConfigurationSubject.Value != null) - _profileService.RenderForEditor = true; + _profileService.UpdateFocusProfile = false; Tick(_timeSubject.Value); } + _suspendedEditingSubject.OnNext(suspend); ApplyFocusMode(); } - + public void ChangeFocusMode(ProfileEditorFocusMode focusMode) { if (_focusModeSubject.Value == focusMode) @@ -411,10 +396,8 @@ internal class ProfileEditorService : IProfileEditorService public void SaveProfile() { Profile? profile = _profileConfigurationSubject.Value?.Profile; - if (profile == null) - return; - - _profileService.SaveProfile(profile, true); + if (profile != null) + _profileService.SaveProfile(profile, true); } /// diff --git a/src/Artemis.UI.Shared/Styles/TextBlock.axaml b/src/Artemis.UI.Shared/Styles/TextBlock.axaml index 8a5099cde..6c28e9938 100644 --- a/src/Artemis.UI.Shared/Styles/TextBlock.axaml +++ b/src/Artemis.UI.Shared/Styles/TextBlock.axaml @@ -10,15 +10,10 @@ This is heading 5 This is heading 6 This is a subtitle - - This is heading 1 - This is heading 2 - This is heading 3 - This is heading 4 - This is heading 5 - This is heading 6 - This is a subtitle - + Danger + Warning + Success + Info @@ -81,4 +76,30 @@ + + + + + + + + + + diff --git a/src/Artemis.UI/Artemis.UI.csproj b/src/Artemis.UI/Artemis.UI.csproj index 4dea86a84..27e75baa2 100644 --- a/src/Artemis.UI/Artemis.UI.csproj +++ b/src/Artemis.UI/Artemis.UI.csproj @@ -39,9 +39,17 @@ + + + + + ProfileSelectionStepView.axaml + Code + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs index 7578952da..60a0b548d 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs @@ -16,7 +16,6 @@ using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services.ProfileEditor; using Newtonsoft.Json; using ReactiveUI; -using Serilog; namespace Artemis.UI.Screens.ProfileEditor.MenuBar; @@ -182,7 +181,7 @@ public class MenuBarViewModel : ActivatableViewModelBase if (!await _windowService.ShowConfirmContentDialog("Delete profile", "Are you sure you want to permanently delete this profile?")) return; - if (ProfileConfiguration.IsBeingEdited) + if (_profileService.FocusProfile == ProfileConfiguration) await _router.Navigate("home"); _profileService.RemoveProfileConfiguration(ProfileConfiguration); } diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorView.axaml.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorView.axaml.cs index 3fb88d5c5..f7e4491a5 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorView.axaml.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorView.axaml.cs @@ -1,10 +1,10 @@ using System; using System.Linq; using System.Reactive.Disposables; +using System.Reactive.Linq; using Avalonia; using Avalonia.Controls.PanAndZoom; using Avalonia.Input; -using Avalonia.Markup.Xaml; using Avalonia.Media; using Avalonia.ReactiveUI; using Avalonia.Threading; @@ -19,7 +19,7 @@ public partial class VisualEditorView : ReactiveUserControl ViewModel.AutoFitRequested -= ViewModelOnAutoFitRequested).DisposeWith(d); }); - this.WhenAnyValue(v => v.Bounds).Subscribe(_ => - { - if (!_movedByUser) - AutoFit(true); - }); + this.WhenAnyValue(v => v.Bounds).Where(_ => !_movedByUser).Subscribe(_ => AutoFit(true)); } private void ZoomBorderOnPointerWheelChanged(object? sender, PointerWheelEventArgs e) diff --git a/src/Artemis.UI/Screens/Settings/Updating/ReleaseView.axaml.cs b/src/Artemis.UI/Screens/Settings/Updating/ReleaseView.axaml.cs index 29e385e8a..02cd2454c 100644 --- a/src/Artemis.UI/Screens/Settings/Updating/ReleaseView.axaml.cs +++ b/src/Artemis.UI/Screens/Settings/Updating/ReleaseView.axaml.cs @@ -1,6 +1,4 @@ -using Avalonia; using Avalonia.Controls; -using Avalonia.Markup.Xaml; namespace Artemis.UI.Screens.Settings.Updating; @@ -10,9 +8,4 @@ public partial class ReleaseView : UserControl { InitializeComponent(); } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs b/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs index c0d130ce6..2f45408ed 100644 --- a/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs +++ b/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs @@ -52,9 +52,9 @@ public class ProfileConfigurationEditViewModel : DialogViewModelBase().First().ToString()) + + _profileConfiguration = profileConfiguration == ProfileConfiguration.Empty + ? profileService.CreateProfileConfiguration(profileCategory, "New profile", Enum.GetValues().First().ToString()) : profileConfiguration; _profileName = _profileConfiguration.Name; _iconType = _profileConfiguration.Icon.IconType; @@ -140,7 +140,7 @@ public class ProfileConfigurationEditViewModel : DialogViewModelBase Confirm { get; } public ReactiveCommand Delete { get; } public ReactiveCommand Cancel { get; } - + private async Task ExecuteDelete() { if (IsNew) @@ -148,7 +148,7 @@ public class ProfileConfigurationEditViewModel : DialogViewModelBase _iconType; set => RaiseAndSetIfChanged(ref _iconType, value); } - + public ProfileIconViewModel? SelectedMaterialIcon { get => _selectedMaterialIcon; diff --git a/src/Artemis.UI/Screens/Sidebar/SidebarCategoryViewModel.cs b/src/Artemis.UI/Screens/Sidebar/SidebarCategoryViewModel.cs index 188faa33a..5fd46c94c 100644 --- a/src/Artemis.UI/Screens/Sidebar/SidebarCategoryViewModel.cs +++ b/src/Artemis.UI/Screens/Sidebar/SidebarCategoryViewModel.cs @@ -24,9 +24,9 @@ namespace Artemis.UI.Screens.Sidebar; public class SidebarCategoryViewModel : ActivatableViewModelBase { private readonly IProfileService _profileService; - private readonly IWindowService _windowService; - private readonly ISidebarVmFactory _vmFactory; private readonly IRouter _router; + private readonly ISidebarVmFactory _vmFactory; + private readonly IWindowService _windowService; private ObservableAsPropertyHelper? _isCollapsed; private ObservableAsPropertyHelper? _isSuspended; private SidebarProfileConfigurationViewModel? _selectedProfileConfiguration; @@ -67,9 +67,9 @@ public class SidebarCategoryViewModel : ActivatableViewModelBase .WhereNotNull() .Subscribe(s => _router.Navigate($"profile-editor/{s.ProfileConfiguration.ProfileId}", new RouterNavigationOptions {IgnoreOnPartialMatch = true, RecycleScreens = false})) .DisposeWith(d); - + _router.CurrentPath.WhereNotNull().Subscribe(r => SelectedProfileConfiguration = ProfileConfigurations.FirstOrDefault(c => c.Matches(r))).DisposeWith(d); - + // Update the list of profiles whenever the category fires events Observable.FromEventPattern(x => profileCategory.ProfileConfigurationAdded += x, x => profileCategory.ProfileConfigurationAdded -= x) .Subscribe(e => profileConfigurations.Add(e.EventArgs.ProfileConfiguration)) @@ -77,7 +77,7 @@ public class SidebarCategoryViewModel : ActivatableViewModelBase Observable.FromEventPattern(x => profileCategory.ProfileConfigurationRemoved += x, x => profileCategory.ProfileConfigurationRemoved -= x) .Subscribe(e => profileConfigurations.RemoveMany(profileConfigurations.Items.Where(c => c == e.EventArgs.ProfileConfiguration))) .DisposeWith(d); - + _isCollapsed = ProfileCategory.WhenAnyValue(vm => vm.IsCollapsed).ToProperty(this, vm => vm.IsCollapsed).DisposeWith(d); _isSuspended = ProfileCategory.WhenAnyValue(vm => vm.IsSuspended).ToProperty(this, vm => vm.IsSuspended).DisposeWith(d); }); @@ -136,7 +136,7 @@ public class SidebarCategoryViewModel : ActivatableViewModelBase { if (await _windowService.ShowConfirmContentDialog($"Delete {ProfileCategory.Name}", "Do you want to delete this category and all its profiles?")) { - if (ProfileCategory.ProfileConfigurations.Any(c => c.IsBeingEdited)) + if (ProfileCategory.ProfileConfigurations.Any(c => _profileService.FocusProfile == c)) await _router.Navigate("home"); _profileService.DeleteProfileCategory(ProfileCategory); } @@ -153,7 +153,7 @@ public class SidebarCategoryViewModel : ActivatableViewModelBase } private async Task ExecuteImportProfile() - { + { string[]? result = await _windowService.CreateOpenFileDialog() .HavingFilter(f => f.WithExtension("json").WithName("Artemis profile")) .ShowAsync(); diff --git a/src/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationViewModel.cs b/src/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationViewModel.cs index 98749cb51..aafa9ca52 100644 --- a/src/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationViewModel.cs +++ b/src/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationViewModel.cs @@ -98,7 +98,7 @@ public class SidebarProfileConfigurationViewModel : ActivatableViewModelBase if (!await _windowService.ShowConfirmContentDialog("Delete profile", "Are you sure you want to permanently delete this profile?")) return; - if (ProfileConfiguration.IsBeingEdited) + if (_profileService.FocusProfile == ProfileConfiguration) await _router.Navigate("home"); _profileService.RemoveProfileConfiguration(ProfileConfiguration); } diff --git a/src/Artemis.UI/Screens/Workshop/Categories/CategoriesView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Categories/CategoriesView.axaml.cs index 15ac17ab5..8cb505ab9 100644 --- a/src/Artemis.UI/Screens/Workshop/Categories/CategoriesView.axaml.cs +++ b/src/Artemis.UI/Screens/Workshop/Categories/CategoriesView.axaml.cs @@ -1,6 +1,5 @@ using Avalonia; using Avalonia.Input; -using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; namespace Artemis.UI.Screens.Workshop.Categories; @@ -12,10 +11,6 @@ public partial class CategoriesView : ReactiveUserControl InitializeComponent(); } - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } private void InputElement_OnPointerReleased(object? sender, PointerReleasedEventArgs e) { diff --git a/src/Artemis.UI/Screens/Workshop/CurrentUser/WorkshopLoginView.axaml.cs b/src/Artemis.UI/Screens/Workshop/CurrentUser/WorkshopLoginView.axaml.cs index 064ae6691..9d6092a7f 100644 --- a/src/Artemis.UI/Screens/Workshop/CurrentUser/WorkshopLoginView.axaml.cs +++ b/src/Artemis.UI/Screens/Workshop/CurrentUser/WorkshopLoginView.axaml.cs @@ -1,4 +1,3 @@ -using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; namespace Artemis.UI.Screens.Workshop.CurrentUser; @@ -9,9 +8,4 @@ public partial class WorkshopLoginView : ReactiveUserControl { InitializeComponent(); } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeView.axaml.cs index e28f6659b..4a143850a 100644 --- a/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeView.axaml.cs +++ b/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeView.axaml.cs @@ -1,6 +1,3 @@ -using Avalonia; -using Avalonia.Controls; -using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; namespace Artemis.UI.Screens.Workshop.Home; @@ -11,9 +8,4 @@ public partial class WorkshopHomeView : ReactiveUserControl { InitializeComponent(); } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsView.axaml.cs index 25ca88e9f..1150bd94c 100644 --- a/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsView.axaml.cs +++ b/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsView.axaml.cs @@ -1,4 +1,3 @@ -using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; namespace Artemis.UI.Screens.Workshop.Profile; @@ -9,9 +8,4 @@ public partial class ProfileDetailsView : ReactiveUserControl { InitializeComponent(); } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Profile/ProfilePreviewView.axaml b/src/Artemis.UI/Screens/Workshop/Profile/ProfilePreviewView.axaml new file mode 100644 index 000000000..0b30e7d07 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Profile/ProfilePreviewView.axaml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Artemis.UI/Screens/Workshop/Profile/ProfilePreviewView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Profile/ProfilePreviewView.axaml.cs new file mode 100644 index 000000000..b956e6ecf --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Profile/ProfilePreviewView.axaml.cs @@ -0,0 +1,79 @@ +using System; +using System.Linq; +using System.Reactive.Linq; +using Avalonia; +using Avalonia.Controls.PanAndZoom; +using Avalonia.Input; +using Avalonia.Media; +using Avalonia.ReactiveUI; +using ReactiveUI; + +namespace Artemis.UI.Screens.Workshop.Profile; + +public partial class ProfilePreviewView : ReactiveUserControl +{ + private bool _movedByUser; + + public ProfilePreviewView() + { + InitializeComponent(); + + ZoomBorder.PropertyChanged += ZoomBorderOnPropertyChanged; + ZoomBorder.PointerMoved += ZoomBorderOnPointerMoved; + ZoomBorder.PointerWheelChanged += ZoomBorderOnPointerWheelChanged; + UpdateZoomBorderBackground(); + + this.WhenAnyValue(v => v.Bounds).Where(_ => !_movedByUser).Subscribe(_ => AutoFit()); + } + + private void ZoomBorderOnPointerWheelChanged(object? sender, PointerWheelEventArgs e) + { + _movedByUser = true; + } + + private void ZoomBorderOnPointerMoved(object? sender, PointerEventArgs e) + { + if (e.GetCurrentPoint(ZoomBorder).Properties.IsMiddleButtonPressed) + _movedByUser = true; + } + + private void ZoomBorderOnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property.Name == nameof(ZoomBorder.Background)) + UpdateZoomBorderBackground(); + } + + private void UpdateZoomBorderBackground() + { + if (ZoomBorder.Background is VisualBrush visualBrush) + visualBrush.DestinationRect = new RelativeRect(ZoomBorder.OffsetX * -1, ZoomBorder.OffsetY * -1, 20, 20, RelativeUnit.Absolute); + } + + private void ZoomBorder_OnZoomChanged(object sender, ZoomChangedEventArgs e) + { + UpdateZoomBorderBackground(); + } + + private void AutoFit() + { + if (ViewModel == null || !ViewModel.Devices.Any()) + return; + + double left = ViewModel.Devices.Select(d => d.Rectangle.Left).Min(); + double top = ViewModel.Devices.Select(d => d.Rectangle.Top).Min(); + double bottom = ViewModel.Devices.Select(d => d.Rectangle.Bottom).Max(); + double right = ViewModel.Devices.Select(d => d.Rectangle.Right).Max(); + + // Add a 10 pixel margin around the rect + Rect scriptRect = new(new Point(left - 10, top - 10), new Point(right + 10, bottom + 10)); + + // The scale depends on the available space + double scale = Math.Min(3, Math.Min(Bounds.Width / scriptRect.Width, Bounds.Height / scriptRect.Height)); + + // Pan and zoom to make the script fit + ZoomBorder.Zoom(scale, 0, 0, true); + ZoomBorder.Pan(Bounds.Center.X - scriptRect.Center.X * scale, Bounds.Center.Y - scriptRect.Center.Y * scale, true); + + _movedByUser = false; + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Profile/ProfilePreviewViewModel.cs b/src/Artemis.UI/Screens/Workshop/Profile/ProfilePreviewViewModel.cs new file mode 100644 index 000000000..a874ff268 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Profile/ProfilePreviewViewModel.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reactive.Disposables; +using Artemis.Core; +using Artemis.Core.Services; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Services; +using ReactiveUI; + +namespace Artemis.UI.Screens.Workshop.Profile; + +public class ProfilePreviewViewModel : ActivatableViewModelBase +{ + private readonly IProfileService _profileService; + private readonly IWindowService _windowService; + private ProfileConfiguration? _profileConfiguration; + + public ProfilePreviewViewModel(IProfileService profileService, IRgbService rgbService, IWindowService windowService) + { + _profileService = profileService; + _windowService = windowService; + + Devices = new ObservableCollection(rgbService.EnabledDevices.OrderBy(d => d.ZIndex)); + + this.WhenAnyValue(vm => vm.ProfileConfiguration).Subscribe(_ => Update()); + this.WhenActivated(d => Disposable.Create(() => PreviewProfile(null)).DisposeWith(d)); + } + + public ObservableCollection Devices { get; } + + public ProfileConfiguration? ProfileConfiguration + { + get => _profileConfiguration; + set => RaiseAndSetIfChanged(ref _profileConfiguration, value); + } + + private void Update() + { + try + { + PreviewProfile(ProfileConfiguration); + } + catch (Exception e) + { + _windowService.ShowExceptionDialog("Failed to load preview", e); + } + } + + private void PreviewProfile(ProfileConfiguration? profileConfiguration) + { + _profileService.FocusProfile = profileConfiguration; + _profileService.UpdateFocusProfile = profileConfiguration != null; + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Search/SearchView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Search/SearchView.axaml.cs index e9da03bdb..c819fce33 100644 --- a/src/Artemis.UI/Screens/Workshop/Search/SearchView.axaml.cs +++ b/src/Artemis.UI/Screens/Workshop/Search/SearchView.axaml.cs @@ -1,6 +1,4 @@ -using System; using Avalonia.Controls; -using Avalonia.Markup.Xaml; namespace Artemis.UI.Screens.Workshop.Search; @@ -10,9 +8,4 @@ public partial class SearchView : UserControl { InitializeComponent(); } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/EntryTypeView.axaml.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/EntryTypeView.axaml.cs index d50e2ea6e..3e4df145c 100644 --- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/EntryTypeView.axaml.cs +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/EntryTypeView.axaml.cs @@ -1,4 +1,3 @@ -using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps; @@ -9,9 +8,4 @@ public partial class EntryTypeView : ReactiveUserControl { InitializeComponent(); } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/EntryTypeViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/EntryTypeViewModel.cs index dda445838..9d3c9ea5d 100644 --- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/EntryTypeViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/EntryTypeViewModel.cs @@ -1,6 +1,6 @@ -using System; using System.Reactive; using System.Reactive.Linq; +using Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Profile; using Artemis.WebClient.Workshop; using ReactiveUI; diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/LoginStepView.axaml.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/LoginStepView.axaml.cs index c8b574eee..3b4ec4b2b 100644 --- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/LoginStepView.axaml.cs +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/LoginStepView.axaml.cs @@ -1,4 +1,3 @@ -using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps; @@ -9,9 +8,4 @@ public partial class LoginStepView : ReactiveUserControl { InitializeComponent(); } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileAdaptionHintsLayerViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileAdaptionHintsLayerViewModel.cs new file mode 100644 index 000000000..ade952508 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileAdaptionHintsLayerViewModel.cs @@ -0,0 +1,51 @@ +using System.Reactive; +using System.Reactive.Linq; +using System.Threading.Tasks; +using Artemis.Core; +using Artemis.Core.Services; +using Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Services; +using FluentAvalonia.Core; +using ReactiveUI; + +namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Profile; + +public class ProfileAdaptionHintsLayerViewModel : ViewModelBase +{ + private readonly IWindowService _windowService; + private readonly IProfileService _profileService; + private readonly ObservableAsPropertyHelper _adaptionHintText; + private int _adaptionHintCount; + + public Layer Layer { get; } + + public ProfileAdaptionHintsLayerViewModel(Layer layer, IWindowService windowService, IProfileService profileService) + { + _windowService = windowService; + _profileService = profileService; + _adaptionHintText = this.WhenAnyValue(vm => vm.AdaptionHintCount).Select(c => c == 1 ? "1 adaption hint" : $"{c} adaption hints").ToProperty(this, vm => vm.AdaptionHintText); + + Layer = layer; + EditAdaptionHints = ReactiveCommand.CreateFromTask(ExecuteEditAdaptionHints); + AdaptionHintCount = layer.Adapter.AdaptionHints.Count; + } + + public ReactiveCommand EditAdaptionHints { get; } + + public int AdaptionHintCount + { + get => _adaptionHintCount; + private set => RaiseAndSetIfChanged(ref _adaptionHintCount, value); + } + + public string AdaptionHintText => _adaptionHintText.Value; + + private async Task ExecuteEditAdaptionHints() + { + await _windowService.ShowDialogAsync(Layer); + _profileService.SaveProfile(Layer.Profile, true); + + AdaptionHintCount = Layer.Adapter.AdaptionHints.Count; + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileAdaptionHintsStepView.axaml b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileAdaptionHintsStepView.axaml new file mode 100644 index 000000000..b85ffdb0f --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileAdaptionHintsStepView.axaml @@ -0,0 +1,67 @@ + + + + + + + + + Set up profile adaption hints + + + Add hints below to help decide where to place this each layer when the profile is imported by another user. + + + + + Learn more about adaption hints + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileAdaptionHintsStepView.axaml.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileAdaptionHintsStepView.axaml.cs new file mode 100644 index 000000000..a5f7815d7 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileAdaptionHintsStepView.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Profile; + +public partial class ProfileAdaptionHintsStepView : ReactiveUserControl +{ + public ProfileAdaptionHintsStepView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileAdaptionHintsStepViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileAdaptionHintsStepViewModel.cs new file mode 100644 index 000000000..fd27ecccb --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileAdaptionHintsStepViewModel.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reactive; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Threading.Tasks; +using Artemis.Core; +using Artemis.Core.Services; +using Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs; +using Artemis.UI.Shared.Services; +using DynamicData; +using ReactiveUI; +using DynamicData.Aggregation; + +namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Profile; + +public class ProfileAdaptionHintsStepViewModel : SubmissionViewModel +{ + private readonly IWindowService _windowService; + private readonly IProfileService _profileService; + private readonly SourceList _layers; + + public ProfileAdaptionHintsStepViewModel(IWindowService windowService, IProfileService profileService, Func getLayerViewModel) + { + _windowService = windowService; + _profileService = profileService; + _layers = new SourceList(); + _layers.Connect().Bind(out ReadOnlyObservableCollection layers).Subscribe(); + + GoBack = ReactiveCommand.Create(() => State.ChangeScreen()); + Continue = ReactiveCommand.Create(ExecuteContinue, _layers.Connect().AutoRefresh(l => l.AdaptionHintCount).Filter(l => l.AdaptionHintCount == 0).IsEmpty()); + EditAdaptionHints = ReactiveCommand.CreateFromTask(ExecuteEditAdaptionHints); + Layers = layers; + + this.WhenActivated((CompositeDisposable _) => + { + if (State.EntrySource is ProfileConfiguration profileConfiguration && profileConfiguration.Profile != null) + { + _layers.Edit(l => + { + l.Clear(); + l.AddRange(profileConfiguration.Profile.GetAllLayers().Select(getLayerViewModel)); + }); + } + }); + } + + public override ReactiveCommand Continue { get; } + public override ReactiveCommand GoBack { get; } + public ReactiveCommand EditAdaptionHints { get; } + public ReadOnlyObservableCollection Layers { get; } + + private async Task ExecuteEditAdaptionHints(Layer layer) + { + await _windowService.ShowDialogAsync(layer); + _profileService.SaveProfile(layer.Profile, true); + } + + private void ExecuteContinue() + { + if (Layers.Any(l => l.AdaptionHintCount == 0)) + return; + + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileSelectionStepView.axaml b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileSelectionStepView.axaml new file mode 100644 index 000000000..8fd0eb216 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileSelectionStepView.axaml @@ -0,0 +1,56 @@ + + + + + + + + + + Profile selection + + + Please select the profile you want to share, a preview will be shown below. + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileSelectionStepView.axaml.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileSelectionStepView.axaml.cs new file mode 100644 index 000000000..84cb5bf27 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileSelectionStepView.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Profile; + +public partial class ProfileSelectionStepView : ReactiveUserControl +{ + public ProfileSelectionStepView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileSelectionStepViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileSelectionStepViewModel.cs new file mode 100644 index 000000000..08f9aafd0 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileSelectionStepViewModel.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reactive; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using Artemis.Core; +using Artemis.Core.Services; +using Artemis.Storage.Entities.Profile; +using Artemis.Storage.Repositories.Interfaces; +using Artemis.UI.Screens.Workshop.Profile; +using ReactiveUI; + +namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Profile; + +public class ProfileSelectionStepViewModel : SubmissionViewModel +{ + private readonly IProfileService _profileService; + private readonly IProfileCategoryRepository _profileCategoryRepository; + private ProfileConfiguration? _selectedProfile; + + /// + public ProfileSelectionStepViewModel(IProfileService profileService, ProfilePreviewViewModel profilePreviewViewModel) + { + _profileService = profileService; + + // Use copies of the profiles, the originals are used by the core and could be disposed + Profiles = new ObservableCollection(_profileService.ProfileConfigurations.Select(_profileService.CloneProfileConfiguration)); + ProfilePreview = profilePreviewViewModel; + + GoBack = ReactiveCommand.Create(() => State.ChangeScreen()); + Continue = ReactiveCommand.Create(ExecuteContinue, this.WhenAnyValue(vm => vm.SelectedProfile).Select(p => p != null)); + + this.WhenAnyValue(vm => vm.SelectedProfile).Subscribe(p => Update(p)); + this.WhenActivated((CompositeDisposable _) => + { + if (State.EntrySource is ProfileConfiguration profileConfiguration) + SelectedProfile = Profiles.FirstOrDefault(p => p.ProfileId == profileConfiguration.ProfileId); + }); + } + + private void Update(ProfileConfiguration? profileConfiguration) + { + ProfilePreview.ProfileConfiguration = null; + + foreach (ProfileConfiguration configuration in Profiles) + { + if (configuration == profileConfiguration) + _profileService.ActivateProfile(configuration); + else + _profileService.DeactivateProfile(configuration); + } + + ProfilePreview.ProfileConfiguration = profileConfiguration; + } + + public ObservableCollection Profiles { get; } + public ProfilePreviewViewModel ProfilePreview { get; } + + public ProfileConfiguration? SelectedProfile + { + get => _selectedProfile; + set => RaiseAndSetIfChanged(ref _selectedProfile, value); + } + + /// + public override ReactiveCommand Continue { get; } + + /// + public override ReactiveCommand GoBack { get; } + + private void ExecuteContinue() + { + if (SelectedProfile == null) + return; + + State.EntrySource = SelectedProfile; + State.Name = SelectedProfile.Name; + State.Icon = SelectedProfile.Icon.GetIconStream(); + + State.ChangeScreen(); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ProfileSelectionStepView.axaml b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ProfileSelectionStepView.axaml deleted file mode 100644 index ceb0a3469..000000000 --- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ProfileSelectionStepView.axaml +++ /dev/null @@ -1,10 +0,0 @@ - - Welcome to Avalonia! - diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ProfileSelectionStepView.axaml.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ProfileSelectionStepView.axaml.cs deleted file mode 100644 index 37923b020..000000000 --- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ProfileSelectionStepView.axaml.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Avalonia; -using Avalonia.Controls; -using Avalonia.Markup.Xaml; - -namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps; - -public partial class ProfileSelectionStepView : UserControl -{ - public ProfileSelectionStepView() - { - InitializeComponent(); - } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ProfileSelectionStepViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ProfileSelectionStepViewModel.cs deleted file mode 100644 index 3a258bb66..000000000 --- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ProfileSelectionStepViewModel.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Reactive; -using ReactiveUI; - -namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps; - -public class ProfileSelectionStepViewModel : SubmissionViewModel -{ - /// - public override ReactiveCommand Continue { get; } - - /// - public override ReactiveCommand GoBack { get; } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ValidateEmailStepView.axaml.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ValidateEmailStepView.axaml.cs index 1d9f32be8..a5899731e 100644 --- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ValidateEmailStepView.axaml.cs +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ValidateEmailStepView.axaml.cs @@ -1,4 +1,3 @@ -using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps; @@ -9,9 +8,4 @@ public partial class ValidateEmailStepView : ReactiveUserControl { InitializeComponent(); } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/SubmissionWizardState.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/SubmissionWizardState.cs index 19256ba56..3525d9ed2 100644 --- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/SubmissionWizardState.cs +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/SubmissionWizardState.cs @@ -7,8 +7,8 @@ namespace Artemis.UI.Screens.Workshop.SubmissionWizard; public class SubmissionWizardState { - private readonly SubmissionWizardViewModel _wizardViewModel; private readonly IContainer _container; + private readonly SubmissionWizardViewModel _wizardViewModel; public SubmissionWizardState(SubmissionWizardViewModel wizardViewModel, IContainer container) { @@ -27,6 +27,8 @@ public class SubmissionWizardState public List Tags { get; set; } = new(); public List Images { get; set; } = new(); + public object? EntrySource { get; set; } + public void ChangeScreen() where TSubmissionViewModel : SubmissionViewModel { _wizardViewModel.Screen = _container.Resolve();