Skip to content

Commit

Permalink
Fleshed out AvalonDock and pane system. Layout loads and saves automa…
Browse files Browse the repository at this point in the history
…tically. Added Project Explorer. Name of project (or Horizon if no project is open) now displays at top of workspace window.

#22
#23
#24
  • Loading branch information
TheHeadmaster committed Jul 30, 2024
1 parent cfcddd2 commit 297b6b0
Show file tree
Hide file tree
Showing 14 changed files with 519 additions and 10 deletions.
23 changes: 23 additions & 0 deletions Horizon/View/Panes/PanesStyleSelector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Horizon.ViewModel.Panes;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;

namespace Horizon.View.Panes;

public sealed class PanesStyleSelector : StyleSelector
{
public Style? ToolStyle { get; set; }
public Style? PageStyle { get; set; }

public override Style SelectStyle(object item, DependencyObject container) => item switch
{
ToolViewModel => this.ToolStyle ?? base.SelectStyle(item, container),
PageViewModel => this.PageStyle ?? base.SelectStyle(item, container),
_ => base.SelectStyle(item, container)
};
}
17 changes: 17 additions & 0 deletions Horizon/View/Panes/PanesTemplateSelector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Horizon.ViewModel.Panes;
using System.Windows;
using System.Windows.Controls;

namespace Horizon.View.Panes;

public sealed class PanesTemplateSelector : DataTemplateSelector
{
public DataTemplate? ProjectExplorerViewTemplate { get; set; }

public override DataTemplate? SelectTemplate(object item, DependencyObject container) =>
item switch
{
ProjectExplorerViewModel => this.ProjectExplorerViewTemplate,
_ => base.SelectTemplate(item, container)
};
}
13 changes: 13 additions & 0 deletions Horizon/View/Panes/ProjectExplorerPane.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<rui:ReactiveUserControl x:Class="Horizon.View.Panes.ProjectExplorerPane" x:TypeArguments="panes:ProjectExplorerViewModel"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Horizon.View.Panes"
xmlns:rui="http://reactiveui.net"
xmlns:panes="clr-namespace:Horizon.ViewModel.Panes"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
</Grid>
</rui:ReactiveUserControl>
21 changes: 21 additions & 0 deletions Horizon/View/Panes/ProjectExplorerPane.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Horizon.ViewModel.Panes;
using ReactiveUI;

namespace Horizon.View.Panes;

/// <summary>
/// Interaction logic for ProjectExplorerPane.xaml
/// </summary>
public partial class ProjectExplorerPane
{
public ProjectExplorerPane()
{
this.InitializeComponent();

this.ViewModel = new ProjectExplorerViewModel();

this.WhenActivated(dispose =>
{
});
}
}
2 changes: 1 addition & 1 deletion Horizon/View/Windows/NewProjectWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public NewProjectWindow()

this.ViewModel = new()
{
AvailableTemplates = App.AvailableTemplates
AvailableTemplates = App.ViewModel.AvailableTemplates
};

this.WhenActivated(dispose =>
Expand Down
89 changes: 83 additions & 6 deletions Horizon/View/Windows/Workspace.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,92 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:panes="clr-namespace:Horizon.View.Panes"
xmlns:windows="clr-namespace:Horizon.View.Windows"
xmlns:vm="clr-namespace:Horizon.ViewModel"
xmlns:xcad="https://github.com/Dirkster99/AvalonDock"
d:DataContext="{d:DesignInstance Type=vm:WorkspaceViewModel}"
mc:Ignorable="d"
Width="1366" Height="768" WindowStartupLocation="CenterScreen" WindowState="Maximized" Background="{StaticResource BaseBackgroundColor}" FontFamily="Cascadia Mono" Style="{StaticResource BorderlessWindowStyle}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="24" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
</Grid>
<xcad:DockingManager x:Name="Dock" BorderBrush="{DynamicResource BaseHighlightColor}" BorderThickness="1" AllowMixedOrientation="True">
<xcad:DockingManager.DocumentHeaderTemplate>
<DataTemplate DataType="vm:PageViewModel">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Title}" TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
<TextBlock Text="*" VerticalAlignment="Center">
<TextBlock.Visibility>
<PriorityBinding FallbackValue="Collapsed">
<Binding Path="IsDirty" Mode="OneWay" Converter="{StaticResource DefaultBoolToVisibilityConverter}" />
</PriorityBinding>
</TextBlock.Visibility>
</TextBlock>
<Image Source="/Resources/Images/Read Only.png"
Margin="3,0,0,0"
VerticalAlignment="Center">
<Image.Visibility>
<PriorityBinding FallbackValue="Collapsed">
<Binding Path="IsReadOnly" Mode="OneWay" Converter="{StaticResource DefaultBoolToVisibilityConverter}" />
</PriorityBinding>
</Image.Visibility>
<Image.ToolTip>
<PriorityBinding FallbackValue="">
<Binding Path="ReadOnlyReason" Mode="OneWay" />
</PriorityBinding>
</Image.ToolTip>
</Image>
</StackPanel>
</DataTemplate>
</xcad:DockingManager.DocumentHeaderTemplate>
<xcad:DockingManager.AnchorableTitleTemplate>
<DataTemplate DataType="vm:ToolViewModel">
<StackPanel Orientation="Horizontal">
<Image Margin="-4,0,0,0" Source="{Binding IconSource}" Width="20" Height="20" />
<TextBlock Margin="4,0,0,0" Text="{Binding Title}" VerticalAlignment="Center" />
</StackPanel>
</DataTemplate>
</xcad:DockingManager.AnchorableTitleTemplate>
<xcad:DockingManager.AnchorableHeaderTemplate>
<DataTemplate DataType="vm:ToolViewModel">
<StackPanel Orientation="Horizontal">
<Image Margin="-4,0,0,0" Source="{Binding IconSource}" Width="20" Height="20" />
<TextBlock Margin="4,0,0,0" Text="{Binding Title}" VerticalAlignment="Center" />
</StackPanel>
</DataTemplate>
</xcad:DockingManager.AnchorableHeaderTemplate>
<xcad:DockingManager.LayoutItemTemplateSelector>
<panes:PanesTemplateSelector>
<panes:PanesTemplateSelector.ProjectExplorerViewTemplate>
<DataTemplate>
<panes:ProjectExplorerPane />
</DataTemplate>
</panes:PanesTemplateSelector.ProjectExplorerViewTemplate>
</panes:PanesTemplateSelector>
</xcad:DockingManager.LayoutItemTemplateSelector>
<xcad:DockingManager.LayoutItemContainerStyleSelector>
<panes:PanesStyleSelector>
<panes:PanesStyleSelector.ToolStyle>
<Style TargetType="{x:Type LayoutAnchorableItem}">
<Setter Property="Title" Value="{Binding Model.Title}" />
<Setter Property="IconSource" Value="{Binding Model.IconSource}" />
<Setter Property="Visibility" Value="{Binding Model.IsVisible, Mode=TwoWay, Converter={StaticResource DefaultBoolToVisibilityConverter}, ConverterParameter={x:Static Visibility.Hidden}}" />
<Setter Property="ContentId" Value="{Binding Model.ContentID}" />
<Setter Property="IsSelected" Value="{Binding Model.IsSelected, Mode=TwoWay}" />
<Setter Property="IsActive" Value="{Binding Model.IsActive, Mode=TwoWay}" />
</Style>
</panes:PanesStyleSelector.ToolStyle>
<panes:PanesStyleSelector.PageStyle>
<Style TargetType="{x:Type LayoutItem}">
<Setter Property="Title" Value="{Binding Model.Title}" />
<Setter Property="ToolTip" Value="{Binding Model.Title}" />
<Setter Property="IconSource" Value="{Binding Model.IconSource}" />
<Setter Property="ContentId" Value="{Binding Model.ContentID}" />
</Style>
</panes:PanesStyleSelector.PageStyle>
</panes:PanesStyleSelector>
</xcad:DockingManager.LayoutItemContainerStyleSelector>
<xcad:DockingManager.LayoutUpdateStrategy>
<vm:LayoutInitializer />
</xcad:DockingManager.LayoutUpdateStrategy>
<xcad:LayoutRoot x:Name="_layoutRoot" />
</xcad:DockingManager>
</windows:BorderlessReactiveWindow>
81 changes: 79 additions & 2 deletions Horizon/View/Windows/Workspace.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
using ReactiveUI;
using AvalonDock;
using Horizon.Converters;
using ReactiveMarbles.ObservableEvents;
using ReactiveUI;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using EventExtensions = System.Windows.Controls.EventExtensions;

namespace Horizon.View.Windows;

Expand All @@ -18,10 +26,79 @@ public Workspace()

this.WhenActivated(dispose =>
{
this.PropertyBindings(dispose);
this.EventBindings(dispose);
this.KeyBindings(dispose);
this.Interactions(dispose);
});
}

/// <summary>
/// Binds view model properties to the view.
/// </summary>
/// <param name="dispose">The disposable.</param>
private void PropertyBindings(CompositeDisposable dispose)
{
this.OneWayBind(this.ViewModel, vm => vm.Title, view => view.Title)
.DisposeWith(dispose);

this.OneWayBind(this.ViewModel, vm => vm.Tools, view => view.Dock.AnchorablesSource)
.DisposeWith(dispose);

this.OneWayBind(this.ViewModel, vm => vm.Pages, view => view.Dock.DocumentsSource)
.DisposeWith(dispose);

this.Bind(this.ViewModel, vm => vm.ActiveDocument, view => view.Dock.ActiveContent,
vmToViewConverterOverride: new ActiveDocumentConverter(),
viewToVMConverterOverride: new ActiveDocumentConverter())
.DisposeWith(dispose);
}

/// <summary>
/// Binds view model commands to view events.
/// </summary>
/// <param name="dispose">The disposable.</param>
private void EventBindings(CompositeDisposable dispose)
{
this.Dock.Events().DocumentClosing
.Select(x => x)
.InvokeCommand(this, x => x.ViewModel!.DocumentClosing)
.DisposeWith(dispose);

this.Events().Closing
.Select(x => this.Dock)
.InvokeCommand(this, x => x.ViewModel!.SaveLayout)
.DisposeWith(dispose);

EventExtensions.Events(this.Dock).Loaded
.Select(x => x.Source as DockingManager)
.InvokeCommand(this, x => x.ViewModel!.LoadLayout)
.DisposeWith(dispose);

EventExtensions.Events(this.Dock).Unloaded
.Select(x => x.Source as DockingManager)
.InvokeCommand(this, x => x.ViewModel!.SaveLayout)
.DisposeWith(dispose);
}

/// <summary>
/// Binds view model commands to key bindings.
/// </summary>
/// <param name="dispose">The disposable.</param>
private void KeyBindings(CompositeDisposable dispose)
{
this.Events().KeyUp
.Where(x => x.Key == Key.N && Keyboard.Modifiers.HasFlag(ModifierKeys.Control | ModifierKeys.Shift))
.Select(x => new Unit())
.InvokeCommand(App.CommandsViewModel, x => x.NewProjectDialog)
.DisposeWith(dispose);

this.Events().KeyUp
.Where(x => x.Key == Key.F4 && Keyboard.Modifiers.HasFlag(ModifierKeys.Alt))
.Subscribe(_ => this.Close())
.DisposeWith(dispose);
}

/// <summary>
/// Registers interaction handlers.
/// </summary>
Expand All @@ -46,6 +123,6 @@ private void Interactions(CompositeDisposable dispose)
}
}, RxApp.MainThreadScheduler);
})
.DisposeWith(dispose);
.DisposeWith(dispose);
}
}
15 changes: 15 additions & 0 deletions Horizon/ViewModel/AppViewModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Horizon.ObjectModel;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using System.Collections.ObjectModel;

namespace Horizon.ViewModel;

public sealed class AppViewModel : ReactiveObject
{
[Reactive]
public ObservableCollection<ProjectTemplate> AvailableTemplates { get; set; } = [new() { Name = "Starbound Mod Project", Description = "A mod project for the game Starbound", Tags = ["Starbound", "Mod"] }];

[Reactive]
public ProjectFile? CurrentProject { get; set; }
}
39 changes: 39 additions & 0 deletions Horizon/ViewModel/LayoutInitializer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using AvalonDock.Layout;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Horizon.ViewModel;

public sealed class LayoutInitializer : ILayoutUpdateStrategy
{
public bool BeforeInsertAnchorable(LayoutRoot layout, LayoutAnchorable anchorableToShow, ILayoutContainer destinationContainer)
{
// AvalonDock wants to add the anchorable into destinationContainer just for test provide a new anchorable pane if the
// pane is floating let the manager go ahead
LayoutAnchorablePane? destPane = destinationContainer as LayoutAnchorablePane;
if (destinationContainer?.FindParent<LayoutFloatingWindow>() is not null)
{
return false;
}

LayoutAnchorablePane? toolsPane = layout.Descendents().OfType<LayoutAnchorablePane>().FirstOrDefault(d => d.Name == "ToolsPane");
if (toolsPane is not null)
{
toolsPane.Children.Add(anchorableToShow);
return true;
}

return false;
}

public void AfterInsertAnchorable(LayoutRoot layout, LayoutAnchorable anchorableShown)
{ }

public bool BeforeInsertDocument(LayoutRoot layout, LayoutDocument anchorableToShow, ILayoutContainer destinationContainer) => false;

public void AfterInsertDocument(LayoutRoot layout, LayoutDocument anchorableShown)
{ }
}
32 changes: 32 additions & 0 deletions Horizon/ViewModel/Panes/PageViewModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Horizon.ViewModel.Panes;

public abstract class PageViewModel : PaneViewModel
{
public PageViewModel()
{
this.WhenAnyValue(x => x.ID)
.Select(id => $"{this.GetType().Name}|{id}")
.ToPropertyEx(this, x => x.ContentID);
}

[Reactive]
public int ID { get; set; }

[Reactive]
public bool IsDirty { get; set; }

[Reactive]
public bool IsReadOnly { get; set; }

[Reactive]
public string? ReadOnlyReason { get; set; }
}
Loading

0 comments on commit 297b6b0

Please sign in to comment.