From 8c2a41a13320677fd6946529c2d7666c94b1e273 Mon Sep 17 00:00:00 2001 From: Qrakhen Date: Thu, 27 Nov 2025 23:35:22 +0100 Subject: [PATCH] add tiling frames --- Qrakhen.TilingFrames/AssemblyInfo.cs | 10 + .../DragAndDrop/DragAndDropControl.cs | 98 ++++++++ .../Controls/DropArea/DropArea.cs | 9 + .../DropArea/DropAreaDecisionEvent.cs | 3 + .../Controls/DropArea/DropDecision.cs | 11 + Qrakhen.TilingFrames/Controls/HostControl.cs | 43 ++++ .../Controls/HostFrameControl.cs | 39 ++++ Qrakhen.TilingFrames/Controls/PanelControl.cs | 53 +++++ .../Controls/TilingGridAttachment.cs | 55 +++++ .../Controls/TilingPanelControl.cs | 69 ++++++ Qrakhen.TilingFrames/Controls/TilingRoot.cs | 30 +++ .../Converters/AlphaRatioConverter.cs | 30 +++ .../Converters/BetaRatioConverter.cs | 30 +++ Qrakhen.TilingFrames/Models/EmptyFrame.cs | 17 ++ Qrakhen.TilingFrames/Models/EmptyHost.cs | 8 + Qrakhen.TilingFrames/Models/FrameButtons.cs | 19 ++ Qrakhen.TilingFrames/Models/HostFrame.cs | 44 ++++ .../Models/ObservableObject.cs | 25 ++ .../Models/TilingDirection.cs | 24 ++ Qrakhen.TilingFrames/Models/TilingHost.cs | 74 ++++++ Qrakhen.TilingFrames/Models/TilingMethod.cs | 16 ++ Qrakhen.TilingFrames/Models/TilingNode.cs | 8 + Qrakhen.TilingFrames/Models/TilingPanel.cs | 218 ++++++++++++++++++ .../Qrakhen.TilingFrames.csproj | 10 + Qrakhen.TilingFrames/README.md | 45 ++++ Qrakhen.TilingFrames/Themes/Colors.xaml | 36 +++ .../Themes/Controls/DropArea.xaml | 92 ++++++++ .../Themes/Controls/Host.xaml | 117 ++++++++++ .../Themes/Controls/HostFrame.xaml | 49 ++++ .../Themes/Controls/Panel.xaml | 115 +++++++++ .../Themes/Controls/TilingRoot.xaml | 26 +++ .../Themes/Templates/Generic.xaml | 17 ++ Qrakhen.TilingFrames/Themes/Themes.xaml | 9 + 33 files changed, 1449 insertions(+) create mode 100644 Qrakhen.TilingFrames/AssemblyInfo.cs create mode 100644 Qrakhen.TilingFrames/Controls/DragAndDrop/DragAndDropControl.cs create mode 100644 Qrakhen.TilingFrames/Controls/DropArea/DropArea.cs create mode 100644 Qrakhen.TilingFrames/Controls/DropArea/DropAreaDecisionEvent.cs create mode 100644 Qrakhen.TilingFrames/Controls/DropArea/DropDecision.cs create mode 100644 Qrakhen.TilingFrames/Controls/HostControl.cs create mode 100644 Qrakhen.TilingFrames/Controls/HostFrameControl.cs create mode 100644 Qrakhen.TilingFrames/Controls/PanelControl.cs create mode 100644 Qrakhen.TilingFrames/Controls/TilingGridAttachment.cs create mode 100644 Qrakhen.TilingFrames/Controls/TilingPanelControl.cs create mode 100644 Qrakhen.TilingFrames/Controls/TilingRoot.cs create mode 100644 Qrakhen.TilingFrames/Converters/AlphaRatioConverter.cs create mode 100644 Qrakhen.TilingFrames/Converters/BetaRatioConverter.cs create mode 100644 Qrakhen.TilingFrames/Models/EmptyFrame.cs create mode 100644 Qrakhen.TilingFrames/Models/EmptyHost.cs create mode 100644 Qrakhen.TilingFrames/Models/FrameButtons.cs create mode 100644 Qrakhen.TilingFrames/Models/HostFrame.cs create mode 100644 Qrakhen.TilingFrames/Models/ObservableObject.cs create mode 100644 Qrakhen.TilingFrames/Models/TilingDirection.cs create mode 100644 Qrakhen.TilingFrames/Models/TilingHost.cs create mode 100644 Qrakhen.TilingFrames/Models/TilingMethod.cs create mode 100644 Qrakhen.TilingFrames/Models/TilingNode.cs create mode 100644 Qrakhen.TilingFrames/Models/TilingPanel.cs create mode 100644 Qrakhen.TilingFrames/Qrakhen.TilingFrames.csproj create mode 100644 Qrakhen.TilingFrames/README.md create mode 100644 Qrakhen.TilingFrames/Themes/Colors.xaml create mode 100644 Qrakhen.TilingFrames/Themes/Controls/DropArea.xaml create mode 100644 Qrakhen.TilingFrames/Themes/Controls/Host.xaml create mode 100644 Qrakhen.TilingFrames/Themes/Controls/HostFrame.xaml create mode 100644 Qrakhen.TilingFrames/Themes/Controls/Panel.xaml create mode 100644 Qrakhen.TilingFrames/Themes/Controls/TilingRoot.xaml create mode 100644 Qrakhen.TilingFrames/Themes/Templates/Generic.xaml create mode 100644 Qrakhen.TilingFrames/Themes/Themes.xaml diff --git a/Qrakhen.TilingFrames/AssemblyInfo.cs b/Qrakhen.TilingFrames/AssemblyInfo.cs new file mode 100644 index 0000000..b0ec827 --- /dev/null +++ b/Qrakhen.TilingFrames/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Windows; + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] diff --git a/Qrakhen.TilingFrames/Controls/DragAndDrop/DragAndDropControl.cs b/Qrakhen.TilingFrames/Controls/DragAndDrop/DragAndDropControl.cs new file mode 100644 index 0000000..4f1b3c4 --- /dev/null +++ b/Qrakhen.TilingFrames/Controls/DragAndDrop/DragAndDropControl.cs @@ -0,0 +1,98 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; + +namespace Qrakhen.TilingFrames.Controls.DragAndDrop; + +public abstract partial class DragAndDropControl : Control +{ + public virtual void OnDragStart(object model, UIElement draggedElement) { } + + public virtual void OnDragEnd(object model, UIElement targetElement) { } + + public virtual void OnDrag(object sender, MouseEventArgs args) { } + + #region Dependency Properties + + public bool AllowDrag { + get => (bool)GetValue(AllowDragProperty); + set => SetValue(AllowDragProperty, value); + } + + public static DependencyProperty AllowDragProperty = DependencyProperty + .Register( + nameof(AllowDrag), + typeof(bool), + typeof(DragAndDropControl), + new PropertyMetadata(true)); + + #endregion + + #region Attached Properties + + public static readonly DependencyProperty IsDraggableProperty = DependencyProperty + .RegisterAttached( + "IsDraggable", + typeof(bool), + typeof(DragAndDropControl), + new PropertyMetadata(false, OnIsDraggableChanged)); + + public static bool GetIsDraggable(DependencyObject obj) => (bool)obj.GetValue(IsDraggableProperty); + public static void SetIsDraggable(DependencyObject obj, bool value) => obj.SetValue(IsDraggableProperty, value); + + private static void OnIsDraggableChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) + { + if (sender is UIElement element && (bool)e.NewValue) { + + } + } + + private static void HandleDragStart(object sender, MouseButtonEventArgs args, DragAndDropControl dndControl) + { + if (sender is UIElement element) { + + } + } + + private static void HandleDrop(object sender, MouseButtonEventArgs args, DragAndDropControl dndControl) + { + // moi schaun + } + + private static T? FindVisualParent(DependencyObject child) where T : DependencyObject + { + // Todo: find a better solution to this, if there even is one. + // There are, in fact, many controls even in the WPF standard that do similar lookups, + // so I assume it can't be _that_ bad, but every clean solution kind of seems to need some ugly spot in it. :( + + DependencyObject parent = VisualTreeHelper.GetParent(child); + while (parent != null) { + if (parent is T expected) + return expected; + parent = VisualTreeHelper.GetParent(parent); + } + return null; + } + + public static readonly DependencyProperty HandlerProperty = DependencyProperty + .RegisterAttached( + "Handler", + typeof(DependencyObject), + typeof(DragAndDropControl), + new PropertyMetadata(null, OnHandlerChanged)); + + public static DependencyObject GetHandler(DependencyObject obj) => (DependencyObject)obj.GetValue(HandlerProperty); + public static void SetHandler(DependencyObject obj, bool value) => obj.SetValue(HandlerProperty, value); + + private static void OnHandlerChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) + { + if (sender is UIElement element && e.NewValue is DragAndDropControl dndControl) { + + } + + throw new InvalidOperationException($"Can not set {e.NewValue} as handler. Handler needs to be of type DragAndDropControl."); + } + + #endregion +} diff --git a/Qrakhen.TilingFrames/Controls/DropArea/DropArea.cs b/Qrakhen.TilingFrames/Controls/DropArea/DropArea.cs new file mode 100644 index 0000000..9c72058 --- /dev/null +++ b/Qrakhen.TilingFrames/Controls/DropArea/DropArea.cs @@ -0,0 +1,9 @@ +using CopaData.FileInspector.GUI.TilingPanels.Controls.DragAndDrop; +using System.Windows.Controls; + +namespace CopaData.FileInspector.GUI.TilingPanels.Controls.DropArea; + +public class DropArea : DragAndDropControl +{ +} + diff --git a/Qrakhen.TilingFrames/Controls/DropArea/DropAreaDecisionEvent.cs b/Qrakhen.TilingFrames/Controls/DropArea/DropAreaDecisionEvent.cs new file mode 100644 index 0000000..9c6386b --- /dev/null +++ b/Qrakhen.TilingFrames/Controls/DropArea/DropAreaDecisionEvent.cs @@ -0,0 +1,3 @@ +namespace CopaData.FileInspector.GUI.TilingPanels.Controls.DropArea; + +public delegate void DropAreaDecisionEvent(DropArea sender, HostControl host, DropDecision decision); diff --git a/Qrakhen.TilingFrames/Controls/DropArea/DropDecision.cs b/Qrakhen.TilingFrames/Controls/DropArea/DropDecision.cs new file mode 100644 index 0000000..b367311 --- /dev/null +++ b/Qrakhen.TilingFrames/Controls/DropArea/DropDecision.cs @@ -0,0 +1,11 @@ +namespace Qrakhen.TilingFrames.Controls.DropArea; + +public enum DropDecision +{ + Cancel = 0, + Center, + Left, + Right, + Top, + Bottom +} \ No newline at end of file diff --git a/Qrakhen.TilingFrames/Controls/HostControl.cs b/Qrakhen.TilingFrames/Controls/HostControl.cs new file mode 100644 index 0000000..c3fded0 --- /dev/null +++ b/Qrakhen.TilingFrames/Controls/HostControl.cs @@ -0,0 +1,43 @@ +using System.Windows; +using System.Windows.Controls; +using Qrakhen.TilingFrames.Controls.DragAndDrop; +using Qrakhen.TilingFrames.Models; + +namespace Qrakhen.TilingFrames.Controls; + +/// +/// Control that represents the state of a , +/// with all the necessary interaction logic tied to it. +/// +public class HostControl : DragAndDropControl +{ + public TilingHost? Host => DataContext as TilingHost; + + static HostControl() + { + DefaultStyleKeyProperty.OverrideMetadata( + typeof(HostControl), + new FrameworkPropertyMetadata(typeof(HostControl))); + } + + public HostControl() + { + + } + + #region DependencyProperties + + /*public Host Host + { + get => (Host)GetValue(HostProperty); + set => SetValue(HostProperty, value); + } + + public static DependencyProperty HostProperty = DependencyProperty + .Register(nameof(Host), + typeof(Host), + typeof(HostControl), + new PropertyMetadata(null));*/ + + #endregion +} diff --git a/Qrakhen.TilingFrames/Controls/HostFrameControl.cs b/Qrakhen.TilingFrames/Controls/HostFrameControl.cs new file mode 100644 index 0000000..8f87b09 --- /dev/null +++ b/Qrakhen.TilingFrames/Controls/HostFrameControl.cs @@ -0,0 +1,39 @@ +using System.Windows; +using System.Windows.Controls; +using Qrakhen.TilingFrames.Models; + +namespace Qrakhen.TilingFrames.Controls; + +/// +/// Control that represents the state of a , +/// with all the necessary interaction logic tied to it. +/// +public class HostFrameControl : ContentControl +{ + static HostFrameControl() + { + DefaultStyleKeyProperty.OverrideMetadata( + typeof(HostFrameControl), + new FrameworkPropertyMetadata(typeof(HostFrameControl))); + } + + public HostFrameControl() + { + + } + + #region DependencyProperties + + public HostFrame Frame { + get => (HostFrame)GetValue(FrameProperty); + set => SetValue(FrameProperty, value); + } + + public static DependencyProperty FrameProperty = DependencyProperty + .Register(nameof(Frame), + typeof(HostFrame), + typeof(HostFrameControl), + new PropertyMetadata(null)); + + #endregion +} diff --git a/Qrakhen.TilingFrames/Controls/PanelControl.cs b/Qrakhen.TilingFrames/Controls/PanelControl.cs new file mode 100644 index 0000000..2d34df1 --- /dev/null +++ b/Qrakhen.TilingFrames/Controls/PanelControl.cs @@ -0,0 +1,53 @@ +using Qrakhen.TilingFrames.Models; +using System.Windows; +using System.Windows.Controls; + +namespace Qrakhen.TilingFrames.Controls; + +public class PanelControl : Control +{ + static PanelControl() + { + DefaultStyleKeyProperty.OverrideMetadata( + typeof(PanelControl), + new FrameworkPropertyMetadata(typeof(PanelControl))); + } + + public PanelControl() + { + + } + + #region DependencyProperties + + public Orientation SplitOrientation => Panel.SplitOrientation; + + public TilingNode Alpha => Panel.Alpha; + public TilingNode? Beta => Panel.Beta; + + public Models.TilingPanel Panel { + get => (Models.TilingPanel)GetValue(PanelProperty); + set => SetValue(PanelProperty, value); + } + + public static DependencyProperty PanelProperty = DependencyProperty + .Register(nameof(Panel), + typeof(Models.TilingPanel), + typeof(PanelControl), + new PropertyMetadata(null)); + + public double SplitterWidth { + get => (double)GetValue(SplitterWidthProperty); + set => SetValue(SplitterWidthProperty, value); + } + + public static DependencyProperty SplitterWidthProperty = DependencyProperty + .Register( + nameof(SplitterWidth), + typeof(double), + typeof(PanelControl), + new PropertyMetadata(8.0)); + + #endregion +} + diff --git a/Qrakhen.TilingFrames/Controls/TilingGridAttachment.cs b/Qrakhen.TilingFrames/Controls/TilingGridAttachment.cs new file mode 100644 index 0000000..82c8131 --- /dev/null +++ b/Qrakhen.TilingFrames/Controls/TilingGridAttachment.cs @@ -0,0 +1,55 @@ +using System.Windows; +using System.Windows.Controls; + +namespace Qrakhen.TilingFrames.Controls +{ + /// + /// Used to force-update columns into rows when orientation is vertical + /// + public static class TilingGridAttachment + { + #region AlphaRow + public static readonly DependencyProperty AlphaRowProperty = + DependencyProperty.RegisterAttached( + "AlphaRow", + typeof(int), + typeof(TilingGridAttachment), + new PropertyMetadata(0, OnAlphaRowChanged)); + + public static int GetAlphaRow(DependencyObject obj) + => (int)obj.GetValue(AlphaRowProperty); + public static void SetAlphaRow(DependencyObject obj, int value) + => obj.SetValue(AlphaRowProperty, value); + + private static void OnAlphaRowChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is ContentPresenter presenter) { + Grid.SetRow(presenter, (int)e.NewValue); + Grid.SetColumn(presenter, 0); // Always Column 0 for vertical split + } + } + #endregion + + #region BetaRow + public static readonly DependencyProperty BetaRowProperty = + DependencyProperty.RegisterAttached( + "BetaRow", + typeof(int), + typeof(TilingGridAttachment), + new PropertyMetadata(0, OnBetaRowChanged)); + + public static int GetBetaRow(DependencyObject obj) + => (int)obj.GetValue(BetaRowProperty); + public static void SetBetaRow(DependencyObject obj, int value) + => obj.SetValue(BetaRowProperty, value); + + private static void OnBetaRowChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is ContentPresenter presenter) { + Grid.SetRow(presenter, (int)e.NewValue); + Grid.SetColumn(presenter, 0); // Always Column 0 for vertical split + } + } + #endregion + } +} diff --git a/Qrakhen.TilingFrames/Controls/TilingPanelControl.cs b/Qrakhen.TilingFrames/Controls/TilingPanelControl.cs new file mode 100644 index 0000000..ccd2d69 --- /dev/null +++ b/Qrakhen.TilingFrames/Controls/TilingPanelControl.cs @@ -0,0 +1,69 @@ +using Qrakhen.TilingFrames.Models; +using System.Windows; +using System.Windows.Controls; + +namespace Qrakhen.TilingFrames.Controls; + +/// +/// For Pop-Outs etc. +/// +public class TilingWindowControl : Window +{ + static TilingWindowControl() + { + DefaultStyleKeyProperty.OverrideMetadata( + typeof(TilingWindowControl), + new FrameworkPropertyMetadata(typeof(TilingWindowControl))); + } + + public TilingPanel Root { + get => (TilingPanel)GetValue(RootProperty); + set => SetValue(RootProperty, value); + } + + public static DependencyProperty RootProperty = DependencyProperty + .Register( + nameof(Root), + typeof(TilingPanel), + typeof(TilingWindowControl)); +} + +public class TilingRootControl : TilingPanelControl +{ + static TilingRootControl() + { + DefaultStyleKeyProperty.OverrideMetadata( + typeof(TilingRootControl), + new FrameworkPropertyMetadata(typeof(TilingRootControl))); + } +} + +public class TilingPanelControl : Control +{ + static TilingPanelControl() + { + DefaultStyleKeyProperty.OverrideMetadata( + typeof(TilingPanelControl), + new FrameworkPropertyMetadata(typeof(TilingPanelControl))); + } +} + +public class TilingHostControl : Control +{ + static TilingHostControl() + { + DefaultStyleKeyProperty.OverrideMetadata( + typeof(TilingHostControl), + new FrameworkPropertyMetadata(typeof(TilingHostControl))); + } +} + +public class TilingFrameControl : ContentControl +{ + static TilingFrameControl() + { + DefaultStyleKeyProperty.OverrideMetadata( + typeof(TilingFrameControl), + new FrameworkPropertyMetadata(typeof(TilingFrameControl))); + } +} \ No newline at end of file diff --git a/Qrakhen.TilingFrames/Controls/TilingRoot.cs b/Qrakhen.TilingFrames/Controls/TilingRoot.cs new file mode 100644 index 0000000..4024e27 --- /dev/null +++ b/Qrakhen.TilingFrames/Controls/TilingRoot.cs @@ -0,0 +1,30 @@ +using System.Windows; +using System.Windows.Controls; + +namespace Qrakhen.TilingFrames.Controls; + +public class TilingRoot : Control +{ + static TilingRoot() + { + DefaultStyleKeyProperty.OverrideMetadata( + typeof(TilingRoot), + new FrameworkPropertyMetadata(typeof(TilingRoot))); + } + + #region Dependency Properties + + public double SplitterWidth { + get => (double)GetValue(SplitterWidthProperty); + set => SetValue(SplitterWidthProperty, value); + } + + public static DependencyProperty SplitterWidthProperty = DependencyProperty + .Register( + nameof(SplitterWidth), + typeof(double), + typeof(TilingRoot), + new PropertyMetadata(8.0)); + + #endregion +} diff --git a/Qrakhen.TilingFrames/Converters/AlphaRatioConverter.cs b/Qrakhen.TilingFrames/Converters/AlphaRatioConverter.cs new file mode 100644 index 0000000..ad23da8 --- /dev/null +++ b/Qrakhen.TilingFrames/Converters/AlphaRatioConverter.cs @@ -0,0 +1,30 @@ +using System.Globalization; +using System.Windows; +using System.Windows.Data; +using System.Windows.Markup; + +namespace Qrakhen.TilingFrames.Converters +{ + public class AlphaRatioConverter : MarkupExtension, IValueConverter + { + public override object ProvideValue(IServiceProvider serviceProvider) => this; + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is double ratio) { + if (ratio <= double.Epsilon) + return new GridLength(0, GridUnitType.Star); + if (ratio <= .5) + return new GridLength(1, GridUnitType.Star); + else if (ratio >= 1) + return new GridLength(1, GridUnitType.Star); + return new GridLength((1 - ratio) / ratio, GridUnitType.Star); + } + + throw new ArgumentException($"Expected value to be ratio (double), god {value} instead."); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + => throw new NotImplementedException(); + } +} diff --git a/Qrakhen.TilingFrames/Converters/BetaRatioConverter.cs b/Qrakhen.TilingFrames/Converters/BetaRatioConverter.cs new file mode 100644 index 0000000..2e92fe1 --- /dev/null +++ b/Qrakhen.TilingFrames/Converters/BetaRatioConverter.cs @@ -0,0 +1,30 @@ +using System.Globalization; +using System.Windows; +using System.Windows.Data; +using System.Windows.Markup; + +namespace Qrakhen.TilingFrames.Converters +{ + public class BetaRatioConverter : MarkupExtension, IValueConverter + { + public override object ProvideValue(IServiceProvider serviceProvider) => this; + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is double ratio) { + if (ratio >= 1) + return new GridLength(0, GridUnitType.Star); + if (ratio >= .5) + return new GridLength(1, GridUnitType.Star); + else if (ratio <= double.Epsilon) + return new GridLength(1, GridUnitType.Star); + return new GridLength((1 - ratio) / ratio, GridUnitType.Star); + } + + throw new ArgumentException($"Expected value to be ratio (double), god {value} instead."); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + => throw new NotImplementedException(); + } +} diff --git a/Qrakhen.TilingFrames/Models/EmptyFrame.cs b/Qrakhen.TilingFrames/Models/EmptyFrame.cs new file mode 100644 index 0000000..60e0bae --- /dev/null +++ b/Qrakhen.TilingFrames/Models/EmptyFrame.cs @@ -0,0 +1,17 @@ +namespace Qrakhen.TilingFrames.Models; + +public class EmptyFrame : HostFrame +{ + public EmptyFrame() + { + HeaderText = "Empty"; + } +} + +public class TestOverrideFrame : HostFrame +{ + public TestOverrideFrame() + { + HeaderText = "Testerinho"; + } +} diff --git a/Qrakhen.TilingFrames/Models/EmptyHost.cs b/Qrakhen.TilingFrames/Models/EmptyHost.cs new file mode 100644 index 0000000..5f97b49 --- /dev/null +++ b/Qrakhen.TilingFrames/Models/EmptyHost.cs @@ -0,0 +1,8 @@ +namespace Qrakhen.TilingFrames.Models; + +public class EmptyHost : TilingHost +{ + public EmptyHost() : base(new EmptyFrame(), new TestOverrideFrame()) + { + } +} diff --git a/Qrakhen.TilingFrames/Models/FrameButtons.cs b/Qrakhen.TilingFrames/Models/FrameButtons.cs new file mode 100644 index 0000000..4c4fa6e --- /dev/null +++ b/Qrakhen.TilingFrames/Models/FrameButtons.cs @@ -0,0 +1,19 @@ +namespace Qrakhen.TilingFrames.Models; + +/// +/// Flags for declaring the buttons on a +/// +[Flags] +public enum FrameButtons +{ + None = 0, + Close = 1 << 0, + Minimize = 1 << 1, + Pin = 1 << 2, + Split = 1 << 3, + PopOut = 1 << 4, + Options = 1 << 5, + Help = 1 << 6, + + Default = Close | Pin | PopOut +} diff --git a/Qrakhen.TilingFrames/Models/HostFrame.cs b/Qrakhen.TilingFrames/Models/HostFrame.cs new file mode 100644 index 0000000..a79e742 --- /dev/null +++ b/Qrakhen.TilingFrames/Models/HostFrame.cs @@ -0,0 +1,44 @@ +namespace Qrakhen.TilingFrames.Models; + +/// +/// Base data class for content that is displayed for a 's tabs or stand-alone pop-out windows.
+/// Anything that you can physically see from the root is a .
+/// Serving a header text, pin state and button configuration properties. +///
+public abstract class HostFrame : ObservableObject +{ + /// + private string _headerText = string.Empty; + /// + /// The title of the tab that will be displayed inside the tab button. + /// + public string HeaderText { + get => _headerText; + set => SetField(ref _headerText, value); + } + + /// + private FrameButtons _frameButtons = FrameButtons.Default; + /// + /// The buttons to be displayed next to the header. + /// + public FrameButtons FrameButtons { + get => _frameButtons; + set => SetField(ref _frameButtons, value); + } + + /// + private bool _isPinned = false; + /// + /// The title of the tab that will be displayed inside the tab button. + /// + public bool IsPinned { + get => _isPinned; + set => SetField(ref _isPinned, value); + } + + public virtual object? GetOptions() => null; + public virtual void SetOptions(object? options) { } + + public virtual object? GetHelp() => null; +} diff --git a/Qrakhen.TilingFrames/Models/ObservableObject.cs b/Qrakhen.TilingFrames/Models/ObservableObject.cs new file mode 100644 index 0000000..f26bbaa --- /dev/null +++ b/Qrakhen.TilingFrames/Models/ObservableObject.cs @@ -0,0 +1,25 @@ +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace Qrakhen.TilingFrames.Models; + +public abstract class ObservableObject : INotifyPropertyChanged +{ + public event PropertyChangedEventHandler? PropertyChanged; + + protected bool SetField([NotNullIfNotNull("newValue")] ref T field, T newValue, [CallerMemberName] string? propertyName = null) + { + if (EqualityComparer.Default.Equals(field, newValue)) + return false; + + field = newValue; + OnPropertyChanged(propertyName); + return true; + } + + protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } +} \ No newline at end of file diff --git a/Qrakhen.TilingFrames/Models/TilingDirection.cs b/Qrakhen.TilingFrames/Models/TilingDirection.cs new file mode 100644 index 0000000..05fb647 --- /dev/null +++ b/Qrakhen.TilingFrames/Models/TilingDirection.cs @@ -0,0 +1,24 @@ +namespace Qrakhen.TilingFrames.Models; + +/// +/// Used to declare where a new shall be placed when tiling. +/// +public enum TilingDirection +{ + /// + /// Right side of the target host, the new child becomes beta. + /// + Right = 0, + /// + /// The bottom of the target host, the new child becomes beta. + /// + Bottom = 1, + /// + /// Left side of the target host, the new child becomes alpha and the target host moves to beta. + /// + Left = 2, + /// + /// The top of the target host, the new child becomes alpha and the target host moves to beta. + /// + Top = 3 +} \ No newline at end of file diff --git a/Qrakhen.TilingFrames/Models/TilingHost.cs b/Qrakhen.TilingFrames/Models/TilingHost.cs new file mode 100644 index 0000000..546d4bd --- /dev/null +++ b/Qrakhen.TilingFrames/Models/TilingHost.cs @@ -0,0 +1,74 @@ +using CopaData.FileInspector.GUI.Models; +using System.Collections.ObjectModel; +using System.Collections.Specialized; + +namespace Qrakhen.TilingFrames.Models; + +/// +/// Host node that by paradigm is located only at the very end of the tree's branches.
+/// Hosts may contain at least one central control, or multiple tabs of controls.
+/// Note that tab contents must _not_ be any tiling controls, as that would break hierarchy and branching logic.
+/// Everything above a will be a by definition. +///
+public class TilingHost : TilingNode +{ + public ObservableCollection Frames { get; } = []; + + private HostFrame? _activeFrame; + public HostFrame? ActiveFrame { + get => _activeFrame; + set => SetField(ref _activeFrame, value); + } + + public bool ShowTabs => Frames.Count > 1; + + public bool IsEmpty => Frames.Count == 0; + + public TilingHost(params HostFrame[] frames) + { + if (frames != null && frames.Length > 0) { + Frames = new ObservableCollection(frames); + ActiveFrame = Frames[^1]; + } + Frames.CollectionChanged += OnTabsChanged; + } + + private void OnTabsChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + if (e.NewItems != null && e.NewItems.Count > 0) { + // Make the last item active + ActiveFrame = e.NewItems[^1] as HostFrame; + } else if (e.OldItems != null && e.OldItems.Count > 0) { + if (ActiveFrame != null && !Frames.Contains(ActiveFrame)) { + // Previous active tab got removed, revert back to the most recent tab or set ActiveTab to null if tabs are empty. + if (Frames.Count > 0) { + ActiveFrame = Frames[^1]; + } else { + ActiveFrame = null; + } + } + } + } + + /// + /// Inserts into as a new tab + /// after being dropped in the center region by the user.
+ /// If the previous host only had one frame inside it, it will be detached from its parent. + /// Otherwise, only the frame will be removed and inserted into 's frames. + ///
+ public static void InsertHost(TilingPanel rootPanel, TilingHost targetHost, TilingHost newHost) + { + HostFrame? frame = newHost.ActiveFrame; + if (frame == null) { + throw new InvalidOperationException($"No active frame to be inserted could be found within {newHost}'s frames."); + } + + if (newHost.Frames.Count == 1) { + TilingPanel.Detach(rootPanel, newHost); + } else { + newHost.Frames.Remove(frame); + } + + targetHost.Frames.Add(frame); + } +} diff --git a/Qrakhen.TilingFrames/Models/TilingMethod.cs b/Qrakhen.TilingFrames/Models/TilingMethod.cs new file mode 100644 index 0000000..3346b53 --- /dev/null +++ b/Qrakhen.TilingFrames/Models/TilingMethod.cs @@ -0,0 +1,16 @@ +namespace Qrakhen.TilingFrames.Models; + +/// +/// Declares the intent of dropping a host into another. +/// +public enum TilingMethod +{ + /// + /// In the case of the host being placed as a split next to the target host. + /// + Split = 0, + /// + /// In case of the new host becoming a tab of the target host, rather than splitting. + /// + Drop = 1 +} diff --git a/Qrakhen.TilingFrames/Models/TilingNode.cs b/Qrakhen.TilingFrames/Models/TilingNode.cs new file mode 100644 index 0000000..7c75a7d --- /dev/null +++ b/Qrakhen.TilingFrames/Models/TilingNode.cs @@ -0,0 +1,8 @@ +namespace Qrakhen.TilingFrames.Models; + +/// +/// Base node for the binary tree structure to work.
+/// A tiling node is either a , which has a reference to two s.
+/// Those nodes may then be additional s, or, when at the end of the branch, a . +///
+public abstract class TilingNode : ObservableObject; diff --git a/Qrakhen.TilingFrames/Models/TilingPanel.cs b/Qrakhen.TilingFrames/Models/TilingPanel.cs new file mode 100644 index 0000000..fb5042f --- /dev/null +++ b/Qrakhen.TilingFrames/Models/TilingPanel.cs @@ -0,0 +1,218 @@ +using System.Windows.Controls; + +namespace Qrakhen.TilingFrames.Models; + +/// +/// This class represents a branch in the tree structure, having two children,
+/// which can be either another - in the case of the tree branching onwards -
+/// or a , if we're at the end of a branch (a so-called leaf node).
+/// A panel contains information about how its children and +/// are separated by exposing an and split ratio. +///
+/// +/// The entire attaching and detaching logic is handled from static methods in this class for brevity.
+/// Those methods are and . +///
+public class TilingPanel : TilingNode +{ + /// + private Orientation _splitOrientation = Orientation.Horizontal; + /// + /// The orientation of the split, so either vertical or horizontal. + /// + public Orientation SplitOrientation { + get => _splitOrientation; + set => SetField(ref _splitOrientation, value); + } + + /// + private double _splitRatio = 0.5; + /// + /// The width or height ratio of the split, with 0.5 being the exact middle. + /// + public double SplitRatio { + get => _splitRatio; + set => SetField(ref _splitRatio, Math.Min(1, Math.Max(0, value))); + } + + /// + private TilingNode _alpha; + /// + /// 'Alpha is always set' is an enforced paradigm, so it will always point to an existing node. + /// + public TilingNode Alpha { + get => _alpha; + private set => SetField(ref _alpha, value); + } + + /// + private TilingNode? _beta; + /// + /// The beta node may be null, for example if a panel is not yet split. + /// + public TilingNode? Beta { + get => _beta; + private set => SetField(ref _beta, value); + } + + /// + /// Whether this panel is split in two, so whether beta is not null. + /// + public bool IsSplit => Beta != null; + + public TilingPanel(TilingNode alpha, TilingNode? beta = null) + : base() + { + _alpha = alpha; + _beta = beta; + } + + /// + /// Traverses down the entire tree from until + /// is encountered, returning its direct parent panel. + /// + /// + /// This may not be the quickest approach, but we're looking at UI tiling panels with perhaps 20 children at most. + /// + public static TilingPanel? GetParentPanel(TilingPanel rootPanel, TilingNode node) + { + if (rootPanel.Alpha == node || rootPanel.Beta == node) { + return rootPanel; + } + + TilingPanel? parent = null; + if (rootPanel.Alpha is TilingPanel alphaPanel) { + parent = GetParentPanel(alphaPanel, node); + } + + if (parent == null && rootPanel.Beta is TilingPanel betaPanel) { + parent = GetParentPanel(betaPanel, node); + } + + return parent; + } + + /// + /// + /// Attaches 's active frame to 's parent as a new .
+ /// The parent will be looked up by traversing the tree from .
+ /// The following ordered rules apply when attaching: + ///
+ /// + /// 1. If 's parent has an open beta slot, will become the beta node.
+ /// 2. If one of the parent's child nodes is an , it will be replaced with
+ /// 3. If none of the above apply, the parent's beta node will be split into a new tiling panel. + ///
+ ///
+ /// + /// All of these actions will automatically split the parent panel.
+ /// If had a parent assigned previously and only a single frame, it will be detached from that branch first.
+ /// If more frames are present in , only the active frame will be removed from it,
+ /// and a new is created, containing only that frame.
+ /// Note that all passed elements have to be children to the same tree. + ///
+ /// The root to start branch traversal from. + /// The host to be split. + /// + /// The host to be attached to 's parent. + /// If set to null, an will be created in its place. + /// + /// The direction at which to drop relative to . + /// The that now contains and . + public static TilingPanel Attach(TilingPanel rootPanel, + TilingHost targetHost, + TilingHost? newHost, + TilingDirection direction) + { + // This method looks way more complicated than it actually is, + // all I wanted to achieve is one centralized place where branching and child assignments happen, + // as tree structures are notoriously hard to navigate and handle when putting references everywhere. + // It really boils down to a simple three-step process: + // 1. Get or create correct host to attach, + // 2. Find out where to place the new host (Alpha or Beta, depending on tiling direction), + // 3. Create branch to be attached, or attach to free slot of parent. + // Todo: This note is in place for the reviewer to have a bit more context. Remove when done. + + TilingPanel? parent = GetParentPanel(rootPanel, targetHost); + if (parent == null) { + throw new NullReferenceException($"Detached target: Could not find a parent for {targetHost} in any of {rootPanel}'s child nodes."); + } + + if (newHost == null) { + // Substitute for the split. + newHost = new EmptyHost(); + } else { + if (newHost.Frames.Count < 2) { + // Detach newHost from a potential parent. + Detach(rootPanel, newHost); + } else { + // Only take the active frame from newHost and instantiate a host to attach. + HostFrame frame = newHost.ActiveFrame!; // Frames.Count >= 2 already ensures ActiveFrame not to be null. + newHost = new TilingHost([frame]); + } + } + + // I tried formulating an explanation for this step but I simply can't. + // "Left or Top go alpha, Bottom or Right go beta." + bool newHostIsAlpha = direction is TilingDirection.Left or TilingDirection.Top; + + // Check whether there's a free beta slot in the parent to use + if (parent.Beta == null) { + if (newHostIsAlpha) { + // Open beta slot but newHost wants to be in alpha slot, simply swap and attach. + parent.Beta = parent.Alpha; + parent.Alpha = newHost; + } else { + // Open beta slot means we just put newHost there, no branching needed. + parent.Beta = newHost; + } + return parent; + } + + // Create a new branch to hold both target- and newHost. + TilingPanel branch; + if (direction is TilingDirection.Left or TilingDirection.Top) { + branch = new TilingPanel(newHost, targetHost); + } else { + branch = new TilingPanel(targetHost, newHost); + } + + // Assign new branch to correct chíld node. + if (parent.Alpha == targetHost) { + parent.Alpha = branch; + } else if (parent.Beta == targetHost) { + parent.Beta = branch; + } + + return branch; + } + + /// + /// Detaches from its parent node, which will be looked up by traversing the .
+ /// If the child happened to be the alpha value, the beta value will be moved into the alpha slot to ensure alpha always having a value.
+ /// If the child's parent node ends up having no children left, which would violate the 'alpha is always set' rule,
+ /// that node itself will also be detached in order to prevent trailing zombie nodes. + ///
+ public static void Detach(TilingPanel rootPanel, TilingNode child) + { + TilingPanel? parent = GetParentPanel(rootPanel, child); + if (parent == null) { + return; // Already an orphan. + } + + if (parent.Alpha == child) { + if (parent.Beta != null) { + // Move beta over to alpha if beta has a value. + parent.Alpha = parent.Beta; + parent.Beta = null; + } else { + // Both branches have detached, which means we'll detach the parent itself + // in order to prevent empty, trailing panels with no children. + Detach(rootPanel, child); + } + } else { + // Child was the beta node, so we simply null it. + parent.Beta = null; + } + } +} diff --git a/Qrakhen.TilingFrames/Qrakhen.TilingFrames.csproj b/Qrakhen.TilingFrames/Qrakhen.TilingFrames.csproj new file mode 100644 index 0000000..a68849e --- /dev/null +++ b/Qrakhen.TilingFrames/Qrakhen.TilingFrames.csproj @@ -0,0 +1,10 @@ + + + + net10.0-windows + enable + true + enable + + + diff --git a/Qrakhen.TilingFrames/README.md b/Qrakhen.TilingFrames/README.md new file mode 100644 index 0000000..7833a67 --- /dev/null +++ b/Qrakhen.TilingFrames/README.md @@ -0,0 +1,45 @@ +# TilingPanels + +Cool Library to have tiling panels. +Is that not cool? +I think it very much is cool. +Very nice, yes. Alpha & Beta. No Gamma or Delta. Just Alpha-Beta. + +Data structure resembles a binary tree with uniform branch- and end-nodes (TilingPanels & TilingHosts). + +### TilingNode + +### TilingPanel + +### TilingHost + +### HostFrame + +### Node Structure +```cs + RootPanel + / \ + A B + / \ + Panel Host + / \ |-Frame (Single Frame) + / \ + Panel Host + / \ |-Frame (Tab) + / . |-Frame (Tab) + Host + |-Frame (Tab) + |-Frame (Tab) + |-Frame (Tab) +``` +Key Behaviours: + - All Panels have information about how and where their content is split in two. + - Every panel may have up to two children (Alpha/Beta), where Alpha is never null. + - Beta may not be set (In a case of a single frame with no splits, for example). + - If a child located at Alpha is detached, Beta will move to alpha, to ensure the 'Alpha is always set' paradigm. + - If a child is detached and both alpha and beta result in being null, the entire Panel is detached from its parent. + - Hosts are always at the end of the branches, everything above a Node is a Panel. + - The things you're dragging around to re-order, separate and split panels are Frames. + - Hosts contain Frames, which will be displayed as tabs if stacked atop each other, or as a single frame if only one is present. + - All mutations of the tree structure expect the root node as their first argument. + That is done so there's no two-way referencing and to keep stuff simple. \ No newline at end of file diff --git a/Qrakhen.TilingFrames/Themes/Colors.xaml b/Qrakhen.TilingFrames/Themes/Colors.xaml new file mode 100644 index 0000000..6b9ec96 --- /dev/null +++ b/Qrakhen.TilingFrames/Themes/Colors.xaml @@ -0,0 +1,36 @@ + + + #242527 + #28292c + #161718 + #1c1d1f + #323336 + #37393c + #727478 + #84868b + #fcfeff + #97999c + #32ce96 + #48faaf + #249672 + #249672 + #ef4232 + + + + + + + + + + + + + + + + + + diff --git a/Qrakhen.TilingFrames/Themes/Controls/DropArea.xaml b/Qrakhen.TilingFrames/Themes/Controls/DropArea.xaml new file mode 100644 index 0000000..07edfc6 --- /dev/null +++ b/Qrakhen.TilingFrames/Themes/Controls/DropArea.xaml @@ -0,0 +1,92 @@ + + + + + + + + + \ No newline at end of file diff --git a/Qrakhen.TilingFrames/Themes/Controls/Host.xaml b/Qrakhen.TilingFrames/Themes/Controls/Host.xaml new file mode 100644 index 0000000..3a68878 --- /dev/null +++ b/Qrakhen.TilingFrames/Themes/Controls/Host.xaml @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Qrakhen.TilingFrames/Themes/Controls/HostFrame.xaml b/Qrakhen.TilingFrames/Themes/Controls/HostFrame.xaml new file mode 100644 index 0000000..fd2bf24 --- /dev/null +++ b/Qrakhen.TilingFrames/Themes/Controls/HostFrame.xaml @@ -0,0 +1,49 @@ + + + + + + + + + + No Template override for the provided HostFrame model found. + You need to override the data template of your host frame like so: + + +<DataTemplate DataType="{x:Type YourFrameModel}"> + <TextBlock Text="This is my model view! :)" /> +</DataTemplate> + + + + + + + + + + Drag or Drop something here :) + + + + + \ No newline at end of file diff --git a/Qrakhen.TilingFrames/Themes/Controls/Panel.xaml b/Qrakhen.TilingFrames/Themes/Controls/Panel.xaml new file mode 100644 index 0000000..e4c007a --- /dev/null +++ b/Qrakhen.TilingFrames/Themes/Controls/Panel.xaml @@ -0,0 +1,115 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/Qrakhen.TilingFrames/Themes/Controls/TilingRoot.xaml b/Qrakhen.TilingFrames/Themes/Controls/TilingRoot.xaml new file mode 100644 index 0000000..6ba5fe4 --- /dev/null +++ b/Qrakhen.TilingFrames/Themes/Controls/TilingRoot.xaml @@ -0,0 +1,26 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/Qrakhen.TilingFrames/Themes/Templates/Generic.xaml b/Qrakhen.TilingFrames/Themes/Templates/Generic.xaml new file mode 100644 index 0000000..fe0e561 --- /dev/null +++ b/Qrakhen.TilingFrames/Themes/Templates/Generic.xaml @@ -0,0 +1,17 @@ + + + diff --git a/Qrakhen.TilingFrames/Themes/Themes.xaml b/Qrakhen.TilingFrames/Themes/Themes.xaml new file mode 100644 index 0000000..1867834 --- /dev/null +++ b/Qrakhen.TilingFrames/Themes/Themes.xaml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file