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. 69
      StabilityMatrix.Avalonia/ViewLocator.cs
  8. 28
      StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTabViewModelBase.cs
  9. 29
      StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs
  10. 28
      StabilityMatrix.Avalonia/ViewModels/Inference/ModelCardViewModel.cs
  11. 125
      StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs
  12. 50
      StabilityMatrix.Avalonia/Views/InferencePage.axaml
  13. 42
      StabilityMatrix.Avalonia/Views/InferencePage.axaml.cs
  14. 6
      StabilityMatrix.Avalonia/Views/InferenceTextToImageView.axaml
  15. 14
      StabilityMatrix.Avalonia/Views/InferenceTextToImageView.axaml.cs
  16. 14
      StabilityMatrix.Core/Attributes/ViewAttribute.cs

18
StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs

@ -3,18 +3,15 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Avalonia;
using Avalonia.Input;
using Avalonia.Threading;
using Avalonia.Xaml.Interactivity;
using AvaloniaEdit;
using AvaloniaEdit.Document;
using AvaloniaEdit.Editing;
using NLog;
using StabilityMatrix.Avalonia.Controls.CodeCompletion;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Models.TagCompletion;
using StabilityMatrix.Core.Extensions;
using TextMateSharp.Grammars;
using CompletionWindow = StabilityMatrix.Avalonia.Controls.CodeCompletion.CompletionWindow;
namespace StabilityMatrix.Avalonia.Behaviors;
@ -27,19 +24,20 @@ public class TextEditorCompletionBehavior : Behavior<TextEditor>
private CompletionWindow? completionWindow;
public static readonly StyledProperty<ICompletionProvider> CompletionProviderProperty =
AvaloniaProperty.Register<TextEditorCompletionBehavior, ICompletionProvider>(nameof(CompletionProvider));
public static readonly StyledProperty<ICompletionProvider?> CompletionProviderProperty =
AvaloniaProperty.Register<TextEditorCompletionBehavior, ICompletionProvider?>(nameof(CompletionProvider));
public ICompletionProvider CompletionProvider
public ICompletionProvider? CompletionProvider
{
get => GetValue(CompletionProviderProperty);
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");
public ITokenizerProvider TokenizerProvider
public ITokenizerProvider? TokenizerProvider
{
get => GetValue(TokenizerProviderProperty);
set => SetValue(TokenizerProviderProperty, value);
@ -78,7 +76,7 @@ public class TextEditorCompletionBehavior : Behavior<TextEditor>
private CompletionWindow CreateCompletionWindow(TextArea textArea)
{
var window = new CompletionWindow(textArea, CompletionProvider, TokenizerProvider)
var window = new CompletionWindow(textArea, CompletionProvider!, TokenizerProvider!)
{
WindowManagerAddShadowHint = false,
CloseWhenCaretAtBeginning = true,
@ -189,7 +187,7 @@ public class TextEditorCompletionBehavior : Behavior<TextEditor>
var caretAbsoluteOffset = caret - line.Offset;
// Tokenize
var result = TokenizerProvider.TokenizeLine(lineText);
var result = TokenizerProvider!.TokenizeLine(lineText);
var currentTokenIndex = -1;
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"
x:DataType="inference:ModelCardViewModel">
<Design.PreviewWith>
<controls:ModelCard DataContext="{x:Static mocks:DesignData.ModelCardViewModel}"/>
<StackPanel MinWidth="300">
<controls:ModelCard DataContext="{x:Static mocks:DesignData.ModelCardViewModel}"/>
</StackPanel>
</Design.PreviewWith>
<Style Selector="controls|ModelCard">
@ -14,20 +16,46 @@
<Setter Property="Template">
<ControlTemplate>
<controls:Card Padding="8">
<Grid Margin="8" ColumnDefinitions="Auto,*,Auto">
<Grid Margin="8" RowDefinitions="*,*" ColumnDefinitions="Auto,*,Auto">
<!-- Model -->
<TextBlock
Grid.Column="0"
Margin="0,0,8,0"
VerticalAlignment="Center"
HorizontalAlignment="Left"
MinWidth="60"
Text="Model" />
<ui:FAComboBox
Grid.Row="0"
Grid.Column="1"
HorizontalAlignment="Stretch"
ItemsSource="{Binding ClientManager.ModelNames}"
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>
<!-- 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>
</controls:Card>
</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}");
}
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}");
}
@ -214,7 +214,7 @@ public class ServiceManager<T>
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}");
}

69
StabilityMatrix.Avalonia/ViewLocator.cs

@ -1,15 +1,23 @@
using System;
using System.Diagnostics;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.ViewModels.Base;
using NLog;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia;
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 />
public Control Build(object? data)
{
@ -19,8 +27,8 @@ public class ViewLocator : IDataTemplate, INavigationPageFactory
if (Attribute.GetCustomAttribute(type, typeof(ViewAttribute)) is ViewAttribute viewAttr)
{
var viewType = viewAttr.GetViewType();
return GetView(viewType);
var viewType = viewAttr.ViewType;
return GetView(viewType, data, viewAttr.IsPersistent);
}
return new TextBlock
@ -31,7 +39,6 @@ public class ViewLocator : IDataTemplate, INavigationPageFactory
private Control GetView(Type viewType)
{
// Otherwise get from the service provider
if (App.Services.GetService(viewType) is Control 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 />
public bool Match(object? data)
{
@ -58,9 +108,10 @@ public class ViewLocator : IDataTemplate, INavigationPageFactory
throw new InvalidOperationException("View not found for " + srcType.FullName);
}
var viewType = viewAttr.GetViewType();
var view = GetView(viewType);
// Get new view
var view = GetView(viewAttr.ViewType);
view.DataContext ??= App.Services.GetService(srcType);
return view;
}
@ -73,8 +124,8 @@ public class ViewLocator : IDataTemplate, INavigationPageFactory
throw new InvalidOperationException("View not found for " + target.GetType().FullName);
}
var viewType = viewAttr.GetViewType();
var view = GetView(viewType);
var viewType = viewAttr.ViewType;
var view = GetView(viewType, target, viewAttr.IsPersistent);
view.DataContext ??= target;
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 StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Core.Models.FileInterfaces;
#pragma warning disable CS0657 // Not a valid attribute location for this declaration
namespace StabilityMatrix.Avalonia.ViewModels.Inference;
public abstract partial class InferenceTabViewModelBase : LoadableViewModelBase
public abstract partial class InferenceTabViewModelBase : LoadableViewModelBase, IDisposable, IPersistentViewProvider
{
/// <summary>
/// The title of the tab
/// </summary>
public virtual string TabTitle => ProjectFile?.Name ?? "New Project";
public virtual string TabTitle => ProjectFile?.NameWithoutExtension ?? "New Project";
/// <summary>
/// Whether there are unsaved changes
@ -26,4 +30,22 @@ public abstract partial class InferenceTabViewModelBase : LoadableViewModelBase
[NotifyPropertyChangedFor(nameof(TabTitle))]
[property: JsonIgnore]
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;
[View(typeof(InferenceTextToImageView))]
[View(typeof(InferenceTextToImageView), persistent: true)]
public partial class InferenceTextToImageViewModel : InferenceTabViewModelBase
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
@ -129,7 +129,7 @@ public partial class InferenceTextToImageViewModel : InferenceTabViewModelBase
// GenerateImageCommand.WithNotificationErrorHandler(notificationService);
}
private (NodeDictionary prompt, string[] outputs) BuildPrompt()
private (NodeDictionary prompt, string[] outputs) BuildPrompt(GenerateOverrides? overrides = null)
{
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 (IsHiresFixEnabled)
if (overrides is { IsHiresFixEnabled: true } || IsHiresFixEnabled)
{
var hiresUpscalerCard = UpscalerCardViewModel;
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)
{
@ -357,7 +359,8 @@ public partial class InferenceTextToImageViewModel : InferenceTabViewModelBase
// If enabled, randomize the seed
var seedCard = StackCardViewModel.GetCard<SeedCardViewModel>();
if (seedCard.IsRandomizeEnabled)
if (overrides is not { UseCurrentSeed: true } &&
seedCard.IsRandomizeEnabled)
{
seedCard.GenerateNewSeed();
}
@ -474,15 +477,27 @@ public partial class InferenceTextToImageViewModel : InferenceTabViewModelBase
}
[RelayCommand(IncludeCancelCommand = true)]
private async Task GenerateImage(CancellationToken cancellationToken = default)
private async Task GenerateImage(string? options = null, CancellationToken cancellationToken = default)
{
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)
{
Logger.Debug($"[Image Generation Canceled] {e.Message}");
}
}
internal class GenerateOverrides
{
public bool? IsHiresFixEnabled { get; set; }
public bool? UseCurrentSeed { get; set; }
}
}

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

125
StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs

@ -1,8 +1,7 @@
using System;
using System.Collections.ObjectModel;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Avalonia.Collections;
using Avalonia.Controls.Notifications;
using Avalonia.Platform.Storage;
using CommunityToolkit.Mvvm.ComponentModel;
@ -52,10 +51,13 @@ public partial class InferenceViewModel : PageViewModelBase
public IInferenceClientManager ClientManager { get; }
public AvaloniaList<InferenceTabViewModelBase> Tabs { get; } = new();
public ObservableCollection<InferenceTabViewModelBase> Tabs { get; } = new();
[ObservableProperty]
private LoadableViewModelBase? selectedTab;
private InferenceTabViewModelBase? selectedTab;
[ObservableProperty]
private int selectedTabIndex;
[ObservableProperty]
private PackagePair? runningPackage;
@ -64,7 +66,6 @@ public partial class InferenceViewModel : PageViewModelBase
ServiceManager<ViewModelBase> vmFactory,
IApiFactory apiFactory,
INotificationService notificationService,
// IRelayCommandFactory commandFactory,
IInferenceClientManager inferenceClientManager,
ISettingsManager settingsManager
)
@ -72,7 +73,6 @@ public partial class InferenceViewModel : PageViewModelBase
this.vmFactory = vmFactory;
this.apiFactory = apiFactory;
this.notificationService = notificationService;
// this.commandFactory = commandFactory;
this.settingsManager = settingsManager;
ClientManager = inferenceClientManager;
@ -88,18 +88,12 @@ public partial class InferenceViewModel : PageViewModelBase
public override void OnLoaded()
{
base.OnLoaded();
if (Tabs.Count == 0)
{
AddTab();
}
// Select first tab if none is selected
if (SelectedTab is null && Tabs.Count > 0)
{
SelectedTab = Tabs[0];
}
base.OnLoaded();
}
/// <summary>
@ -109,6 +103,9 @@ public partial class InferenceViewModel : PageViewModelBase
private void AddTab()
{
Tabs.Add(vmFactory.Get<InferenceTextToImageViewModel>());
// Set as new selected tab
SelectedTabIndex = Tabs.Count - 1;
}
/// <summary>
@ -123,7 +120,26 @@ public partial class InferenceViewModel : PageViewModelBase
}
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>
@ -233,15 +249,66 @@ public partial class InferenceViewModel : PageViewModelBase
return;
}
// Update project file
currentTab.ProjectFile = new FilePath(result.TryGetLocalPath()!);
notificationService.Show(
"Saved",
$"Saved project to {result.Name}",
NotificationType.Success
);
}
/*public AsyncRelayCommand MenuOpenProjectCommand =>
commandFactory.CreateWithNotificationErrorHandling(MenuOpenProject);*/
/// <summary>
/// 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>
/// Menu "Open Project" command.
@ -285,22 +352,28 @@ public partial class InferenceViewModel : PageViewModelBase
var document = await JsonSerializer.DeserializeAsync<InferenceProjectDocument>(stream);
if (document is null)
{
Logger.Warn("MenuOpenProject: Deserialize project file returned null");
return;
throw new ApplicationException("MenuOpenProject: Deserialize project file returned null");
}
if (document.State is null)
{
throw new ApplicationException("MenuOpenProject: Deserialize project file returned null state");
}
InferenceTabViewModelBase? vm = null;
if (document.ProjectType is InferenceProjectType.TextToImage && document.State is not null)
InferenceTabViewModelBase vm;
if (document.ProjectType is InferenceProjectType.TextToImage)
{
// Get view model
var textToImage = vmFactory.Get<InferenceTextToImageViewModel>();
// Load state
textToImage.LoadStateFromJsonObject(document.State);
// Set the file backing the view model
textToImage.ProjectFile = new FilePath(file.TryGetLocalPath()!);
vm = textToImage;
}
if (vm == null)
else
{
Logger.Warn("MenuOpenProject: Unknown project type");
return;
throw new InvalidOperationException($"Unsupported project type: {document.ProjectType}");
}
Tabs.Add(vm);

50
StabilityMatrix.Avalonia/Views/InferencePage.axaml

@ -2,13 +2,10 @@
x:Class="StabilityMatrix.Avalonia.Views.InferencePage"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:avaloniaEdit="clr-namespace:AvaloniaEdit;assembly=AvaloniaEdit"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
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:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:system="clr-namespace:System;assembly=System.Runtime"
xmlns:vmInference="using:StabilityMatrix.Avalonia.ViewModels.Inference"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels"
@ -17,23 +14,59 @@
d:DesignWidth="1000"
x:DataType="vm:InferenceViewModel"
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>
<!-- AddTabButtonCommand="{Binding AddTabCommand}" -->
<ui:TabView
x:Name="TabView"
CanDragTabs="True"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
CanReorderTabs="True"
CloseButtonOverlayMode="Auto"
TabCloseRequested="TabView_OnTabCloseRequested"
AddTabButtonCommand="{Binding AddTabCommand}"
AddTabButtonClick="TabView_OnAddTabButtonClick"
TabItems="{Binding Tabs}"
SelectedItem="{Binding SelectedTab, Mode=TwoWay}"
SelectedItem="{Binding SelectedTab}"
SelectedIndex="{Binding SelectedTabIndex}"
TabWidthMode="SizeToContent">
<ui:TabView.TabItemTemplate>
<DataTemplate DataType="{x:Type vmInference:InferenceTextToImageViewModel}">
<ui:TabViewItem Header="Project 1"
<ui:TabViewItem Header="{Binding TabTitle}"
IconSource="Document"
Content="{Binding}">
</ui:TabViewItem>
Content="{Binding}" />
</DataTemplate>
</ui:TabView.TabItemTemplate>
<ui:TabView.TabStripFooter>
@ -58,6 +91,7 @@
InputGesture="Ctrl+O"/>
<Separator/>
<MenuItem Header="Save"
Command="{Binding MenuSaveCommand}"
InputGesture="Ctrl+S"/>
<MenuItem Header="Save As..."
Command="{Binding MenuSaveAsCommand}"

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

@ -1,5 +1,9 @@
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using System;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Interactivity;
using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.ViewModels;
@ -8,16 +12,23 @@ namespace StabilityMatrix.Avalonia.Views;
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()
{
InitializeComponent();
AddHandler(DragDrop.DropEvent, DropHandler);
AddHandler(DragDrop.DragOverEvent, DragOverHandler);
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
addTabFlyout = Resources["AddTabFlyout"] as CommandBarFlyout
?? throw new NullReferenceException("AddTabFlyout not found");
}
private void TabView_OnTabCloseRequested(TabView sender, TabViewTabCloseRequestedEventArgs args)
@ -40,4 +51,21 @@ public partial class InferencePage : UserControlBase
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"
Alignment="Right"
Id="PromptPane"
Proportion="0.3">
Proportion="0.25">
<Tool
x:Name="PromptTool"
Title="Prompt"
@ -137,6 +137,7 @@
icons:Attached.Icon="fa-solid fa-magnifying-glass-plus"
Classes="transparent"
Command="{Binding GenerateImageCommand}"
CommandParameter="current_seed,hires_fix"
ToolTip.Tip="Generate with Hires Scaling" />
<!-- Main Generate Button -->
@ -174,6 +175,7 @@
icons:Attached.Icon="fa-solid fa-repeat"
Classes="transparent"
Command="{Binding GenerateImageCommand}"
CommandParameter="current_seed"
ToolTip.Tip="Generate with current seed" />
</Grid>
</controls:Card>
@ -190,7 +192,7 @@
x:Name="ImageGalleryPane"
Alignment="Right"
Id="ImageGalleryPane"
Proportion="0.3">
Proportion="0.35">
<Tool
x:Name="ImageGalleryTool"
Title="Image Output"

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

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

14
StabilityMatrix.Core/Attributes/ViewAttribute.cs

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

Loading…
Cancel
Save