diff --git a/src/Artemis.Core/Plugins/Modules/DataModel.cs b/src/Artemis.Core/Plugins/Modules/DataModel.cs index 510883b39..ea1cacd6d 100644 --- a/src/Artemis.Core/Plugins/Modules/DataModel.cs +++ b/src/Artemis.Core/Plugins/Modules/DataModel.cs @@ -285,34 +285,43 @@ namespace Artemis.Core.Modules internal bool IsPropertyInUse(string path, bool includeChildren) { path = path.ToUpperInvariant(); - return includeChildren - ? _activePathsHashSet.Any(p => p.StartsWith(path, StringComparison.Ordinal)) - : _activePathsHashSet.Contains(path); + lock (_activePaths) + { + return includeChildren + ? _activePathsHashSet.Any(p => p.StartsWith(path, StringComparison.Ordinal)) + : _activePathsHashSet.Contains(path); + } } internal void AddDataModelPath(DataModelPath path) { - if (_activePaths.Contains(path)) - return; + lock (_activePaths) + { + if (_activePaths.Contains(path)) + return; - _activePaths.Add(path); + _activePaths.Add(path); - // Add to the hashset if this is the first path pointing - string hashPath = path.Path.ToUpperInvariant(); - if (!_activePathsHashSet.Contains(hashPath)) - _activePathsHashSet.Add(hashPath); + // Add to the hashset if this is the first path pointing + string hashPath = path.Path.ToUpperInvariant(); + if (!_activePathsHashSet.Contains(hashPath)) + _activePathsHashSet.Add(hashPath); + } OnActivePathAdded(new DataModelPathEventArgs(path)); } internal void RemoveDataModelPath(DataModelPath path) { - if (!_activePaths.Remove(path)) - return; + lock (_activePaths) + { + if (!_activePaths.Remove(path)) + return; - // Remove from the hashset if this was the last path pointing there - if (_activePaths.All(p => p.Path != path.Path)) - _activePathsHashSet.Remove(path.Path.ToUpperInvariant()); + // Remove from the hashset if this was the last path pointing there + if (_activePaths.All(p => p.Path != path.Path)) + _activePathsHashSet.Remove(path.Path.ToUpperInvariant()); + } OnActivePathRemoved(new DataModelPathEventArgs(path)); } diff --git a/src/Artemis.Core/Services/PluginManagementService.cs b/src/Artemis.Core/Services/PluginManagementService.cs index f56d20fb7..61d965cdc 100644 --- a/src/Artemis.Core/Services/PluginManagementService.cs +++ b/src/Artemis.Core/Services/PluginManagementService.cs @@ -634,7 +634,7 @@ namespace Artemis.Core.Services if (isAutoEnable) { // Schedule a retry based on the amount of attempts - if (pluginFeature.AutoEnableAttempts < 4) + if (pluginFeature.AutoEnableAttempts < 4 && pluginFeature.Plugin.IsEnabled) { TimeSpan retryDelay = TimeSpan.FromSeconds(pluginFeature.AutoEnableAttempts * 10); _logger.Warning( diff --git a/src/Artemis.Core/VisualScripting/Node.cs b/src/Artemis.Core/VisualScripting/Node.cs index c81d54ade..a9bdfb8f2 100644 --- a/src/Artemis.Core/VisualScripting/Node.cs +++ b/src/Artemis.Core/VisualScripting/Node.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using Artemis.Core.Properties; using Ninject; using Ninject.Parameters; diff --git a/src/Artemis.UI.Shared/NullCommand.cs b/src/Artemis.UI.Shared/NullCommand.cs new file mode 100644 index 000000000..06e23ebbf --- /dev/null +++ b/src/Artemis.UI.Shared/NullCommand.cs @@ -0,0 +1,36 @@ +using System; +using System.Windows.Input; + +namespace Artemis.UI.Shared; + +/// +/// Represents a placeholder command that does nothing and can't be executed. +/// +public class NullCommand : ICommand +{ + private static readonly Lazy _instance = new(() => new NullCommand()); + + private NullCommand() + { + } + + /// + /// Gets the static instance of this command. + /// + public static ICommand Instance => _instance.Value; + + /// + public event EventHandler? CanExecuteChanged; + + /// + public void Execute(object? parameter) + { + throw new InvalidOperationException("NullCommand cannot be executed"); + } + + /// + public bool CanExecute(object? parameter) + { + return false; + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Styles/Artemis.axaml b/src/Artemis.UI.Shared/Styles/Artemis.axaml index 19d356099..cf068ddac 100644 --- a/src/Artemis.UI.Shared/Styles/Artemis.axaml +++ b/src/Artemis.UI.Shared/Styles/Artemis.axaml @@ -1,24 +1,14 @@  - + - - - - - - - - - - - - - - - - + + + + + + diff --git a/src/Artemis.UI.Shared/Styles/Controls/GradientPicker.axaml b/src/Artemis.UI.Shared/Styles/Controls/GradientPicker.axaml index 4bde3cdaf..208eba1da 100644 --- a/src/Artemis.UI.Shared/Styles/Controls/GradientPicker.axaml +++ b/src/Artemis.UI.Shared/Styles/Controls/GradientPicker.axaml @@ -88,9 +88,9 @@ Background="{DynamicResource LightCheckerboardBrush}" Margin="5 0"> - + - - - - - - - - - diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeViewModel.cs index eac0c9789..0326d7775 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeViewModel.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; -using System.Reactive; using System.Reactive.Disposables; using Artemis.Core; using Artemis.Core.Services; @@ -11,87 +10,114 @@ using Artemis.UI.Shared.Services.Interfaces; using Artemis.UI.Shared.Services.ProfileEditor; using ReactiveUI; -namespace Artemis.UI.Screens.ProfileEditor.ProfileTree +namespace Artemis.UI.Screens.ProfileEditor.ProfileTree; + +public class ProfileTreeViewModel : TreeItemViewModel { - public class ProfileTreeViewModel : TreeItemViewModel + private readonly IProfileEditorService _profileEditorService; + private TreeItemViewModel? _selectedChild; + + public ProfileTreeViewModel(IWindowService windowService, + IProfileEditorService profileEditorService, + ILayerBrushService layerBrushService, + IProfileEditorVmFactory profileEditorVmFactory, + IRgbService rgbService) + : base(null, null, windowService, profileEditorService, rgbService, layerBrushService, profileEditorVmFactory) { - private TreeItemViewModel? _selectedChild; - - public ProfileTreeViewModel(IWindowService windowService, - IProfileEditorService profileEditorService, - ILayerBrushService layerBrushService, - IProfileEditorVmFactory profileEditorVmFactory, - IRgbService rgbService) - : base(null, null, windowService, profileEditorService, rgbService, layerBrushService, profileEditorVmFactory) + _profileEditorService = profileEditorService; + this.WhenActivated(d => { - this.WhenActivated(d => + profileEditorService.ProfileConfiguration.WhereNotNull().Subscribe(configuration => { - profileEditorService.ProfileConfiguration.WhereNotNull().Subscribe(configuration => + if (configuration.Profile == null) { - if (configuration.Profile == null) - { - windowService.ShowConfirmContentDialog("Failed to load profile", "It appears that this profile is corrupt and cannot be loaded. Please check your logs.", "Confirm", null); - return; - } + windowService.ShowConfirmContentDialog("Failed to load profile", "It appears that this profile is corrupt and cannot be loaded. Please check your logs.", "Confirm", null); + return; + } - ProfileElement = configuration.Profile.GetRootFolder(); - SubscribeToProfileElement(d); - CreateTreeItems(); - }).DisposeWith(d); + ProfileElement = configuration.Profile.GetRootFolder(); + SubscribeToProfileElement(d); + CreateTreeItems(); + }).DisposeWith(d); - profileEditorService.ProfileElement.Subscribe(SelectCurrentProfileElement).DisposeWith(d); - }); + profileEditorService.ProfileElement.Subscribe(SelectCurrentProfileElement).DisposeWith(d); + }); - this.WhenAnyValue(vm => vm.SelectedChild).Subscribe(model => - { - if (model?.ProfileElement is RenderProfileElement renderProfileElement) - profileEditorService.ChangeCurrentProfileElement(renderProfileElement); - }); - - ClearSelection = ReactiveCommand.Create(() => profileEditorService.ChangeCurrentProfileElement(null)); - } - - public ReactiveCommand ClearSelection { get; } - - public TreeItemViewModel? SelectedChild + this.WhenAnyValue(vm => vm.SelectedChild).Subscribe(model => { - get => _selectedChild; - set => RaiseAndSetIfChanged(ref _selectedChild, value); - } + if (model?.ProfileElement is RenderProfileElement renderProfileElement) + profileEditorService.ChangeCurrentProfileElement(renderProfileElement); + }); + } - private void SelectCurrentProfileElement(RenderProfileElement? element) + public TreeItemViewModel? SelectedChild + { + get => _selectedChild; + set => RaiseAndSetIfChanged(ref _selectedChild, value); + } + + public override bool SupportsChildren => true; + + public void ClearSelection() + { + _profileEditorService.ChangeCurrentProfileElement(null); + } + + public void RenameSelected() + { + SelectedChild?.Rename.Execute().Subscribe(); + } + + public void DeleteSelected() + { + SelectedChild?.Delete.Execute().Subscribe(); + } + + public void DuplicateSelected() + { + SelectedChild?.Duplicate.Execute().Subscribe(); + } + + public void CopySelected() + { + SelectedChild?.Copy.Execute().Subscribe(); + } + + public void PasteSelected() + { + SelectedChild?.Paste.Execute().Subscribe(); + } + + private void SelectCurrentProfileElement(RenderProfileElement? element) + { + if (SelectedChild?.ProfileElement == element) + return; + + // Find the tree item belonging to the selected element + List treeItems = GetAllTreeItems(Children); + TreeItemViewModel? selected = treeItems.FirstOrDefault(e => e.ProfileElement == element); + + // Walk up the tree, expanding parents + TreeItemViewModel? currentParent = selected?.Parent; + while (currentParent != null) { - if (SelectedChild?.ProfileElement == element) - return; - - // Find the tree item belonging to the selected element - List treeItems = GetAllTreeItems(Children); - TreeItemViewModel? selected = treeItems.FirstOrDefault(e => e.ProfileElement == element); - - // Walk up the tree, expanding parents - TreeItemViewModel? currentParent = selected?.Parent; - while (currentParent != null) - { - currentParent.IsExpanded = true; - currentParent = currentParent.Parent; - } - - SelectedChild = selected; + currentParent.IsExpanded = true; + currentParent = currentParent.Parent; } - private List GetAllTreeItems(ObservableCollection treeItems) + SelectedChild = selected; + } + + private List GetAllTreeItems(ObservableCollection treeItems) + { + List result = new(); + foreach (TreeItemViewModel treeItemViewModel in treeItems) { - List result = new(); - foreach (TreeItemViewModel treeItemViewModel in treeItems) - { - result.Add(treeItemViewModel); - if (treeItemViewModel.Children.Any()) - result.AddRange(GetAllTreeItems(treeItemViewModel.Children)); - } - - return result; + result.Add(treeItemViewModel); + if (treeItemViewModel.Children.Any()) + result.AddRange(GetAllTreeItems(treeItemViewModel.Children)); } - public override bool SupportsChildren => true; + return result; } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertiesViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertiesViewModel.cs index 67f4ee047..e2006f865 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertiesViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertiesViewModel.cs @@ -57,7 +57,14 @@ public class PropertiesViewModel : ActivatableViewModelBase { _profileElement = profileEditorService.ProfileElement.ToProperty(this, vm => vm.ProfileElement).DisposeWith(d); _pixelsPerSecond = profileEditorService.PixelsPerSecond.ToProperty(this, vm => vm.PixelsPerSecond).DisposeWith(d); - Disposable.Create(() => _settingsService.SaveAllSettings()).DisposeWith(d); + Disposable.Create(() => + { + _settingsService.SaveAllSettings(); + foreach ((LayerPropertyGroup _, PropertyGroupViewModel value) in _cachedViewModels) + value.Dispose(); + _cachedViewModels.Clear(); + + }).DisposeWith(d); }); this.WhenAnyValue(vm => vm.ProfileElement).Subscribe(_ => UpdateGroups()); } diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineGroupViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineGroupViewModel.cs index 87cac2175..76c010225 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineGroupViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineGroupViewModel.cs @@ -23,7 +23,7 @@ public class TimelineGroupViewModel : ActivatableViewModelBase { _pixelsPerSecond = p; UpdateKeyframePositions(); - }); + }).DisposeWith(d); this.WhenAnyValue(vm => vm.PropertyGroupViewModel.IsExpanded).Subscribe(_ => UpdateKeyframePositions()).DisposeWith(d); PropertyGroupViewModel.WhenAnyValue(vm => vm.IsExpanded).Subscribe(_ => this.RaisePropertyChanged(nameof(Children))).DisposeWith(d); }); diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/StatusBar/StatusBarViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/StatusBar/StatusBarViewModel.cs index 802c407a8..341aef64c 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/StatusBar/StatusBarViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/StatusBar/StatusBarViewModel.cs @@ -24,7 +24,7 @@ public class StatusBarViewModel : ActivatableViewModelBase { _profileElement = profileEditorService.ProfileElement.ToProperty(this, vm => vm.ProfileElement).DisposeWith(d); _history = profileEditorService.History.ToProperty(this, vm => vm.History).DisposeWith(d); - _pixelsPerSecond = profileEditorService.PixelsPerSecond.ToProperty(this, vm => vm.PixelsPerSecond); + _pixelsPerSecond = profileEditorService.PixelsPerSecond.ToProperty(this, vm => vm.PixelsPerSecond).DisposeWith(d); }); this.WhenAnyValue(vm => vm.History) diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorView.axaml b/src/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorView.axaml index 1d7464a62..7aa8d3859 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorView.axaml +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorView.axaml @@ -9,6 +9,18 @@ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.ProfileEditor.VisualEditor.VisualEditorView" x:DataType="visualEditor:VisualEditorViewModel"> + + + + + + + + + + + + diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorTitleBarView.axaml.cs b/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorTitleBarView.axaml.cs index 08b7a31c3..ae89f6723 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorTitleBarView.axaml.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorTitleBarView.axaml.cs @@ -19,7 +19,6 @@ namespace Artemis.UI.Screens.ProfileEditor private void MenuItem_OnSubmenuOpened(object? sender, RoutedEventArgs e) { - } } -} +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Root/RootViewModel.cs b/src/Artemis.UI/Screens/Root/RootViewModel.cs index 7bb261274..0cdcc27b3 100644 --- a/src/Artemis.UI/Screens/Root/RootViewModel.cs +++ b/src/Artemis.UI/Screens/Root/RootViewModel.cs @@ -15,6 +15,7 @@ using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Platform; using Avalonia.Threading; +using JetBrains.Annotations; using ReactiveUI; namespace Artemis.UI.Screens.Root @@ -95,6 +96,7 @@ namespace Artemis.UI.Screens.Root { _lifeTime.MainWindow = null; SidebarViewModel = null; + Router.NavigateAndReset.Execute(new EmptyViewModel(this, "blank")).Subscribe(); OnMainWindowClosed(); } @@ -220,4 +222,12 @@ namespace Artemis.UI.Screens.Root #endregion } + + internal class EmptyViewModel : MainScreenViewModel + { + /// + public EmptyViewModel(IScreen hostScreen, string urlPathSegment) : base(hostScreen, urlPathSegment) + { + } + } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabViewModel.cs index d3c3c4ff8..6519d770a 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabViewModel.cs @@ -4,6 +4,7 @@ using System.Collections.ObjectModel; using System.Diagnostics; using System.Linq; using System.Reactive; +using System.Reactive.Disposables; using System.Threading; using System.Threading.Tasks; using Artemis.Core; @@ -30,8 +31,8 @@ namespace Artemis.UI.Screens.Settings DisplayName = "General"; _settingsService = settingsService; _debugService = debugService; - _fluentAvaloniaTheme = AvaloniaLocator.Current.GetService(); ; - + _fluentAvaloniaTheme = AvaloniaLocator.Current.GetService(); + List layerBrushProviders = pluginManagementService.GetFeaturesOfType(); LayerBrushDescriptors = new ObservableCollection(layerBrushProviders.SelectMany(l => l.LayerBrushDescriptors)); _defaultLayerBrushDescriptor = _settingsService.GetSetting("ProfileEditor.DefaultLayerBrushDescriptor", new LayerBrushReference @@ -45,8 +46,12 @@ namespace Artemis.UI.Screens.Settings ShowSetupWizard = ReactiveCommand.Create(ExecuteShowSetupWizard); ShowDebugger = ReactiveCommand.Create(ExecuteShowDebugger); ShowDataFolder = ReactiveCommand.Create(ExecuteShowDataFolder); - - UIColorScheme.SettingChanged += UIColorSchemeOnSettingChanged; + + this.WhenActivated(d => + { + UIColorScheme.SettingChanged += UIColorSchemeOnSettingChanged; + Disposable.Create(() => UIColorScheme.SettingChanged -= UIColorSchemeOnSettingChanged).DisposeWith(d); + }); } private void UIColorSchemeOnSettingChanged(object? sender, EventArgs e) @@ -139,7 +144,7 @@ namespace Artemis.UI.Screens.Settings } #endregion - + #region Updating private Task ExecuteCheckForUpdate(CancellationToken cancellationToken) diff --git a/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorView.axaml b/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorView.axaml index 66f414663..c2c55e11c 100644 --- a/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorView.axaml +++ b/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorView.axaml @@ -7,7 +7,18 @@ xmlns:controls="clr-namespace:Artemis.UI.Shared.Controls;assembly=Artemis.UI.Shared" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.SurfaceEditor.SurfaceEditorView"> - + + + + + + + + + + + + + IsVisible="{CompiledBinding DisplayValue}"> diff --git a/src/Artemis.UI/Screens/VisualScripting/CableViewModel.cs b/src/Artemis.UI/Screens/VisualScripting/CableViewModel.cs index 383388bb1..d4d8bba4c 100644 --- a/src/Artemis.UI/Screens/VisualScripting/CableViewModel.cs +++ b/src/Artemis.UI/Screens/VisualScripting/CableViewModel.cs @@ -15,14 +15,15 @@ namespace Artemis.UI.Screens.VisualScripting; public class CableViewModel : ActivatableViewModelBase { - private readonly ObservableAsPropertyHelper _connected; - private readonly ObservableAsPropertyHelper _fromPoint; - private readonly ObservableAsPropertyHelper _toPoint; - private readonly ObservableAsPropertyHelper _valuePoint; private ObservableAsPropertyHelper? _cableColor; + private ObservableAsPropertyHelper? _fromPoint; + private ObservableAsPropertyHelper? _toPoint; + private ObservableAsPropertyHelper? _valuePoint; + private ObservableAsPropertyHelper? _connected; private PinViewModel? _fromViewModel; private PinViewModel? _toViewModel; + private bool _displayValue; public CableViewModel(NodeScriptViewModel nodeScriptViewModel, IPin from, IPin to) { @@ -42,25 +43,32 @@ public class CableViewModel : ActivatableViewModelBase .Switch() .ToProperty(this, vm => vm.CableColor) .DisposeWith(d); + + _fromPoint = this.WhenAnyValue(vm => vm.FromViewModel) + .Select(p => p != null ? p.WhenAnyValue(pvm => pvm.Position) : Observable.Never()) + .Switch() + .ToProperty(this, vm => vm.FromPoint) + .DisposeWith(d); + _toPoint = this.WhenAnyValue(vm => vm.ToViewModel) + .Select(p => p != null ? p.WhenAnyValue(pvm => pvm.Position) : Observable.Never()) + .Switch() + .ToProperty(this, vm => vm.ToPoint) + .DisposeWith(d); + _valuePoint = this.WhenAnyValue(vm => vm.FromPoint, vm => vm.ToPoint).Select(tuple => new Point( + tuple.Item1.X + (tuple.Item2.X - tuple.Item1.X) / 2, + tuple.Item1.Y + (tuple.Item2.Y - tuple.Item1.Y) / 2 + )).ToProperty(this, vm => vm.ValuePoint) + .DisposeWith(d); + + // Not a perfect solution but this makes sure the cable never renders at 0,0 (can happen when the cable spawns before the pin ever rendered) + _connected = this.WhenAnyValue(vm => vm.FromPoint, vm => vm.ToPoint) + .Select(tuple => tuple.Item1 != new Point(0, 0) && tuple.Item2 != new Point(0, 0)) + .ToProperty(this, vm => vm.Connected) + .DisposeWith(d); }); - _fromPoint = this.WhenAnyValue(vm => vm.FromViewModel) - .Select(p => p != null ? p.WhenAnyValue(pvm => pvm.Position) : Observable.Never()) - .Switch() - .ToProperty(this, vm => vm.FromPoint); - _toPoint = this.WhenAnyValue(vm => vm.ToViewModel) - .Select(p => p != null ? p.WhenAnyValue(pvm => pvm.Position) : Observable.Never()) - .Switch() - .ToProperty(this, vm => vm.ToPoint); - _valuePoint = this.WhenAnyValue(vm => vm.FromPoint, vm => vm.ToPoint).Select(tuple => new Point( - tuple.Item1.X + (tuple.Item2.X - tuple.Item1.X) / 2, - tuple.Item1.Y + (tuple.Item2.Y - tuple.Item1.Y) / 2 - )).ToProperty(this, vm => vm.ValuePoint); - // Not a perfect solution but this makes sure the cable never renders at 0,0 (can happen when the cable spawns before the pin ever rendered) - _connected = this.WhenAnyValue(vm => vm.FromPoint, vm => vm.ToPoint) - .Select(tuple => tuple.Item1 != new Point(0, 0) && tuple.Item2 != new Point(0, 0)) - .ToProperty(this, vm => vm.Connected); + DisplayValue = !nodeScriptViewModel.IsPreview; } public PinViewModel? FromViewModel @@ -75,10 +83,16 @@ public class CableViewModel : ActivatableViewModelBase set => RaiseAndSetIfChanged(ref _toViewModel, value); } - public bool Connected => _connected.Value; + public bool DisplayValue + { + get => _displayValue; + set => _displayValue = value; + } - public Point FromPoint => _fromPoint.Value; - public Point ToPoint => _toPoint.Value; - public Point ValuePoint => _valuePoint.Value; + public bool Connected => _connected?.Value ?? false; + + public Point FromPoint => _fromPoint?.Value ?? new Point(); + public Point ToPoint => _toPoint?.Value ?? new Point(); + public Point ValuePoint => _valuePoint?.Value ?? new Point(); public Color CableColor => _cableColor?.Value ?? new Color(255, 255, 255, 255); } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/VisualScripting/NodePickerView.axaml b/src/Artemis.UI/Screens/VisualScripting/NodePickerView.axaml index 1d5f3d0c3..db9c8adae 100644 --- a/src/Artemis.UI/Screens/VisualScripting/NodePickerView.axaml +++ b/src/Artemis.UI/Screens/VisualScripting/NodePickerView.axaml @@ -21,7 +21,7 @@ FontFamily="{StaticResource SymbolThemeFontFamily}" Classes="AppBarButton" Command="{Binding $parent[TextBox].Clear}" - IsVisible="{Binding Text, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type TextBox}}, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" /> + IsVisible="{Binding $parent[TextBox].Text, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" /> - - - - - + + + + + + + + + + + + - - - - - - - - - - + + + + + + + + + + - - + + - + - + - - - - + - diff --git a/src/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml.cs b/src/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml.cs index 6a5414150..0651ed5a6 100644 --- a/src/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml.cs +++ b/src/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.Linq; using System.Reactive.Disposables; +using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Events; using Artemis.UI.Shared.Controls; @@ -15,6 +17,8 @@ using Avalonia.Interactivity; using Avalonia.Markup.Xaml; using Avalonia.Media; using Avalonia.ReactiveUI; +using Avalonia.Threading; +using DynamicData.Binding; using ReactiveUI; namespace Artemis.UI.Screens.VisualScripting; @@ -38,27 +42,17 @@ public class NodeScriptView : ReactiveUserControl UpdateZoomBorderBackground(); _grid.AddHandler(PointerReleasedEvent, CanvasOnPointerReleased, RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble, true); + this.WhenActivated(d => { - ViewModel!.PickerPositionSubject.Subscribe(p => - { - ViewModel.NodePickerViewModel.Position = p; - _grid?.ContextFlyout?.ShowAt(_grid, true); - }).DisposeWith(d); - + ViewModel!.PickerPositionSubject.Subscribe(ShowPickerAt).DisposeWith(d); if (ViewModel.IsPreview) { BoundsProperty.Changed.Subscribe(BoundsPropertyChanged).DisposeWith(d); - ViewModel.NodeScript.NodeAdded += NodesChanged; - ViewModel.NodeScript.NodeRemoved += NodesChanged; - Disposable.Create(() => - { - ViewModel.NodeScript.NodeAdded -= NodesChanged; - ViewModel.NodeScript.NodeRemoved -= NodesChanged; - }).DisposeWith(d); + ViewModel.NodeViewModels.ToObservableChangeSet().Subscribe(_ => AutoFitIfPreview()).DisposeWith(d); } - AutoFit(true); + Dispatcher.UIThread.InvokeAsync(() => AutoFit(true), DispatcherPriority.ContextIdle); }); } @@ -68,17 +62,20 @@ public class NodeScriptView : ReactiveUserControl return base.MeasureOverride(availableSize); } + private void ShowPickerAt(Point point) + { + if (ViewModel == null) + return; + ViewModel.NodePickerViewModel.Position = point; + _grid?.ContextFlyout?.ShowAt(_grid, true); + } + private void AutoFitIfPreview() { if (ViewModel != null && ViewModel.IsPreview) AutoFit(true); } - private void NodesChanged(object? sender, SingleValueEventArgs e) - { - AutoFitIfPreview(); - } - private void BoundsPropertyChanged(AvaloniaPropertyChangedEventArgs obj) { if (_nodesContainer.ItemContainerGenerator.Containers.Select(c => c.ContainerControl).Contains(obj.Sender)) @@ -102,8 +99,8 @@ public class NodeScriptView : ReactiveUserControl double bottom = _nodesContainer.ItemContainerGenerator.Containers.Select(c => c.ContainerControl.Bounds.Bottom).Max(); double right = _nodesContainer.ItemContainerGenerator.Containers.Select(c => c.ContainerControl.Bounds.Right).Max(); - // Add a 5 pixel margin around the rect - Rect scriptRect = new(new Point(left - 5, top - 5), new Point(right + 5, bottom + 5)); + // 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(1, Math.Min(Bounds.Width / scriptRect.Width, Bounds.Height / scriptRect.Height)); diff --git a/src/Artemis.UI/Screens/VisualScripting/NodeView.axaml b/src/Artemis.UI/Screens/VisualScripting/NodeView.axaml index 1d58b8175..aebe108cf 100644 --- a/src/Artemis.UI/Screens/VisualScripting/NodeView.axaml +++ b/src/Artemis.UI/Screens/VisualScripting/NodeView.axaml @@ -63,7 +63,10 @@ - + diff --git a/src/Artemis.UI/Screens/VisualScripting/NodeView.axaml.cs b/src/Artemis.UI/Screens/VisualScripting/NodeView.axaml.cs index 66f1804f3..799e8227a 100644 --- a/src/Artemis.UI/Screens/VisualScripting/NodeView.axaml.cs +++ b/src/Artemis.UI/Screens/VisualScripting/NodeView.axaml.cs @@ -2,7 +2,10 @@ using System; using System.Collections.Generic; using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.Mixins; +using Avalonia.Controls.Presenters; using Avalonia.Input; +using Avalonia.LogicalTree; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; using Avalonia.VisualTree; diff --git a/src/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs b/src/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs index e26d17a2b..79dca9758 100644 --- a/src/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs +++ b/src/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs @@ -35,8 +35,8 @@ public class NodeViewModel : ActivatableViewModelBase public NodeViewModel(NodeScriptViewModel nodeScriptViewModel, INode node, INodeVmFactory nodeVmFactory, INodeEditorService nodeEditorService) { - NodeScriptViewModel = nodeScriptViewModel; _nodeEditorService = nodeEditorService; + NodeScriptViewModel = nodeScriptViewModel; Node = node; DeleteNode = ReactiveCommand.Create(ExecuteDeleteNode, this.WhenAnyValue(vm => vm.IsStaticNode).Select(v => !v)); diff --git a/src/Artemis.UI/Screens/VisualScripting/Pins/InputPinView.axaml b/src/Artemis.UI/Screens/VisualScripting/Pins/InputPinView.axaml index 523457358..b995c00a1 100644 --- a/src/Artemis.UI/Screens/VisualScripting/Pins/InputPinView.axaml +++ b/src/Artemis.UI/Screens/VisualScripting/Pins/InputPinView.axaml @@ -3,28 +3,22 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:pins="clr-namespace:Artemis.UI.Screens.VisualScripting.Pins" + xmlns:converters="clr-namespace:Artemis.UI.Converters" mc:Ignorable="d" d:DesignWidth="200" x:Class="Artemis.UI.Screens.VisualScripting.Pins.InputPinView" x:DataType="pins:PinViewModel"> + + + - - + diff --git a/src/Artemis.UI/Screens/VisualScripting/Pins/OutputPinView.axaml b/src/Artemis.UI/Screens/VisualScripting/Pins/OutputPinView.axaml index cb8e3d832..675b17520 100644 --- a/src/Artemis.UI/Screens/VisualScripting/Pins/OutputPinView.axaml +++ b/src/Artemis.UI/Screens/VisualScripting/Pins/OutputPinView.axaml @@ -3,29 +3,23 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:pins="clr-namespace:Artemis.UI.Screens.VisualScripting.Pins" + xmlns:converters="clr-namespace:Artemis.UI.Converters" mc:Ignorable="d" d:DesignWidth="200" x:Class="Artemis.UI.Screens.VisualScripting.Pins.OutputPinView" x:DataType="pins:PinViewModel"> + + + - - + \ No newline at end of file diff --git a/src/Artemis.UI/ViewLocator.cs b/src/Artemis.UI/ViewLocator.cs index 45ceec2d2..5205c004a 100644 --- a/src/Artemis.UI/ViewLocator.cs +++ b/src/Artemis.UI/ViewLocator.cs @@ -23,7 +23,11 @@ public class ViewLocator : IDataTemplate throw new ArtemisUIException($"The views of activatable view models should inherit ReactiveUserControl, in this case ReactiveUserControl<{data.GetType().Name}>."); if (type != null) + { + Debug.WriteLine("[ViewLocator] Creating instance of '{0}'", type); return (Control) Activator.CreateInstance(type)!; + } + return new TextBlock {Text = "Not Found: " + name}; }