Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into docs-072
Browse files Browse the repository at this point in the history
Pickysaurus committed Jan 15, 2025

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
2 parents 2496ac5 + 39dfe0e commit 6962ff4
Showing 51 changed files with 2,206 additions and 1,285 deletions.
Original file line number Diff line number Diff line change
@@ -24,8 +24,19 @@ public static IObservable<IChangeSet<IJob, JobId>> ObserveActiveJobs<TJobType>(t
{
return jobMonitor.GetObservableChangeSet<TJobType>()
.FilterOnObservable(job => job.ObservableStatus.Select(status => status.IsActive()));
}

}

public static IObservable<bool> HasActiveJob<TJobType>(this IJobMonitor jobMonitor, Func<TJobType, bool> predicate)
where TJobType : IJobDefinition
{
return jobMonitor
.ObserveActiveJobs<TJobType>()
.QueryWhenChanged(query => query.Items.Any(x =>
{
if (x.Definition is not TJobType concrete) return false;
return predicate(concrete);
}));
}

/// <summary>
/// Gets an observable of the average progress percent of all given jobs.
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@ namespace NexusMods.Abstractions.UI;
public interface IReactiveR3Object : ReactiveUI.IReactiveObject, IDisposable
{
Observable<bool> Activation { get; }
void Activate();
IDisposable Activate();
void Deactivate();
}

@@ -23,7 +23,12 @@ public class ReactiveR3Object : IReactiveR3Object
private readonly BehaviorSubject<bool> _activation = new(initialValue: false);
public Observable<bool> Activation => _activation;

public void Activate() => _activation.OnNext(true);
public IDisposable Activate()
{
_activation.OnNext(true);
return Disposable.Create(this, static self => self.Deactivate());
}

public void Deactivate()
{
// NOTE(erri120): no need to deactivate disposed objects, as
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
using JetBrains.Annotations;
using NexusMods.Abstractions.UI;
using NexusMods.Abstractions.UI.Extensions;
using R3;

namespace NexusMods.App.UI.Controls;

/// <summary>
/// Component for a single value and it's formatted string representation.
/// </summary>
[PublicAPI]
public abstract class AFormattedValueComponent<T> : ReactiveR3Object, IItemModelComponent
where T : notnull
{
/// <summary>
/// Gets the value property.
/// </summary>
public BindableReactiveProperty<T> Value { get; }

/// <summary>
/// Gets the formatted value property.
/// </summary>
public BindableReactiveProperty<string> FormattedValue { get; }

private readonly IDisposable? _activationDisposable;

/// <summary>
/// Constructor.
/// </summary>
/// <param name="initialValue">Initial value.</param>
/// <param name="initialFormattedValue">Initial value formatted.</param>
/// <param name="valueObservable">Observable.</param>
/// <param name="subscribeWhenCreated">Whether to subscribe immediately when the component gets created or when the component gets activated.</param>
protected AFormattedValueComponent(T initialValue, string initialFormattedValue, IObservable<T> valueObservable, bool subscribeWhenCreated = false) : this(initialValue, initialFormattedValue, valueObservable.ToObservable(), subscribeWhenCreated) { }

/// <summary>
/// Constructor.
/// </summary>
/// <param name="initialValue">Initial value.</param>
/// <param name="initialFormattedValue">Initial value formatted.</param>
/// <param name="valueObservable">Observable.</param>
/// <param name="subscribeWhenCreated">Whether to subscribe immediately when the component gets created or when the component gets activated.</param>
protected AFormattedValueComponent(T initialValue, string initialFormattedValue, Observable<T> valueObservable, bool subscribeWhenCreated = false)
{
if (!subscribeWhenCreated)
{
Value = new BindableReactiveProperty<T>(value: initialValue);
FormattedValue = new BindableReactiveProperty<string>(value: initialFormattedValue);

_activationDisposable = this.WhenActivated(valueObservable, static (self, valueObservable, disposables) =>
{
valueObservable.ObserveOnUIThreadDispatcher().Subscribe(self, static (value, self) =>
{
self.Value.Value = value;
self.FormattedValue.Value = self.FormatValue(value);
}).AddTo(disposables);
});
}
else
{
Value = valueObservable.ToBindableReactiveProperty(initialValue: initialValue);
FormattedValue = Value
.Select(this, static (value, self) => self.FormatValue(value))
.ToBindableReactiveProperty(initialValue: initialFormattedValue);
}
}

/// <summary>
/// Constructor.
/// </summary>
protected AFormattedValueComponent(T value, string formattedValue)
{
Value = new BindableReactiveProperty<T>(value: value);
FormattedValue = new BindableReactiveProperty<string>(value: formattedValue);
}

/// <summary>
/// Formats the given value to a localized string representation.
/// </summary>
protected abstract string FormatValue(T value);

private bool _isDisposed;
protected override void Dispose(bool disposing)
{
if (!_isDisposed)
{
if (disposing)
{
_activationDisposable?.Dispose();
Disposable.Dispose(Value, FormattedValue);
}

_isDisposed = true;
}

base.Dispose(disposing);
}
}
80 changes: 80 additions & 0 deletions src/NexusMods.App.UI/Controls/TreeDataGrid/AValueComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using JetBrains.Annotations;
using NexusMods.Abstractions.UI;
using NexusMods.Abstractions.UI.Extensions;
using R3;

namespace NexusMods.App.UI.Controls;

/// <summary>
/// Component for a single value.
/// </summary>
[PublicAPI]
public abstract class AValueComponent<T> : ReactiveR3Object, IItemModelComponent
where T : notnull
{
/// <summary>
/// Gets the value property.
/// </summary>
public BindableReactiveProperty<T> Value { get; }

private readonly IDisposable? _activationDisposable;

/// <summary>
/// Constructor.
/// </summary>
/// <param name="initialValue">Initial value.</param>
/// <param name="valueObservable">Observable.</param>
/// <param name="subscribeWhenCreated">Whether to subscribe immediately when the component gets created or when the component gets activated.</param>
protected AValueComponent(T initialValue, IObservable<T> valueObservable, bool subscribeWhenCreated = false) : this(initialValue, valueObservable.ToObservable(), subscribeWhenCreated) { }

/// <summary>
/// Constructor.
/// </summary>
/// <param name="initialValue">Initial value.</param>
/// <param name="valueObservable">Observable.</param>
/// <param name="subscribeWhenCreated">Whether to subscribe immediately when the component gets created or when the component gets activated.</param>
protected AValueComponent(T initialValue, Observable<T> valueObservable, bool subscribeWhenCreated = false)
{
if (!subscribeWhenCreated)
{
Value = new BindableReactiveProperty<T>(value: initialValue);

_activationDisposable = this.WhenActivated(valueObservable, static (self, valueObservable, disposables) =>
{
valueObservable.ObserveOnUIThreadDispatcher().Subscribe(self, static (value, self) =>
{
self.Value.Value = value;
}).AddTo(disposables);
});
}
else
{
Value = valueObservable.ToBindableReactiveProperty(initialValue: initialValue);
}
}

/// <summary>
/// Constructor.
/// </summary>
protected AValueComponent(T value)
{
Value = new BindableReactiveProperty<T>(value: value);
}

private bool _isDisposed;
protected override void Dispose(bool disposing)
{
if (!_isDisposed)
{
if (disposing)
{
_activationDisposable?.Dispose();
Value.Dispose();
}

_isDisposed = true;
}

base.Dispose(disposing);
}
}
152 changes: 152 additions & 0 deletions src/NexusMods.App.UI/Controls/TreeDataGrid/ColumnContentControl.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml.Templates;
using ObservableCollections;
using R3;

namespace NexusMods.App.UI.Controls;

public interface IComponentTemplate
{
Type ComponentType { get; }

ComponentKey ComponentKey { get; }

DataTemplate DataTemplate { get; }
}

public class ComponentTemplate<TComponent> : IComponentTemplate
where TComponent : class, IItemModelComponent<TComponent>, IComparable<TComponent>
{
public Type ComponentType => typeof(TComponent);
public ComponentKey ComponentKey { get; set; }

// NOTE(erri120): Rider currently is unable to properly understand DataTypeAttribute.
// The below is a hack and will be replaced in the future when Rider fixes their
// inline hints. See the bug report for more details:
// https://youtrack.jetbrains.com/issue/RIDER-121820

private DataTemplate? _dataTemplate;
public DataTemplate DataTemplate
{
get => _dataTemplate ?? throw new InvalidOperationException();
set
{
if (value.DataType != ComponentType) throw new InvalidOperationException();
_dataTemplate = value;
}
}
}

/// <summary>
/// Control for columns where row models are <see cref="CompositeItemModel{TKey}"/>.
/// </summary>
public class ColumnContentControl<TKey> : ContentControl
where TKey : notnull
{
[SuppressMessage("ReSharper", "CollectionNeverUpdated.Global", Justification = "Updated in XAML")]
public List<IComponentTemplate> AvailableTemplates { get; } = [];

public Control? Fallback { get; set; }

private readonly SerialDisposable _serialDisposable = new();

/// <summary>
/// Builds a control from the first template in <see cref="AvailableTemplates"/>
/// that matches with a component in the item model.
/// </summary>
private Control? BuildContent(CompositeItemModel<TKey> itemModel)
{
foreach (var template in AvailableTemplates)
{
if (!itemModel.TryGet(template.ComponentKey, template.ComponentType, out var component)) continue;

// NOTE(erri120): DataTemplate.Build doesn't use the
// data you give it, need to manually set the DataContext.
// Otherwise. the new control will inherit the parent context,
// which is CompositeItemModel<TKey>.
var control = template.DataTemplate.Build(data: null);
if (control is null) return null;

control.DataContext = component;
return control;
}

return Fallback;
}

/// <summary>
/// Subscribes to component changes in the item model and rebuilds the content.
/// </summary>
private IDisposable Subscribe(CompositeItemModel<TKey> itemModel)
{
return itemModel.Components
.ObserveChanged()
.ObserveOnUIThreadDispatcher()
.Subscribe((this, itemModel), static (_, state) =>
{
var (self, itemModel) = state;
if (self.Presenter is null) return;

var content = self.BuildContent(itemModel);
self.Presenter.Content = content;
});
}

protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);

if (change.Property == ContentProperty)
{
if (change.NewValue is not CompositeItemModel<TKey> itemModel)
{
_serialDisposable.Disposable = null;
return;
}

// NOTE(erri120): we only care about Content changes when the
// Control is fully constructed and rendered on screen.
if (IsLoaded) _serialDisposable.Disposable = Subscribe(itemModel);
}
}

protected override bool RegisterContentPresenter(ContentPresenter presenter)
{
var didRegister = base.RegisterContentPresenter(presenter);

// NOTE(erri120): Puts content into the presenter before the first render.
if (didRegister && Content is CompositeItemModel<TKey> itemModel)
{
var content = BuildContent(itemModel);
presenter.Content = content;
}

return didRegister;
}

protected override void OnLoaded(RoutedEventArgs e)
{
base.OnLoaded(e);

if (Content is not CompositeItemModel<TKey> itemModel)
{
_serialDisposable.Disposable = null;
return;
}

Debug.Assert(_serialDisposable.Disposable is null, "nothing should've subscribed yet");
_serialDisposable.Disposable = Subscribe(itemModel);
}

protected override void OnUnloaded(RoutedEventArgs e)
{
base.OnUnloaded(e);

_serialDisposable.Disposable = null;
}
}
11 changes: 11 additions & 0 deletions src/NexusMods.App.UI/Controls/TreeDataGrid/ComponentKey.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using JetBrains.Annotations;
using TransparentValueObjects;

namespace NexusMods.App.UI.Controls;

[PublicAPI]
[ValueObject<string>]
public readonly partial struct ComponentKey
{
public static implicit operator ComponentKey(Type type) => From(type.ToString());
}
Loading

0 comments on commit 6962ff4

Please sign in to comment.