Browse Source

Add persistent views, dock serialization

pull/165/head
Ionite 1 year ago
parent
commit
96ab576443
No known key found for this signature in database
  1. 18
      StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs
  2. 48
      StabilityMatrix.Avalonia/Controls/Dock/DockUserControlBase.cs
  3. 38
      StabilityMatrix.Avalonia/Controls/ModelCard.axaml
  4. 8
      StabilityMatrix.Avalonia/Models/IPersistentViewProvider.cs
  5. 3
      StabilityMatrix.Avalonia/Models/Inference/ModelCardModel.cs
  6. 4
      StabilityMatrix.Avalonia/Services/ServiceManager.cs
  7. 67
      StabilityMatrix.Avalonia/ViewLocator.cs
  8. 28
      StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTabViewModelBase.cs
  9. 29
      StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs
  10. 26
      StabilityMatrix.Avalonia/ViewModels/Inference/ModelCardViewModel.cs
  11. 123
      StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs
  12. 50
      StabilityMatrix.Avalonia/Views/InferencePage.axaml
  13. 40
      StabilityMatrix.Avalonia/Views/InferencePage.axaml.cs
  14. 6
      StabilityMatrix.Avalonia/Views/InferenceTextToImageView.axaml
  15. 14
      StabilityMatrix.Avalonia/Views/InferenceTextToImageView.axaml.cs
  16. 12
      StabilityMatrix.Core/Attributes/ViewAttribute.cs

18
StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs

@ -3,18 +3,15 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using Avalonia; using Avalonia;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Threading;
using Avalonia.Xaml.Interactivity; using Avalonia.Xaml.Interactivity;
using AvaloniaEdit; using AvaloniaEdit;
using AvaloniaEdit.Document; using AvaloniaEdit.Document;
using AvaloniaEdit.Editing; using AvaloniaEdit.Editing;
using NLog; using NLog;
using StabilityMatrix.Avalonia.Controls.CodeCompletion; using StabilityMatrix.Avalonia.Controls.CodeCompletion;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Models.TagCompletion; using StabilityMatrix.Avalonia.Models.TagCompletion;
using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Extensions;
using TextMateSharp.Grammars; using TextMateSharp.Grammars;
using CompletionWindow = StabilityMatrix.Avalonia.Controls.CodeCompletion.CompletionWindow;
namespace StabilityMatrix.Avalonia.Behaviors; namespace StabilityMatrix.Avalonia.Behaviors;
@ -27,19 +24,20 @@ public class TextEditorCompletionBehavior : Behavior<TextEditor>
private CompletionWindow? completionWindow; private CompletionWindow? completionWindow;
public static readonly StyledProperty<ICompletionProvider> CompletionProviderProperty = public static readonly StyledProperty<ICompletionProvider?> CompletionProviderProperty =
AvaloniaProperty.Register<TextEditorCompletionBehavior, ICompletionProvider>(nameof(CompletionProvider)); AvaloniaProperty.Register<TextEditorCompletionBehavior, ICompletionProvider?>(nameof(CompletionProvider));
public ICompletionProvider CompletionProvider public ICompletionProvider? CompletionProvider
{ {
get => GetValue(CompletionProviderProperty); get => GetValue(CompletionProviderProperty);
set => SetValue(CompletionProviderProperty, value); set => SetValue(CompletionProviderProperty, value);
} }
public static readonly StyledProperty<ITokenizerProvider> TokenizerProviderProperty = AvaloniaProperty.Register<TextEditorCompletionBehavior, ITokenizerProvider>( public static readonly StyledProperty<ITokenizerProvider?> TokenizerProviderProperty =
AvaloniaProperty.Register<TextEditorCompletionBehavior, ITokenizerProvider?>(
"TokenizerProvider"); "TokenizerProvider");
public ITokenizerProvider TokenizerProvider public ITokenizerProvider? TokenizerProvider
{ {
get => GetValue(TokenizerProviderProperty); get => GetValue(TokenizerProviderProperty);
set => SetValue(TokenizerProviderProperty, value); set => SetValue(TokenizerProviderProperty, value);
@ -78,7 +76,7 @@ public class TextEditorCompletionBehavior : Behavior<TextEditor>
private CompletionWindow CreateCompletionWindow(TextArea textArea) private CompletionWindow CreateCompletionWindow(TextArea textArea)
{ {
var window = new CompletionWindow(textArea, CompletionProvider, TokenizerProvider) var window = new CompletionWindow(textArea, CompletionProvider!, TokenizerProvider!)
{ {
WindowManagerAddShadowHint = false, WindowManagerAddShadowHint = false,
CloseWhenCaretAtBeginning = true, CloseWhenCaretAtBeginning = true,
@ -189,7 +187,7 @@ public class TextEditorCompletionBehavior : Behavior<TextEditor>
var caretAbsoluteOffset = caret - line.Offset; var caretAbsoluteOffset = caret - line.Offset;
// Tokenize // Tokenize
var result = TokenizerProvider.TokenizeLine(lineText); var result = TokenizerProvider!.TokenizeLine(lineText);
var currentTokenIndex = -1; var currentTokenIndex = -1;
IToken? currentToken = null; IToken? currentToken = null;

48
StabilityMatrix.Avalonia/Controls/Dock/DockUserControlBase.cs

@ -0,0 +1,48 @@
using System;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Dock.Avalonia.Controls;
using Dock.Model;
using Dock.Model.Avalonia.Json;
using Dock.Model.Core;
namespace StabilityMatrix.Avalonia.Controls.Dock;
/// <summary>
/// Base for Dock controls
/// Expects a <see cref="DockControl"/> named "Dock" in the XAML
/// </summary>
public abstract class DockUserControlBase : UserControlBase
{
private DockControl _dock = null!;
protected readonly AvaloniaDockSerializer DockSerializer = new();
protected readonly DockState DockState = new();
/// <inheritdoc />
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_dock = this.FindControl<DockControl>("Dock")
?? throw new NullReferenceException("DockControl not found");
if (_dock.Layout is { } layout)
{
DockState.Save(layout);
}
}
protected virtual void LoadDockLayout(string data)
{
if (DockSerializer.Deserialize<IDock?>(data) is { } layout)
{
_dock.Layout = layout;
DockState.Restore(layout);
}
}
protected virtual string SaveDockLayout()
{
return DockSerializer.Serialize(_dock.Layout);
}
}

38
StabilityMatrix.Avalonia/Controls/ModelCard.axaml

@ -6,7 +6,9 @@
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData" xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
x:DataType="inference:ModelCardViewModel"> x:DataType="inference:ModelCardViewModel">
<Design.PreviewWith> <Design.PreviewWith>
<controls:ModelCard DataContext="{x:Static mocks:DesignData.ModelCardViewModel}"/> <StackPanel MinWidth="300">
<controls:ModelCard DataContext="{x:Static mocks:DesignData.ModelCardViewModel}"/>
</StackPanel>
</Design.PreviewWith> </Design.PreviewWith>
<Style Selector="controls|ModelCard"> <Style Selector="controls|ModelCard">
@ -14,20 +16,46 @@
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate> <ControlTemplate>
<controls:Card Padding="8"> <controls:Card Padding="8">
<Grid Margin="8" ColumnDefinitions="Auto,*,Auto"> <Grid Margin="8" RowDefinitions="*,*" ColumnDefinitions="Auto,*,Auto">
<!-- Model -->
<TextBlock <TextBlock
Grid.Column="0" Grid.Column="0"
Margin="0,0,8,0"
VerticalAlignment="Center" VerticalAlignment="Center"
HorizontalAlignment="Left"
MinWidth="60"
Text="Model" /> Text="Model" />
<ui:FAComboBox <ui:FAComboBox
Grid.Row="0"
Grid.Column="1" Grid.Column="1"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
ItemsSource="{Binding ClientManager.ModelNames}" ItemsSource="{Binding ClientManager.ModelNames}"
SelectedItem="{Binding SelectedModelName, Mode=TwoWay}" /> SelectedItem="{Binding SelectedModelName, Mode=TwoWay}" />
<Button Grid.Column="2" Margin="8,0,0,0">
<ui:SymbolIcon FontSize="16" Symbol="Refresh" /> <Button
Grid.Row="0"
Grid.Column="2"
Margin="8,0,0,0">
<ui:SymbolIcon FontSize="16" Symbol="Setting" />
</Button> </Button>
<!-- VAE -->
<TextBlock
Grid.Column="0"
Grid.Row="1"
MinWidth="60"
Margin="0,8,0,0"
VerticalAlignment="Center"
Text="VAE" />
<ui:FAComboBox
Grid.Row="1"
Grid.Column="1"
Grid.ColumnSpan="2"
Margin="0,8,0,0"
HorizontalAlignment="Stretch"
ItemsSource="{Binding ClientManager.ModelNames}"
SelectedItem="{Binding SelectedModelName, Mode=TwoWay}" />
</Grid> </Grid>
</controls:Card> </controls:Card>
</ControlTemplate> </ControlTemplate>

8
StabilityMatrix.Avalonia/Models/IPersistentViewProvider.cs

@ -0,0 +1,8 @@
using Avalonia.Controls;
namespace StabilityMatrix.Avalonia.Models;
public interface IPersistentViewProvider
{
Control? AttachedPersistentView { get; set; }
}

3
StabilityMatrix.Avalonia/Models/Inference/ModelCardModel.cs

@ -1,3 +0,0 @@
namespace StabilityMatrix.Avalonia.Models.Inference;
public record ModelCardModel(string? SelectedModelName) { }

4
StabilityMatrix.Avalonia/Services/ServiceManager.cs

@ -192,7 +192,7 @@ public class ServiceManager<T>
throw new InvalidOperationException($"View not found for {instance.GetType().FullName}"); throw new InvalidOperationException($"View not found for {instance.GetType().FullName}");
} }
if (Activator.CreateInstance(viewAttr.GetViewType()) is not Control view) if (Activator.CreateInstance(viewAttr.ViewType) is not Control view)
{ {
throw new NullReferenceException($"Unable to create instance for {instance.GetType().FullName}"); throw new NullReferenceException($"Unable to create instance for {instance.GetType().FullName}");
} }
@ -214,7 +214,7 @@ public class ServiceManager<T>
throw new InvalidOperationException($"View not found for {instance.GetType().FullName}"); throw new InvalidOperationException($"View not found for {instance.GetType().FullName}");
} }
if (Activator.CreateInstance(viewAttr.GetViewType()) is not Control view) if (Activator.CreateInstance(viewAttr.ViewType) is not Control view)
{ {
throw new NullReferenceException($"Unable to create instance for {instance.GetType().FullName}"); throw new NullReferenceException($"Unable to create instance for {instance.GetType().FullName}");
} }

67
StabilityMatrix.Avalonia/ViewLocator.cs

@ -1,8 +1,9 @@
using System; using System;
using System.Diagnostics;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using NLog;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
@ -10,6 +11,13 @@ namespace StabilityMatrix.Avalonia;
public class ViewLocator : IDataTemplate, INavigationPageFactory public class ViewLocator : IDataTemplate, INavigationPageFactory
{ {
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
/*/// <summary>
/// Weak Dictionary of (DataContext, View) pairs to keep the view and layout alive
/// </summary>
private static readonly ConditionalWeakTable<object, Control> PersistentViewCache = new();*/
/// <inheritdoc /> /// <inheritdoc />
public Control Build(object? data) public Control Build(object? data)
{ {
@ -19,8 +27,8 @@ public class ViewLocator : IDataTemplate, INavigationPageFactory
if (Attribute.GetCustomAttribute(type, typeof(ViewAttribute)) is ViewAttribute viewAttr) if (Attribute.GetCustomAttribute(type, typeof(ViewAttribute)) is ViewAttribute viewAttr)
{ {
var viewType = viewAttr.GetViewType(); var viewType = viewAttr.ViewType;
return GetView(viewType); return GetView(viewType, data, viewAttr.IsPersistent);
} }
return new TextBlock return new TextBlock
@ -31,7 +39,6 @@ public class ViewLocator : IDataTemplate, INavigationPageFactory
private Control GetView(Type viewType) private Control GetView(Type viewType)
{ {
// Otherwise get from the service provider
if (App.Services.GetService(viewType) is Control view) if (App.Services.GetService(viewType) is Control view)
{ {
return view; return view;
@ -43,6 +50,49 @@ public class ViewLocator : IDataTemplate, INavigationPageFactory
}; };
} }
private Control GetView(Type viewType, object context, bool persistent)
{
if (persistent)
{
// Check assignable from IPersistentViewProvider
if (context is not IPersistentViewProvider persistentViewProvider)
{
throw new InvalidOperationException(
$"View {viewType.Name} is marked as persistent but does not implement IPersistentViewProvider");
}
// Try get from context
if (persistentViewProvider.AttachedPersistentView is { } view)
{
Logger.Trace("Got persistent view {ViewType} from context", viewType.Name);
return view;
}
// Otherwise get from service provider
if (App.Services.GetService(viewType) is Control newView)
{
// Set as attached view
persistentViewProvider.AttachedPersistentView = newView;
Logger.Trace("Attached persistent view {ViewType}", viewType.Name);
return newView;
}
}
else
{
// Get from service provider
if (App.Services.GetService(viewType) is Control view)
{
return view;
}
}
return new TextBlock
{
Text = "View Not Found: " + viewType.FullName
};
}
/// <inheritdoc /> /// <inheritdoc />
public bool Match(object? data) public bool Match(object? data)
{ {
@ -58,9 +108,10 @@ public class ViewLocator : IDataTemplate, INavigationPageFactory
throw new InvalidOperationException("View not found for " + srcType.FullName); throw new InvalidOperationException("View not found for " + srcType.FullName);
} }
var viewType = viewAttr.GetViewType(); // Get new view
var view = GetView(viewType); var view = GetView(viewAttr.ViewType);
view.DataContext ??= App.Services.GetService(srcType); view.DataContext ??= App.Services.GetService(srcType);
return view; return view;
} }
@ -73,8 +124,8 @@ public class ViewLocator : IDataTemplate, INavigationPageFactory
throw new InvalidOperationException("View not found for " + target.GetType().FullName); throw new InvalidOperationException("View not found for " + target.GetType().FullName);
} }
var viewType = viewAttr.GetViewType(); var viewType = viewAttr.ViewType;
var view = GetView(viewType); var view = GetView(viewType, target, viewAttr.IsPersistent);
view.DataContext ??= target; view.DataContext ??= target;
return view; return view;
} }

28
StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTabViewModelBase.cs

@ -1,16 +1,20 @@
using System.Text.Json.Serialization; using System;
using System.Text.Json.Serialization;
using Avalonia.Controls;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.FileInterfaces;
#pragma warning disable CS0657 // Not a valid attribute location for this declaration #pragma warning disable CS0657 // Not a valid attribute location for this declaration
namespace StabilityMatrix.Avalonia.ViewModels.Inference; namespace StabilityMatrix.Avalonia.ViewModels.Inference;
public abstract partial class InferenceTabViewModelBase : LoadableViewModelBase public abstract partial class InferenceTabViewModelBase : LoadableViewModelBase, IDisposable, IPersistentViewProvider
{ {
/// <summary> /// <summary>
/// The title of the tab /// The title of the tab
/// </summary> /// </summary>
public virtual string TabTitle => ProjectFile?.Name ?? "New Project"; public virtual string TabTitle => ProjectFile?.NameWithoutExtension ?? "New Project";
/// <summary> /// <summary>
/// Whether there are unsaved changes /// Whether there are unsaved changes
@ -26,4 +30,22 @@ public abstract partial class InferenceTabViewModelBase : LoadableViewModelBase
[NotifyPropertyChangedFor(nameof(TabTitle))] [NotifyPropertyChangedFor(nameof(TabTitle))]
[property: JsonIgnore] [property: JsonIgnore]
private FilePath? projectFile; private FilePath? projectFile;
/// <inheritdoc />
Control? IPersistentViewProvider.AttachedPersistentView { get; set; }
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
((IPersistentViewProvider) this).AttachedPersistentView = null;
}
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
} }

29
StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs

@ -31,7 +31,7 @@ using StabilityMatrix.Core.Models.Api.Comfy.WebSocketData;
namespace StabilityMatrix.Avalonia.ViewModels.Inference; namespace StabilityMatrix.Avalonia.ViewModels.Inference;
[View(typeof(InferenceTextToImageView))] [View(typeof(InferenceTextToImageView), persistent: true)]
public partial class InferenceTextToImageViewModel : InferenceTabViewModelBase public partial class InferenceTextToImageViewModel : InferenceTabViewModelBase
{ {
private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
@ -129,7 +129,7 @@ public partial class InferenceTextToImageViewModel : InferenceTabViewModelBase
// GenerateImageCommand.WithNotificationErrorHandler(notificationService); // GenerateImageCommand.WithNotificationErrorHandler(notificationService);
} }
private (NodeDictionary prompt, string[] outputs) BuildPrompt() private (NodeDictionary prompt, string[] outputs) BuildPrompt(GenerateOverrides? overrides = null)
{ {
using var _ = new CodeTimer(); using var _ = new CodeTimer();
@ -237,7 +237,7 @@ public partial class InferenceTextToImageViewModel : InferenceTabViewModelBase
); );
// If hi-res fix is enabled, add the LatentUpscale node and another KSampler node // If hi-res fix is enabled, add the LatentUpscale node and another KSampler node
if (IsHiresFixEnabled) if (overrides is { IsHiresFixEnabled: true } || IsHiresFixEnabled)
{ {
var hiresUpscalerCard = UpscalerCardViewModel; var hiresUpscalerCard = UpscalerCardViewModel;
var hiresSamplerCard = HiresSamplerCardViewModel; var hiresSamplerCard = HiresSamplerCardViewModel;
@ -347,7 +347,9 @@ public partial class InferenceTextToImageViewModel : InferenceTabViewModelBase
}); });
} }
private async Task GenerateImageImpl(CancellationToken cancellationToken = default) private async Task GenerateImageImpl(
GenerateOverrides? overrides = null,
CancellationToken cancellationToken = default)
{ {
if (!ClientManager.IsConnected) if (!ClientManager.IsConnected)
{ {
@ -357,7 +359,8 @@ public partial class InferenceTextToImageViewModel : InferenceTabViewModelBase
// If enabled, randomize the seed // If enabled, randomize the seed
var seedCard = StackCardViewModel.GetCard<SeedCardViewModel>(); var seedCard = StackCardViewModel.GetCard<SeedCardViewModel>();
if (seedCard.IsRandomizeEnabled) if (overrides is not { UseCurrentSeed: true } &&
seedCard.IsRandomizeEnabled)
{ {
seedCard.GenerateNewSeed(); seedCard.GenerateNewSeed();
} }
@ -474,15 +477,27 @@ public partial class InferenceTextToImageViewModel : InferenceTabViewModelBase
} }
[RelayCommand(IncludeCancelCommand = true)] [RelayCommand(IncludeCancelCommand = true)]
private async Task GenerateImage(CancellationToken cancellationToken = default) private async Task GenerateImage(string? options = null, CancellationToken cancellationToken = default)
{ {
try try
{ {
await GenerateImageImpl(cancellationToken); var overrides = new GenerateOverrides
{
IsHiresFixEnabled = options?.Contains("hires_fix"),
UseCurrentSeed = options?.Contains("current_seed")
};
await GenerateImageImpl(overrides, cancellationToken);
} }
catch (OperationCanceledException e) catch (OperationCanceledException e)
{ {
Logger.Debug($"[Image Generation Canceled] {e.Message}"); Logger.Debug($"[Image Generation Canceled] {e.Message}");
} }
} }
internal class GenerateOverrides
{
public bool? IsHiresFixEnabled { get; set; }
public bool? UseCurrentSeed { get; set; }
}
} }

26
StabilityMatrix.Avalonia/ViewModels/Inference/ModelCardViewModel.cs

@ -1,7 +1,5 @@
using System.Text.Json.Nodes; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.ComponentModel;
using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Models.Inference;
using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
@ -10,23 +8,19 @@ namespace StabilityMatrix.Avalonia.ViewModels.Inference;
[View(typeof(ModelCard))] [View(typeof(ModelCard))]
public partial class ModelCardViewModel : LoadableViewModelBase public partial class ModelCardViewModel : LoadableViewModelBase
{ {
[ObservableProperty] private string? selectedModelName; [ObservableProperty]
private string? selectedModelName;
public ModelCardViewModel(IInferenceClientManager clientManager) [ObservableProperty]
{ private string? selectedVaeName;
ClientManager = clientManager;
}
public IInferenceClientManager ClientManager { get; } [ObservableProperty]
private bool isVaeSelectionEnabled;
public override void LoadStateFromJsonObject(JsonObject state) public IInferenceClientManager ClientManager { get; }
{
var model = DeserializeModel<ModelCardModel>(state);
SelectedModelName = model.SelectedModelName;
}
public override JsonObject SaveStateToJsonObject() public ModelCardViewModel(IInferenceClientManager clientManager)
{ {
return SerializeModel(new ModelCardModel(SelectedModelName)); ClientManager = clientManager;
} }
} }

123
StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs

@ -1,8 +1,7 @@
using System; using System;
using System.Collections.ObjectModel;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Collections;
using Avalonia.Controls.Notifications; using Avalonia.Controls.Notifications;
using Avalonia.Platform.Storage; using Avalonia.Platform.Storage;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
@ -52,10 +51,13 @@ public partial class InferenceViewModel : PageViewModelBase
public IInferenceClientManager ClientManager { get; } public IInferenceClientManager ClientManager { get; }
public AvaloniaList<InferenceTabViewModelBase> Tabs { get; } = new(); public ObservableCollection<InferenceTabViewModelBase> Tabs { get; } = new();
[ObservableProperty] [ObservableProperty]
private LoadableViewModelBase? selectedTab; private InferenceTabViewModelBase? selectedTab;
[ObservableProperty]
private int selectedTabIndex;
[ObservableProperty] [ObservableProperty]
private PackagePair? runningPackage; private PackagePair? runningPackage;
@ -64,7 +66,6 @@ public partial class InferenceViewModel : PageViewModelBase
ServiceManager<ViewModelBase> vmFactory, ServiceManager<ViewModelBase> vmFactory,
IApiFactory apiFactory, IApiFactory apiFactory,
INotificationService notificationService, INotificationService notificationService,
// IRelayCommandFactory commandFactory,
IInferenceClientManager inferenceClientManager, IInferenceClientManager inferenceClientManager,
ISettingsManager settingsManager ISettingsManager settingsManager
) )
@ -72,7 +73,6 @@ public partial class InferenceViewModel : PageViewModelBase
this.vmFactory = vmFactory; this.vmFactory = vmFactory;
this.apiFactory = apiFactory; this.apiFactory = apiFactory;
this.notificationService = notificationService; this.notificationService = notificationService;
// this.commandFactory = commandFactory;
this.settingsManager = settingsManager; this.settingsManager = settingsManager;
ClientManager = inferenceClientManager; ClientManager = inferenceClientManager;
@ -88,18 +88,12 @@ public partial class InferenceViewModel : PageViewModelBase
public override void OnLoaded() public override void OnLoaded()
{ {
base.OnLoaded();
if (Tabs.Count == 0) if (Tabs.Count == 0)
{ {
AddTab(); AddTab();
} }
// Select first tab if none is selected
if (SelectedTab is null && Tabs.Count > 0)
{
SelectedTab = Tabs[0];
}
base.OnLoaded();
} }
/// <summary> /// <summary>
@ -109,6 +103,9 @@ public partial class InferenceViewModel : PageViewModelBase
private void AddTab() private void AddTab()
{ {
Tabs.Add(vmFactory.Get<InferenceTextToImageViewModel>()); Tabs.Add(vmFactory.Get<InferenceTextToImageViewModel>());
// Set as new selected tab
SelectedTabIndex = Tabs.Count - 1;
} }
/// <summary> /// <summary>
@ -123,7 +120,26 @@ public partial class InferenceViewModel : PageViewModelBase
} }
Logger.Trace("Closing tab {Title}", vm.TabTitle); Logger.Trace("Closing tab {Title}", vm.TabTitle);
Tabs.Remove(vm);
// Set the selected tab to the next tab if there is one, then previous, then null
lock (Tabs)
{
var index = Tabs.IndexOf(vm);
if (index < Tabs.Count - 1)
{
SelectedTabIndex = index + 1;
}
else if (index > 0)
{
SelectedTabIndex = index - 1;
}
// Remove the tab
Tabs.RemoveAt(index);
// Dispose the view model
vm.Dispose();
}
} }
/// <summary> /// <summary>
@ -233,6 +249,9 @@ public partial class InferenceViewModel : PageViewModelBase
return; return;
} }
// Update project file
currentTab.ProjectFile = new FilePath(result.TryGetLocalPath()!);
notificationService.Show( notificationService.Show(
"Saved", "Saved",
$"Saved project to {result.Name}", $"Saved project to {result.Name}",
@ -240,8 +259,56 @@ public partial class InferenceViewModel : PageViewModelBase
); );
} }
/*public AsyncRelayCommand MenuOpenProjectCommand => /// <summary>
commandFactory.CreateWithNotificationErrorHandling(MenuOpenProject);*/ /// Menu "Save Project" command.
/// </summary>
[RelayCommand(FlowExceptionsToTaskScheduler = true)]
private async Task MenuSave()
{
// Get the current tab
var currentTab = SelectedTab;
if (currentTab == null)
{
Logger.Info("MenuSaveProject: currentTab is null");
return;
}
// If the tab has no project file, prompt for save as
if (currentTab.ProjectFile is not { } projectFile)
{
await MenuSaveAs();
return;
}
// Otherwise, save to the current project file
var document = InferenceProjectDocument.FromLoadable(currentTab);
// Save to file
try
{
await using var stream = projectFile.Info.OpenWrite();
await JsonSerializer.SerializeAsync(
stream,
document,
new JsonSerializerOptions { WriteIndented = true }
);
}
catch (Exception e)
{
notificationService.ShowPersistent(
"Could not save to file",
$"[{e.GetType().Name}] {e.Message}",
NotificationType.Error);
return;
}
notificationService.Show(
"Saved",
$"Saved project to {projectFile.Name}",
NotificationType.Success
);
}
/// <summary> /// <summary>
/// Menu "Open Project" command. /// Menu "Open Project" command.
@ -285,22 +352,28 @@ public partial class InferenceViewModel : PageViewModelBase
var document = await JsonSerializer.DeserializeAsync<InferenceProjectDocument>(stream); var document = await JsonSerializer.DeserializeAsync<InferenceProjectDocument>(stream);
if (document is null) if (document is null)
{ {
Logger.Warn("MenuOpenProject: Deserialize project file returned null"); throw new ApplicationException("MenuOpenProject: Deserialize project file returned null");
return; }
if (document.State is null)
{
throw new ApplicationException("MenuOpenProject: Deserialize project file returned null state");
} }
InferenceTabViewModelBase? vm = null; InferenceTabViewModelBase vm;
if (document.ProjectType is InferenceProjectType.TextToImage && document.State is not null) if (document.ProjectType is InferenceProjectType.TextToImage)
{ {
// Get view model
var textToImage = vmFactory.Get<InferenceTextToImageViewModel>(); var textToImage = vmFactory.Get<InferenceTextToImageViewModel>();
// Load state
textToImage.LoadStateFromJsonObject(document.State); textToImage.LoadStateFromJsonObject(document.State);
// Set the file backing the view model
textToImage.ProjectFile = new FilePath(file.TryGetLocalPath()!);
vm = textToImage; vm = textToImage;
} }
else
if (vm == null)
{ {
Logger.Warn("MenuOpenProject: Unknown project type"); throw new InvalidOperationException($"Unsupported project type: {document.ProjectType}");
return;
} }
Tabs.Add(vm); Tabs.Add(vm);

50
StabilityMatrix.Avalonia/Views/InferencePage.axaml

@ -2,13 +2,10 @@
x:Class="StabilityMatrix.Avalonia.Views.InferencePage" x:Class="StabilityMatrix.Avalonia.Views.InferencePage"
xmlns="https://github.com/avaloniaui" xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:avaloniaEdit="clr-namespace:AvaloniaEdit;assembly=AvaloniaEdit"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls" xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:icons="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData" xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:system="clr-namespace:System;assembly=System.Runtime"
xmlns:vmInference="using:StabilityMatrix.Avalonia.ViewModels.Inference" xmlns:vmInference="using:StabilityMatrix.Avalonia.ViewModels.Inference"
xmlns:ui="using:FluentAvalonia.UI.Controls" xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels" xmlns:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels"
@ -17,23 +14,59 @@
d:DesignWidth="1000" d:DesignWidth="1000"
x:DataType="vm:InferenceViewModel" x:DataType="vm:InferenceViewModel"
mc:Ignorable="d"> mc:Ignorable="d">
<controls:UserControlBase.Resources>
<ui:CommandBarFlyout Placement="Right" x:Key="AddTabFlyout">
<!-- Note: unlike a regular CommandBar, primary items can be set as the xml content and don't need
to be wrapped in a <ui:CommandBarFlyout.PrimaryCommands> tag
-->
<!--<ui:CommandBarButton Label="Text to Image" ToolTip.Tip="Text to Image">
<ui:CommandBarButton.IconSource>
<controls:FASymbolIconSource Symbol="fa-solid fa-bolt"/>
</ui:CommandBarButton.IconSource>
</ui:CommandBarButton>
<ui:CommandBarButton Label="Save" ToolTip.Tip="Save" />
<ui:CommandBarButton Label="Delete" ToolTip.Tip="Delete" />-->
<ui:CommandBarFlyout.SecondaryCommands>
<ui:CommandBarButton
IconSource="FullScreenMaximize"
Label="Text to Image"
Click="AddTabMenu_TextToImageButton_OnClick"
ToolTip.Tip="Text to Image"/>
<ui:CommandBarButton
IsEnabled="False"
IconSource="ImageCopy"
Label="Image to Image"
ToolTip.Tip="Image to Image"/>
<ui:CommandBarButton
IsEnabled="False"
IconSource="ImageEdit"
Label="Inpaint"
ToolTip.Tip="Inpaint"/>
</ui:CommandBarFlyout.SecondaryCommands>
</ui:CommandBarFlyout>
</controls:UserControlBase.Resources>
<Grid> <Grid>
<!-- AddTabButtonCommand="{Binding AddTabCommand}" -->
<ui:TabView <ui:TabView
x:Name="TabView"
CanDragTabs="True"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
CanReorderTabs="True" CanReorderTabs="True"
CloseButtonOverlayMode="Auto" CloseButtonOverlayMode="Auto"
TabCloseRequested="TabView_OnTabCloseRequested" TabCloseRequested="TabView_OnTabCloseRequested"
AddTabButtonCommand="{Binding AddTabCommand}" AddTabButtonClick="TabView_OnAddTabButtonClick"
TabItems="{Binding Tabs}" TabItems="{Binding Tabs}"
SelectedItem="{Binding SelectedTab, Mode=TwoWay}" SelectedItem="{Binding SelectedTab}"
SelectedIndex="{Binding SelectedTabIndex}"
TabWidthMode="SizeToContent"> TabWidthMode="SizeToContent">
<ui:TabView.TabItemTemplate> <ui:TabView.TabItemTemplate>
<DataTemplate DataType="{x:Type vmInference:InferenceTextToImageViewModel}"> <DataTemplate DataType="{x:Type vmInference:InferenceTextToImageViewModel}">
<ui:TabViewItem Header="Project 1" <ui:TabViewItem Header="{Binding TabTitle}"
IconSource="Document" IconSource="Document"
Content="{Binding}"> Content="{Binding}" />
</ui:TabViewItem>
</DataTemplate> </DataTemplate>
</ui:TabView.TabItemTemplate> </ui:TabView.TabItemTemplate>
<ui:TabView.TabStripFooter> <ui:TabView.TabStripFooter>
@ -58,6 +91,7 @@
InputGesture="Ctrl+O"/> InputGesture="Ctrl+O"/>
<Separator/> <Separator/>
<MenuItem Header="Save" <MenuItem Header="Save"
Command="{Binding MenuSaveCommand}"
InputGesture="Ctrl+S"/> InputGesture="Ctrl+S"/>
<MenuItem Header="Save As..." <MenuItem Header="Save As..."
Command="{Binding MenuSaveAsCommand}" Command="{Binding MenuSaveAsCommand}"

40
StabilityMatrix.Avalonia/Views/InferencePage.axaml.cs

@ -1,5 +1,9 @@
using Avalonia.Input; using System;
using Avalonia.Markup.Xaml; using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Interactivity;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.ViewModels; using StabilityMatrix.Avalonia.ViewModels;
@ -8,16 +12,23 @@ namespace StabilityMatrix.Avalonia.Views;
public partial class InferencePage : UserControlBase public partial class InferencePage : UserControlBase
{ {
private Button? _addButton;
private Button AddButton => _addButton
??= this.FindControl<TabView>("TabView")!
.GetTemplateChildren()
.OfType<Button>()
.First(p => p.Name == "AddButton");
private readonly CommandBarFlyout addTabFlyout;
public InferencePage() public InferencePage()
{ {
InitializeComponent(); InitializeComponent();
AddHandler(DragDrop.DropEvent, DropHandler); AddHandler(DragDrop.DropEvent, DropHandler);
AddHandler(DragDrop.DragOverEvent, DragOverHandler); AddHandler(DragDrop.DragOverEvent, DragOverHandler);
}
private void InitializeComponent() addTabFlyout = Resources["AddTabFlyout"] as CommandBarFlyout
{ ?? throw new NullReferenceException("AddTabFlyout not found");
AvaloniaXamlLoader.Load(this);
} }
private void TabView_OnTabCloseRequested(TabView sender, TabViewTabCloseRequestedEventArgs args) private void TabView_OnTabCloseRequested(TabView sender, TabViewTabCloseRequestedEventArgs args)
@ -40,4 +51,21 @@ public partial class InferencePage : UserControlBase
dropTarget.Drop(sender, e); dropTarget.Drop(sender, e);
} }
} }
private void TabView_OnAddTabButtonClick(TabView sender, EventArgs args)
{
ShowAddTabMenu(false);
}
private void ShowAddTabMenu(bool isTransient)
{
addTabFlyout.ShowMode = isTransient ? FlyoutShowMode.Transient : FlyoutShowMode.Standard;
addTabFlyout.ShowAt(AddButton);
}
private void AddTabMenu_TextToImageButton_OnClick(object? sender, RoutedEventArgs e)
{
(DataContext as InferenceViewModel)!.AddTabCommand.Execute(null);
}
} }

6
StabilityMatrix.Avalonia/Views/InferenceTextToImageView.axaml

@ -103,7 +103,7 @@
x:Name="PromptPane" x:Name="PromptPane"
Alignment="Right" Alignment="Right"
Id="PromptPane" Id="PromptPane"
Proportion="0.3"> Proportion="0.25">
<Tool <Tool
x:Name="PromptTool" x:Name="PromptTool"
Title="Prompt" Title="Prompt"
@ -137,6 +137,7 @@
icons:Attached.Icon="fa-solid fa-magnifying-glass-plus" icons:Attached.Icon="fa-solid fa-magnifying-glass-plus"
Classes="transparent" Classes="transparent"
Command="{Binding GenerateImageCommand}" Command="{Binding GenerateImageCommand}"
CommandParameter="current_seed,hires_fix"
ToolTip.Tip="Generate with Hires Scaling" /> ToolTip.Tip="Generate with Hires Scaling" />
<!-- Main Generate Button --> <!-- Main Generate Button -->
@ -174,6 +175,7 @@
icons:Attached.Icon="fa-solid fa-repeat" icons:Attached.Icon="fa-solid fa-repeat"
Classes="transparent" Classes="transparent"
Command="{Binding GenerateImageCommand}" Command="{Binding GenerateImageCommand}"
CommandParameter="current_seed"
ToolTip.Tip="Generate with current seed" /> ToolTip.Tip="Generate with current seed" />
</Grid> </Grid>
</controls:Card> </controls:Card>
@ -190,7 +192,7 @@
x:Name="ImageGalleryPane" x:Name="ImageGalleryPane"
Alignment="Right" Alignment="Right"
Id="ImageGalleryPane" Id="ImageGalleryPane"
Proportion="0.3"> Proportion="0.35">
<Tool <Tool
x:Name="ImageGalleryTool" x:Name="ImageGalleryTool"
Title="Image Output" Title="Image Output"

14
StabilityMatrix.Avalonia/Views/InferenceTextToImageView.axaml.cs

@ -1,17 +1,21 @@
using Avalonia.Markup.Xaml; using System.Diagnostics;
using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Controls.Dock;
namespace StabilityMatrix.Avalonia.Views; namespace StabilityMatrix.Avalonia.Views;
public partial class InferenceTextToImageView : UserControlBase public partial class InferenceTextToImageView : DockUserControlBase
{ {
public InferenceTextToImageView() public InferenceTextToImageView()
{ {
InitializeComponent(); InitializeComponent();
} }
private void InitializeComponent() ~InferenceTextToImageView()
{ {
AvaloniaXamlLoader.Load(this); if (DataContext is { } dataContext)
{
Debug.WriteLine("InferenceTextToImageView destructor");
}
Debug.WriteLine("InferenceTextToImageView destructor");
} }
} }

12
StabilityMatrix.Core/Attributes/ViewAttribute.cs

@ -3,11 +3,17 @@
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public class ViewAttribute : Attribute public class ViewAttribute : Attribute
{ {
public Type ViewType { get; init; }
public bool IsPersistent { get; init; }
public ViewAttribute(Type type) public ViewAttribute(Type type)
{ {
this.type = type; ViewType = type;
} }
private readonly Type type; public ViewAttribute(Type type, bool persistent)
public Type GetViewType() => type; {
ViewType = type;
IsPersistent = persistent;
}
} }

Loading…
Cancel
Save