This commit is contained in:
Qrakhen 2025-12-03 08:36:36 +01:00
parent 8e1e41d9d8
commit e41b1df0ba
29 changed files with 1080 additions and 551 deletions

View File

@ -2,35 +2,61 @@
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Media; using System.Windows.Media;
using System.Xml.Linq;
namespace Qrakhen.TilingFrames.Controls.DragAndDrop; namespace CopaData.FileInspector.GUI.TilingPanels.Controls.DragAndDrop;
public abstract partial class DragAndDropControl : Control /// <summary>
/// Used to handle drag and drop callbacks via the abstract methods <see cref="OnDragStart(object, UIElement)"/> and <see cref="OnDrop(object, UIElement)"/>.
/// Can be used as a parent to any amount of <see cref="UIElement"/>s that refer to the their handler via <see cref="HandlerProperty"/>.
/// If no Handler is provided, the element itself will be considered its own handler, as long as it is a <see cref="DragAndDropControl"/>.
/// In order to make an element draggable, set <see cref="IsDraggableProperty"/> to true within the element's XAML attributes.
/// To mark a FrameworkElement as a drop zone, set <see cref="IsDropZoneProperty"/> to true.
/// If a draggable element is dropped on a drop zone, the handler's <see cref="DragAndDropControl.OnDrop(object, UIElement)" /> is called.
/// </summary>
public abstract class DragAndDropControl : Control
{ {
public virtual void OnDragStart(object model, UIElement draggedElement) { } /// <summary>
/// Called when a DragDrop operation starts.
/// </summary>
protected virtual void OnElementDragStart(UIElement draggedElement) { }
public virtual void OnDragEnd(object model, UIElement targetElement) { } /// <summary>
/// Called during a DragDrop operation in order to update the cursor, if needed.
/// </summary>
protected virtual void OnElementDragGiveCursorFeedback(object sender, GiveFeedbackEventArgs args) { }
public virtual void OnDrag(object sender, MouseEventArgs args) { } /// <summary>
/// Called during a DragDrop operation.
/// </summary>
protected virtual void OnElementDrag(UIElement draggedElement, MouseEventArgs args) { }
#region Dependency Properties /// <summary>
/// Called when an element was dropped without being on an element marked as a DropZone.
/// </summary>
/// <param name="draggedElement"></param>
protected virtual void OnElementDragStop(UIElement draggedElement) { }
public bool AllowDrag { /// <summary>
get => (bool)GetValue(AllowDragProperty); /// Called when this element is the potential target of a DragDrop operation.
set => SetValue(AllowDragProperty, value); /// </summary>
} protected virtual void OnElementDragOver(DragAndDropData data, UIElement targetElement) { }
public static DependencyProperty AllowDragProperty = DependencyProperty /// <summary>
.Register( /// Called when this element stops being a potential target of a DragDrop operation.
nameof(AllowDrag), /// </summary>
typeof(bool), protected virtual void OnElementDragLeave(UIElement targetElement) { }
typeof(DragAndDropControl),
new PropertyMetadata(true));
#endregion /// <summary>
/// Required callback to handle <paramref name="targetElement"/>,
/// provided with <see cref="DragAndDropData"/> containing both the source element being dragged and it's original data context.
/// </summary>
protected abstract void OnElementDropped(DragAndDropData data, UIElement targetElement);
#region Attached Properties #region Attached Properties
/* === IsDraggable === */
public static readonly DependencyProperty IsDraggableProperty = DependencyProperty public static readonly DependencyProperty IsDraggableProperty = DependencyProperty
.RegisterAttached( .RegisterAttached(
"IsDraggable", "IsDraggable",
@ -41,39 +67,68 @@ public abstract partial class DragAndDropControl : Control
public static bool GetIsDraggable(DependencyObject obj) => (bool)obj.GetValue(IsDraggableProperty); public static bool GetIsDraggable(DependencyObject obj) => (bool)obj.GetValue(IsDraggableProperty);
public static void SetIsDraggable(DependencyObject obj, bool value) => obj.SetValue(IsDraggableProperty, value); public static void SetIsDraggable(DependencyObject obj, bool value) => obj.SetValue(IsDraggableProperty, value);
/// <summary>
/// Adds event listeners to the element's GiveFeedback, MouseLeftButtonUp and MouseMove events.
/// </summary>
private static void OnIsDraggableChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) private static void OnIsDraggableChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{ {
if (sender is UIElement element && (bool)e.NewValue) { if (sender is not FrameworkElement element)
}
}
private static void HandleDragStart(object sender, MouseButtonEventArgs args, DragAndDropControl dndControl)
{ {
if (sender is UIElement element) { throw new InvalidOperationException($"IsDraggable may only be attached to FrameworkElements, got {sender.GetType()} instead.");
}
} }
private static void HandleDrop(object sender, MouseButtonEventArgs args, DragAndDropControl dndControl) if ((bool)e.NewValue)
{ {
// moi schaun element.GiveFeedback += HandleGiveFeedback;
element.MouseLeftButtonUp += HandleLeftMouseButtonUp;
element.MouseMove += HandleMouseMove;
} }
else
private static T? FindVisualParent<T>(DependencyObject child) where T : DependencyObject
{ {
// Todo: find a better solution to this, if there even is one. element.GiveFeedback -= HandleGiveFeedback;
// There are, in fact, many controls even in the WPF standard that do similar lookups, element.MouseLeftButtonUp -= HandleLeftMouseButtonUp;
// so I assume it can't be _that_ bad, but every clean solution kind of seems to need some ugly spot in it. :( element.MouseMove -= HandleMouseMove;
}
}
DependencyObject parent = VisualTreeHelper.GetParent(child); /* === IsDropZone === */
while (parent != null) {
if (parent is T expected) public static readonly DependencyProperty IsDropZoneProperty = DependencyProperty
return expected; .RegisterAttached(
parent = VisualTreeHelper.GetParent(parent); "IsDropZone",
typeof(bool),
typeof(DragAndDropControl),
new PropertyMetadata(false, OnIsDropZoneChanged));
public static bool GetIsDropZone(DependencyObject obj) => (bool)obj.GetValue(IsDropZoneProperty);
public static void SetIsDropZone(DependencyObject obj, bool value) => obj.SetValue(IsDropZoneProperty, value);
/// <summary>
/// Adds an event listener to this element's <see cref="UIElement.Drop"/> event.
/// </summary>
private static void OnIsDropZoneChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
if (sender is not FrameworkElement element)
{
throw new InvalidOperationException($"IsDraggable may only be attached to FrameworkElements, got {sender.GetType()} instead.");
} }
return null;
if ((bool)e.NewValue)
{
element.AllowDrop = true;
element.DragOver += HandleDragOver;
element.DragLeave += HandleDragLeave;
element.Drop += HandleDrop;
} }
else
{
element.DragOver -= HandleDragOver;
element.DragLeave -= HandleDragLeave;
element.Drop -= HandleDrop;
}
}
/* === Handler === */
public static readonly DependencyProperty HandlerProperty = DependencyProperty public static readonly DependencyProperty HandlerProperty = DependencyProperty
.RegisterAttached( .RegisterAttached(
@ -87,12 +142,115 @@ public abstract partial class DragAndDropControl : Control
private static void OnHandlerChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) private static void OnHandlerChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{ {
if (sender is UIElement element && e.NewValue is DragAndDropControl dndControl) { // Simple validation
if (sender is not UIElement element || e.NewValue is not DragAndDropControl dndControl)
} {
throw new InvalidOperationException($"Can not set {e.NewValue} as handler. Handler needs to be of type DragAndDropControl."); throw new InvalidOperationException($"Can not set {e.NewValue} as handler. Handler needs to be of type DragAndDropControl.");
} }
}
#endregion #endregion
/// <summary>
/// Attempts to retrieve the correct <see cref="DragAndDropControl"/> handler for the provided element.<br/>
/// This may either be via the defined Handler property, or the element itself if no handler property was attached.
/// </summary>
private static DragAndDropControl RetrieveHandler(DependencyObject obj)
{
if (obj.GetValue(HandlerProperty) is not DragAndDropControl handler)
{
if (obj is DragAndDropControl)
{
handler = (DragAndDropControl)obj;
}
else // Todo: last resort if ((handler = FindVisualParent<DragAndDropControl>(obj)!) == null)
{
throw new InvalidOperationException($"Could not find a DragAndDropControl for element {obj}. You may explicitly provide one using the DragAndDropControl.Handler attached property.");
}
}
return handler;
}
/// <summary>
/// Dynamic handler for the <see cref="UIElement.MouseMove"/> event.
/// Called when the mouse cursor moves while on an element.
/// This also calls the <see cref="OnElementDrag(UIElement, MouseEventArgs)"/> method in order to handle any specific behaviour <b>during</b> the operation.
/// </summary>
private static void HandleMouseMove(object sender, MouseEventArgs args)
{
if (args.LeftButton == MouseButtonState.Pressed)
{
FrameworkElement element = (FrameworkElement)sender;
DragAndDropControl handler = RetrieveHandler(element);
DragAndDropData data = new(element, element.DataContext);
DragDrop.DoDragDrop(element, data, DragDropEffects.All);
handler.OnElementDrag(element, args);
}
}
/// <summary>
/// Dynamic handler for the <see cref="UIElement.GiveFeedback"/> event.
/// </summary>
private static void HandleGiveFeedback(object sender, GiveFeedbackEventArgs args)
{
FrameworkElement element = (FrameworkElement)sender;
DragAndDropControl handler = RetrieveHandler(element);
handler.OnElementDragGiveCursorFeedback(element, args);
}
/// <summary>
/// Dynamic handler for the <see cref="UIElement.MouseLeftButtonUp"/> event.
/// </summary>
private static void HandleLeftMouseButtonUp(object sender, MouseButtonEventArgs args)
{
FrameworkElement element = (FrameworkElement)sender;
DragAndDropControl handler = RetrieveHandler(element);
handler.OnElementDragStop(element);
}
/// <summary>
/// Dynamic handler for the <see cref="UIElement.DragOver"/> event.
/// </summary>
private static void HandleDragOver(object sender, DragEventArgs args)
{
FrameworkElement element = (FrameworkElement)sender;
DragAndDropControl handler = RetrieveHandler(element);
handler.OnElementDragOver((DragAndDropData)args.Data.GetData(typeof(DragAndDropData)), element);
}
/// <summary>
/// Dynamic handler for the <see cref="UIElement.DragLeave"/> event.
/// </summary>
private static void HandleDragLeave(object sender, DragEventArgs args)
{
FrameworkElement element = (FrameworkElement)sender;
DragAndDropControl handler = RetrieveHandler(element);
handler.OnElementDragLeave(element);
}
/// <summary>
/// Dynamic handler for the <see cref="UIElement.Drop"/> event.
/// </summary>
private static void HandleDrop(object sender, DragEventArgs args)
{
FrameworkElement element = (FrameworkElement)sender;
DragAndDropControl handler = RetrieveHandler(element);
handler.OnElementDropped((DragAndDropData)args.Data.GetData(typeof(DragAndDropData)), element);
}
private static T? FindVisualParent<T>(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;
}
} }

View File

@ -0,0 +1,15 @@
using System.Windows;
namespace CopaData.FileInspector.GUI.TilingPanels.Controls.DragAndDrop;
public class DragAndDropData
{
public FrameworkElement DraggedElement { get; }
public object DataModel { get; }
public DragAndDropData(FrameworkElement draggedElement, object dataModel)
{
DraggedElement = draggedElement;
DataModel = dataModel;
}
}

View File

@ -1,9 +1,17 @@
using Qrakhen.TilingFrames.Controls.DragAndDrop; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
namespace Qrakhen.TilingFrames.Controls.DropArea; namespace CopaData.FileInspector.GUI.TilingPanels.Controls.DropArea;
public class DropArea : DragAndDropControl public class DropArea : Control
{ {
public static readonly DependencyProperty DropDecisionProperty = DependencyProperty
.RegisterAttached(
"Decision",
typeof(DropDecision),
typeof(DropArea));
public static DropDecision GetDropDecision(DependencyObject obj) => (DropDecision)obj.GetValue(DropDecisionProperty);
public static void SetDropDecision(DependencyObject obj, bool value) => obj.SetValue(DropDecisionProperty, value);
} }

View File

@ -1,3 +1,3 @@
namespace Qrakhen.TilingFrames.Controls.DropArea; namespace CopaData.FileInspector.GUI.TilingPanels.Controls.DropArea;
public delegate void DropAreaDecisionEvent(DropArea sender, HostControl host, DropDecision decision); public delegate void DropAreaDecisionEvent(DropArea sender, TilingHostControl host, DropDecision decision);

View File

@ -1,11 +1,11 @@
namespace Qrakhen.TilingFrames.Controls.DropArea; namespace CopaData.FileInspector.GUI.TilingPanels.Controls.DropArea;
public enum DropDecision public enum DropDecision
{ {
Cancel = 0, Center = 0,
Center, Left = 1,
Left, Top = 2,
Right, Right = 3,
Top, Bottom = 4,
Bottom Cancel = 5
} }

View File

@ -1,7 +1,7 @@
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
namespace Qrakhen.TilingFrames.Controls namespace CopaData.FileInspector.GUI.TilingPanels.Controls
{ {
/// <summary> /// <summary>
/// Used to force-update columns into rows when orientation is vertical /// Used to force-update columns into rows when orientation is vertical
@ -23,7 +23,8 @@ namespace Qrakhen.TilingFrames.Controls
private static void OnAlphaRowChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) private static void OnAlphaRowChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{ {
if (d is ContentPresenter presenter) { if (d is ContentPresenter presenter)
{
Grid.SetRow(presenter, (int)e.NewValue); Grid.SetRow(presenter, (int)e.NewValue);
Grid.SetColumn(presenter, 0); // Always Column 0 for vertical split Grid.SetColumn(presenter, 0); // Always Column 0 for vertical split
} }
@ -45,7 +46,8 @@ namespace Qrakhen.TilingFrames.Controls
private static void OnBetaRowChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) private static void OnBetaRowChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{ {
if (d is ContentPresenter presenter) { if (d is ContentPresenter presenter)
{
Grid.SetRow(presenter, (int)e.NewValue); Grid.SetRow(presenter, (int)e.NewValue);
Grid.SetColumn(presenter, 0); // Always Column 0 for vertical split Grid.SetColumn(presenter, 0); // Always Column 0 for vertical split
} }

View File

@ -0,0 +1,74 @@
using System.Windows;
using System.Windows.Controls;
using CopaData.FileInspector.GUI.TilingPanels.Controls.DragAndDrop;
using CopaData.FileInspector.GUI.TilingPanels.Controls.DropArea;
using CopaData.FileInspector.GUI.TilingPanels.Models;
using CopaData.FileInspector.GUI.ViewModels;
namespace CopaData.FileInspector.GUI.TilingPanels.Controls;
/// <summary>
/// Control that represents the state of a <see cref="Models.TilingHost"/>,
/// with all the necessary interaction logic tied to it.
/// </summary>
public class TilingHostControl : DragAndDropControl
{
public TilingHost? Host => DataContext as TilingHost;
static TilingHostControl()
{
DefaultStyleKeyProperty.OverrideMetadata(
typeof(TilingHostControl),
new FrameworkPropertyMetadata(typeof(TilingHostControl)));
}
protected override void OnElementDragOver(DragAndDropData data, UIElement targetElement)
{
SetValue(IsDragTargetPropertyKey, true);
}
protected override void OnElementDragLeave(UIElement targetElement)
{
SetValue(IsDragTargetPropertyKey, false);
}
protected override void OnElementDropped(DragAndDropData data, UIElement targetElement)
{
if (data.DataModel is not TilingFrame frame)
{
// not for us
return;
}
DropDecision decision = (DropDecision)targetElement.GetValue(DropArea.DropArea.DropDecisionProperty);
if (decision == DropDecision.Cancel)
return;
TilingDirection direction = (TilingDirection)decision; // simple cast suffices here
if (direction == TilingDirection.None) {
TilingHost.Insert(TilingManagerViewModel.GlobalRoot, Host!, frame);
} else {
TilingPanel.Attach(TilingManagerViewModel.GlobalRoot, Host!, frame, direction);
}
}
#region Dependency Properties
/// <inheritdoc cref="IsDragTarget" />
private static readonly DependencyPropertyKey IsDragTargetPropertyKey =
DependencyProperty.RegisterReadOnly(
nameof(IsDragTarget),
typeof(bool),
typeof(TilingHostControl),
new PropertyMetadata(false));
/// <inheritdoc cref="IsDragTarget" />
public static readonly DependencyProperty IsDragTargetProperty = IsDragTargetPropertyKey.DependencyProperty;
/// <summary>
/// Whether this <see cref="TilingHostControl"/> is being a potential drop target at the moment.
/// </summary>
public bool IsDragTarget => (bool)GetValue(IsDragTargetProperty);
#endregion
}

View File

@ -0,0 +1,40 @@
using System.Windows;
using System.Windows.Controls;
using CopaData.FileInspector.GUI.TilingPanels.Models;
namespace CopaData.FileInspector.GUI.TilingPanels.Controls;
/// <summary>
/// Control that represents the state of a <see cref="TilingFrame"/>,
/// with all the necessary interaction logic tied to it.
/// </summary>
public class TilingHostFrameControl : ContentControl
{
static TilingHostFrameControl()
{
DefaultStyleKeyProperty.OverrideMetadata(
typeof(TilingHostFrameControl),
new FrameworkPropertyMetadata(typeof(TilingHostFrameControl)));
}
public TilingHostFrameControl()
{
}
#region DependencyProperties
public TilingFrame Frame
{
get => (TilingFrame)GetValue(FrameProperty);
set => SetValue(FrameProperty, value);
}
public static DependencyProperty FrameProperty = DependencyProperty
.Register(nameof(Frame),
typeof(TilingFrame),
typeof(TilingHostFrameControl),
new PropertyMetadata(null));
#endregion
}

View File

@ -1,42 +1,8 @@
using Qrakhen.TilingFrames.Models; using CopaData.FileInspector.GUI.TilingPanels.Models;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
namespace Qrakhen.TilingFrames.Controls; namespace CopaData.FileInspector.GUI.TilingPanels.Controls;
/// <summary>
/// For Pop-Outs etc.
/// </summary>
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 public class TilingPanelControl : Control
{ {
@ -46,24 +12,44 @@ public class TilingPanelControl : Control
typeof(TilingPanelControl), typeof(TilingPanelControl),
new FrameworkPropertyMetadata(typeof(TilingPanelControl))); new FrameworkPropertyMetadata(typeof(TilingPanelControl)));
} }
public TilingPanelControl()
{
}
#region DependencyProperties
public Orientation SplitOrientation => Panel.SplitOrientation;
public TilingNode Alpha => Panel.Alpha;
public TilingNode? Beta => Panel.Beta;
public TilingPanel Panel
{
get => (TilingPanel)GetValue(PanelProperty);
set => SetValue(PanelProperty, value);
}
public static DependencyProperty PanelProperty = DependencyProperty
.Register(nameof(Panel),
typeof(TilingPanel),
typeof(TilingPanelControl),
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(TilingPanelControl),
new PropertyMetadata(6.0));
#endregion
} }
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)));
}
}

View File

@ -0,0 +1,45 @@
using CopaData.FileInspector.GUI.TilingPanels.Models;
using System.Windows;
using System.Windows.Controls;
namespace CopaData.FileInspector.GUI.TilingPanels.Controls;
public class TilingRootControl : Control
{
static TilingRootControl()
{
DefaultStyleKeyProperty.OverrideMetadata(
typeof(TilingRootControl),
new FrameworkPropertyMetadata(typeof(TilingRootControl)));
}
#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(TilingRootControl),
new PropertyMetadata(6.0));
public bool IsPrimaryRoot
{
get => (bool)GetValue(IsPrimaryRootProperty);
set => SetValue(IsPrimaryRootProperty, value);
}
public static DependencyProperty IsPrimaryRootProperty = DependencyProperty
.Register(
nameof(IsPrimaryRoot),
typeof(bool),
typeof(TilingRootControl),
new PropertyMetadata(true));
#endregion
}

View File

@ -0,0 +1,45 @@
using System.Windows;
namespace CopaData.FileInspector.GUI.TilingPanels.Controls;
/// <summary>
/// A dedicated window for hosting its own <see cref="TilingRoot"/>, to be used for pop-out interactions.
/// </summary>
public class TilingWindow : Window
{
static TilingWindow()
{
DefaultStyleKeyProperty.OverrideMetadata(
typeof(TilingWindow),
new FrameworkPropertyMetadata(typeof(TilingWindow)));
}
#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(TilingWindow),
new PropertyMetadata(6.0));
public TilingRootControl Root
{
get => (TilingRootControl)GetValue(RootProperty);
set => SetValue(RootProperty, value);
}
public static DependencyProperty RootProperty = DependencyProperty
.Register(
nameof(Root),
typeof(TilingRootControl),
typeof(TilingWindow));
#endregion
}

View File

@ -3,21 +3,39 @@ using System.Windows;
using System.Windows.Data; using System.Windows.Data;
using System.Windows.Markup; using System.Windows.Markup;
namespace Qrakhen.TilingFrames.Converters namespace CopaData.FileInspector.GUI.TilingPanels.Converters;
/// <summary>
/// Converts a double between 0 and 1 to a star-length representation,
/// to be used in conjunction with <see cref="BetaRatioConverter"/>.
/// </summary>
public class AlphaRatioConverter : MarkupExtension, IValueConverter
{ {
public class AlphaRatioConverter : MarkupExtension, IValueConverter
{
public override object ProvideValue(IServiceProvider serviceProvider) => this; public override object ProvideValue(IServiceProvider serviceProvider) => this;
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{ {
if (value is double ratio) { if (value is double ratio)
{
if (ratio <= double.Epsilon) if (ratio <= double.Epsilon)
{
// If a ratio near zero, alpha is essentially hidden.
return new GridLength(0, GridUnitType.Star); return new GridLength(0, GridUnitType.Star);
}
if (ratio <= .5) if (ratio <= .5)
{
// With a ratio less than or equal to .5, alpha will be calculated as the minor component
return new GridLength(1, GridUnitType.Star); return new GridLength(1, GridUnitType.Star);
else if (ratio >= 1) }
if (ratio >= 1)
{
// With a ratio near 1, alpha will be 1.
return new GridLength(1, GridUnitType.Star); return new GridLength(1, GridUnitType.Star);
}
// Returns the parts-per-ratio that alpha represents
return new GridLength((1 - ratio) / ratio, GridUnitType.Star); return new GridLength((1 - ratio) / ratio, GridUnitType.Star);
} }
@ -26,5 +44,4 @@ namespace Qrakhen.TilingFrames.Converters
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotImplementedException(); => throw new NotImplementedException();
}
} }

View File

@ -3,21 +3,39 @@ using System.Windows;
using System.Windows.Data; using System.Windows.Data;
using System.Windows.Markup; using System.Windows.Markup;
namespace Qrakhen.TilingFrames.Converters namespace CopaData.FileInspector.GUI.TilingPanels.Converters;
/// <summary>
/// Converts a double between 0 and 1 to a star-length representation,
/// to be used in conjunction with <see cref="AlphaRatioConverter"/>.
/// </summary>
public class BetaRatioConverter : MarkupExtension, IValueConverter
{ {
public class BetaRatioConverter : MarkupExtension, IValueConverter
{
public override object ProvideValue(IServiceProvider serviceProvider) => this; public override object ProvideValue(IServiceProvider serviceProvider) => this;
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{ {
if (value is double ratio) { if (value is double ratio)
{
if (ratio >= 1) if (ratio >= 1)
{
// If near 1, beta is essentially hidden.
return new GridLength(0, GridUnitType.Star); return new GridLength(0, GridUnitType.Star);
}
if (ratio >= .5) if (ratio >= .5)
{
// With a ratio larger than or equal to 0.5, beta is the lesser component of the two.
return new GridLength(1, GridUnitType.Star); return new GridLength(1, GridUnitType.Star);
else if (ratio <= double.Epsilon) }
if (ratio <= double.Epsilon)
{
// If near zero, beta covers the entire panel.
return new GridLength(1, GridUnitType.Star); return new GridLength(1, GridUnitType.Star);
}
// Return the parts-per-ratio that beta represents.
return new GridLength((1 - ratio) / ratio, GridUnitType.Star); return new GridLength((1 - ratio) / ratio, GridUnitType.Star);
} }
@ -26,5 +44,4 @@ namespace Qrakhen.TilingFrames.Converters
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotImplementedException(); => throw new NotImplementedException();
}
} }

View File

@ -1,6 +1,9 @@
namespace Qrakhen.TilingFrames.Models; namespace CopaData.FileInspector.GUI.TilingPanels.Models;
public class EmptyFrame : HostFrame /// <summary>
/// Initial empty frame, can be turned of by setting <see cref="TilingRoot.ShowEmptyInitial"/> to false.
/// </summary>
public class EmptyFrame : TilingFrame
{ {
public EmptyFrame() public EmptyFrame()
{ {
@ -8,10 +11,7 @@ public class EmptyFrame : HostFrame
} }
} }
public class TestOverrideFrame : HostFrame /// <summary>
{ /// Todo: remove this later, only used to test the non-overriden case of a data template
public TestOverrideFrame() /// </summary>
{ public class TestOverrideFrame : TilingFrame { public TestOverrideFrame() { HeaderText = "Testerinho"; } }
HeaderText = "Testerinho";
}
}

View File

@ -1,4 +1,4 @@
namespace Qrakhen.TilingFrames.Models; namespace CopaData.FileInspector.GUI.TilingPanels.Models;
public class EmptyHost : TilingHost public class EmptyHost : TilingHost
{ {

View File

@ -1,7 +1,7 @@
namespace Qrakhen.TilingFrames.Models; namespace CopaData.FileInspector.GUI.TilingPanels.Models;
/// <summary> /// <summary>
/// Flags for declaring the buttons on a <see cref="HostFrame"/> /// Flags for declaring the buttons on a <see cref="TilingHost"/>
/// </summary> /// </summary>
[Flags] [Flags]
public enum FrameButtons public enum FrameButtons

View File

@ -1,24 +1,25 @@
namespace Qrakhen.TilingFrames.Models; namespace CopaData.FileInspector.GUI.TilingPanels.Models;
/// <summary> /// <summary>
/// Used to declare where a new <see cref="TilingHost"/> shall be placed when tiling. /// Used to declare where a new <see cref="TilingHost"/> shall be placed when tiling.
/// </summary> /// </summary>
public enum TilingDirection public enum TilingDirection
{ {
/// <summary> None = 0,
/// Right side of the target host, the new child becomes beta.
/// </summary>
Right = 0,
/// <summary>
/// The bottom of the target host, the new child becomes beta.
/// </summary>
Bottom = 1,
/// <summary> /// <summary>
/// Left side of the target host, the new child becomes alpha and the target host moves to beta. /// Left side of the target host, the new child becomes alpha and the target host moves to beta.
/// </summary> /// </summary>
Left = 2, Left = 1,
/// <summary> /// <summary>
/// The top of the target host, the new child becomes alpha and the target host moves to beta. /// The top of the target host, the new child becomes alpha and the target host moves to beta.
/// </summary> /// </summary>
Top = 3 Top = 2,
/// <summary>
/// Right side of the target host, the new child becomes beta.
/// </summary>
Right = 3,
/// <summary>
/// The bottom of the target host, the new child becomes beta.
/// </summary>
Bottom = 4
} }

View File

@ -0,0 +1,60 @@
using CopaData.FileInspector.GUI.Models;
namespace CopaData.FileInspector.GUI.TilingPanels.Models;
/// <summary>
/// Base data class for content that is displayed for a <see cref="TilingHost"/>'s tabs or stand-alone pop-out windows.<br/>
/// Anything that you can physically see from the root <see cref="TilingPanel"/> is a <see cref="TilingFrame"/>.<br/>
/// Serving a header text, pin state and button configuration properties.
/// </summary>
public abstract class TilingFrame : Observable
{
/// <inheritdoc cref="HeaderText"/>
private string _headerText = string.Empty;
/// <summary>
/// The title of the tab that will be displayed inside the tab button.
/// </summary>
public string HeaderText
{
get => _headerText;
set => SetField(ref _headerText, value);
}
/// <inheritdoc cref="FrameButtons"/>
private FrameButtons _frameButtons = FrameButtons.Default;
/// <summary>
/// The buttons to be displayed next to the header.
/// </summary>
public FrameButtons FrameButtons
{
get => _frameButtons;
set => SetField(ref _frameButtons, value);
}
/// <inheritdoc cref="IsPinned"/>
private bool _isPinned = false;
/// <summary>
/// The title of the tab that will be displayed inside the tab button.
/// </summary>
public bool IsPinned
{
get => _isPinned;
set => SetField(ref _isPinned, value);
}
/// <inheritdoc cref="Parent" />
private TilingHost? _parent;
/// <summary>
/// The parent hosting this frame.
/// </summary>
public TilingHost? Parent
{
get => _parent;
set => SetField(ref _parent, value);
}
public virtual object? GetOptions() => null;
public virtual void SetOptions(object? options) { }
public virtual object? GetHelp() => null;
}

View File

@ -2,7 +2,7 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Collections.Specialized; using System.Collections.Specialized;
namespace Qrakhen.TilingFrames.Models; namespace CopaData.FileInspector.GUI.TilingPanels.Models;
/// <summary> /// <summary>
/// Host node that by paradigm is located only at the very end of the tree's branches.<br/> /// Host node that by paradigm is located only at the very end of the tree's branches.<br/>
@ -12,10 +12,11 @@ namespace Qrakhen.TilingFrames.Models;
/// </summary> /// </summary>
public class TilingHost : TilingNode public class TilingHost : TilingNode
{ {
public ObservableCollection<HostFrame> Frames { get; } = []; public ObservableCollection<TilingFrame> Frames { get; } = [];
private HostFrame? _activeFrame; private TilingFrame? _activeFrame;
public HostFrame? ActiveFrame { public TilingFrame? ActiveFrame
{
get => _activeFrame; get => _activeFrame;
set => SetField(ref _activeFrame, value); set => SetField(ref _activeFrame, value);
} }
@ -24,26 +25,47 @@ public class TilingHost : TilingNode
public bool IsEmpty => Frames.Count == 0; public bool IsEmpty => Frames.Count == 0;
public TilingHost(params HostFrame[] frames) public TilingHost(params TilingFrame[] frames)
{ {
if (frames != null && frames.Length > 0) { if (frames != null && frames.Length > 0)
Frames = new ObservableCollection<HostFrame>(frames); {
Frames = new ObservableCollection<TilingFrame>(frames);
ActiveFrame = Frames[^1]; ActiveFrame = Frames[^1];
foreach (var frame in Frames)
{
frame.Parent = this;
} }
Frames.CollectionChanged += OnTabsChanged;
} }
private void OnTabsChanged(object? sender, NotifyCollectionChangedEventArgs e) Frames.CollectionChanged += OnFramesItemsChanged;
}
private void OnFramesItemsChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null && e.NewItems.Count > 0)
{ {
if (e.NewItems != null && e.NewItems.Count > 0) {
// Make the last item active // Make the last item active
ActiveFrame = e.NewItems[^1] as HostFrame; ActiveFrame = e.NewItems[^1] as TilingFrame;
} else if (e.OldItems != null && e.OldItems.Count > 0) { foreach (var item in e.NewItems)
if (ActiveFrame != null && !Frames.Contains(ActiveFrame)) { {
if (item is TilingFrame frame)
{
// We're using a reference here as this is the only place that always listens to updates.
frame.Parent = this;
}
}
}
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. // 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) { if (Frames.Count > 0)
{
ActiveFrame = Frames[^1]; ActiveFrame = Frames[^1];
} else { }
else
{
ActiveFrame = null; ActiveFrame = null;
} }
} }
@ -56,17 +78,15 @@ public class TilingHost : TilingNode
/// If the previous host only had one frame inside it, it will be detached from its parent. /// 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 <paramref name="targetHost"/>'s frames. /// Otherwise, only the frame will be removed and inserted into <paramref name="targetHost"/>'s frames.
/// </summary> /// </summary>
public static void InsertHost(TilingPanel rootPanel, TilingHost targetHost, TilingHost newHost) public static void Insert(TilingPanel rootPanel, TilingHost targetHost, TilingFrame frame)
{ {
HostFrame? frame = newHost.ActiveFrame; if (frame.Parent?.Frames.Count < 2)
if (frame == null) { {
throw new InvalidOperationException($"No active frame to be inserted could be found within {newHost}'s frames."); TilingPanel.Detach(rootPanel, frame.Parent);
} }
else
if (newHost.Frames.Count == 1) { {
TilingPanel.Detach(rootPanel, newHost); frame.Parent?.Frames.Remove(frame);
} else {
newHost.Frames.Remove(frame);
} }
targetHost.Frames.Add(frame); targetHost.Frames.Add(frame);

View File

@ -1,4 +1,4 @@
namespace Qrakhen.TilingFrames.Models; namespace CopaData.FileInspector.GUI.TilingPanels.Models;
/// <summary> /// <summary>
/// Declares the intent of dropping a host into another. /// Declares the intent of dropping a host into another.

View File

@ -1,8 +1,10 @@
namespace Qrakhen.TilingFrames.Models; using CopaData.FileInspector.GUI.Models;
namespace CopaData.FileInspector.GUI.TilingPanels.Models;
/// <summary> /// <summary>
/// Base node for the binary tree structure to work.<br/> /// Base node for the binary tree structure to work.<br/>
/// A tiling node is either a <see cref="TilingPanel"/>, which has a reference to two <see cref="TilingNode"/>s.<br/> /// A tiling node is either a <see cref="TilingPanel"/>, which has a reference to two <see cref="TilingNode"/>s.<br/>
/// Those nodes may then be additional <see cref="TilingPanel"/>s, or, when at the end of the branch, a <see cref="TilingHost"/>. /// Those nodes may then be additional <see cref="TilingPanel"/>s, or, when at the end of the branch, a <see cref="TilingHost"/>.
/// </summary> /// </summary>
public abstract class TilingNode : ObservableObject; public abstract class TilingNode : Observable;

View File

@ -1,6 +1,6 @@
using System.Windows.Controls; using System.Windows.Controls;
namespace Qrakhen.TilingFrames.Models; namespace CopaData.FileInspector.GUI.TilingPanels.Models;
/// <summary> /// <summary>
/// This class represents a branch in the tree structure, having two children,<br/> /// This class represents a branch in the tree structure, having two children,<br/>
@ -20,7 +20,8 @@ public class TilingPanel : TilingNode
/// <summary> /// <summary>
/// The orientation of the split, so either vertical or horizontal. /// The orientation of the split, so either vertical or horizontal.
/// </summary> /// </summary>
public Orientation SplitOrientation { public Orientation SplitOrientation
{
get => _splitOrientation; get => _splitOrientation;
set => SetField(ref _splitOrientation, value); set => SetField(ref _splitOrientation, value);
} }
@ -30,17 +31,31 @@ public class TilingPanel : TilingNode
/// <summary> /// <summary>
/// The width or height ratio of the split, with 0.5 being the exact middle. /// The width or height ratio of the split, with 0.5 being the exact middle.
/// </summary> /// </summary>
public double SplitRatio { public double SplitRatio
{
get => _splitRatio; get => _splitRatio;
set => SetField(ref _splitRatio, Math.Min(1, Math.Max(0, value))); set => SetField(ref _splitRatio, Math.Min(1, Math.Max(0, value)));
} }
/// <inheritdoc cref="CutoffLength" />
private double _cutoffLength = 256;
/// <summary>
/// TODO: Move this into the view/control, it really got nothing in common with the model i think.
/// The width or height of alpha in pixels, will be initialized with 50% of the panel's available width.
/// </summary>
public double CutoffLength
{
get => _cutoffLength;
set => SetField(ref _cutoffLength, Math.Min(1, Math.Max(0, value)));
}
/// <inheritdoc cref="Alpha" /> /// <inheritdoc cref="Alpha" />
private TilingNode _alpha; private TilingNode _alpha;
/// <summary> /// <summary>
/// 'Alpha is always set' is an enforced paradigm, so it will always point to an existing node. /// 'Alpha is always set' is an enforced paradigm, so it will always point to an existing node.
/// </summary> /// </summary>
public TilingNode Alpha { public TilingNode Alpha
{
get => _alpha; get => _alpha;
private set => SetField(ref _alpha, value); private set => SetField(ref _alpha, value);
} }
@ -50,7 +65,8 @@ public class TilingPanel : TilingNode
/// <summary> /// <summary>
/// The beta node may be null, for example if a panel is not yet split. /// The beta node may be null, for example if a panel is not yet split.
/// </summary> /// </summary>
public TilingNode? Beta { public TilingNode? Beta
{
get => _beta; get => _beta;
private set => SetField(ref _beta, value); private set => SetField(ref _beta, value);
} }
@ -76,16 +92,19 @@ public class TilingPanel : TilingNode
/// </remarks> /// </remarks>
public static TilingPanel? GetParentPanel(TilingPanel rootPanel, TilingNode node) public static TilingPanel? GetParentPanel(TilingPanel rootPanel, TilingNode node)
{ {
if (rootPanel.Alpha == node || rootPanel.Beta == node) { if (rootPanel.Alpha == node || rootPanel.Beta == node)
{
return rootPanel; return rootPanel;
} }
TilingPanel? parent = null; TilingPanel? parent = null;
if (rootPanel.Alpha is TilingPanel alphaPanel) { if (rootPanel.Alpha is TilingPanel alphaPanel)
{
parent = GetParentPanel(alphaPanel, node); parent = GetParentPanel(alphaPanel, node);
} }
if (parent == null && rootPanel.Beta is TilingPanel betaPanel) { if (parent == null && rootPanel.Beta is TilingPanel betaPanel)
{
parent = GetParentPanel(betaPanel, node); parent = GetParentPanel(betaPanel, node);
} }
@ -94,34 +113,35 @@ public class TilingPanel : TilingNode
/// <summary> /// <summary>
/// <para> /// <para>
/// Attaches <paramref name="newHost"/>'s active frame to <paramref name="targetHost"/>'s parent as a new <see cref="TilingHost"/>.<br/> /// Attaches <paramref name="newFrame"/> to <paramref name="targetHost"/>'s parent as a new <see cref="TilingHost"/>.<br/>
/// The parent will be looked up by traversing the tree from <paramref name="rootPanel"/>.<br/> /// The parent will be looked up by traversing the tree from <paramref name="rootPanel"/>.<br/>
/// The following ordered rules apply when attaching: /// The following ordered rules apply when attaching:
/// </para> /// </para>
/// <para> /// <para>
/// 1. If <paramref name="targetHost"/>'s parent has an open beta slot, <paramref name="newHost"/> will become the beta node.<br/> /// 1. If <paramref name="targetHost"/>'s parent has an open beta slot, <paramref name="newFrame"/> will become the beta node, wrappped in a new host.<br/>
/// 2. If one of the <paramref name="targetHost"/> parent's child nodes is an <see cref="EmptyHost"/>, it will be replaced with <paramref name="newHost"/><br/> /// 2. If one of the <paramref name="targetHost"/> parent's child nodes is an <see cref="EmptyHost"/>, <paramref name="newFrame"/> will be inserted<br/>
/// 3. If none of the above apply, the <paramref name="targetHost"/> parent's beta node will be split into a new tiling panel. /// 3. If none of the above apply, the <paramref name="targetHost"/> parent's beta node will be split into a new tiling panel.
/// </para> /// </para>
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// All of these actions will automatically split the parent panel.<br/> /// All of these actions will automatically split the parent panel.<br/>
/// If <paramref name="newHost"/> had a parent assigned previously and only a single frame, it will be detached from that branch first.<br/> /// If <paramref name="newFrame"/> had a parent assigned previously, that parent will be detached from the branch first.<br/>
/// If more frames are present in <paramref name="newHost"/>, only the active frame will be removed from it,<br/> /// If more frames are present in <paramref name="newFrame"/>'s parent, only that frame will be removed from it,<br/>
/// and a new <see cref="TilingHost"/> is created, containing only that frame.<br/> /// and a new <see cref="TilingHost"/> is created, containing only that frame.<br/>
/// Note that all passed elements have to be children to the same <see cref="TilingNode"/> tree. /// Note that all passed elements have to be children to the same <see cref="TilingNode"/> tree.
/// In order to attach a <see cref="TilingFrame"/> to a foreign tree, <see cref="Detach"/> it first and insert it to the foreign host.
/// </remarks> /// </remarks>
/// <param name="rootPanel">The root to start branch traversal from.</param> /// <param name="rootPanel">The root to start branch traversal from.</param>
/// <param name="targetHost">The host to be split.</param> /// <param name="targetHost">The host to be split.</param>
/// <param name="newHost"> /// <param name="newFrame">
/// The host to be attached to <paramref name="targetHost"/>'s parent. /// The frame to be attached inside a new host to <paramref name="targetHost"/>'s parent.
/// If set to null, an <see cref="EmptyHost"/> will be created in its place. /// If set to null, an <see cref="EmptyHost"/> will be created in its place.
/// </param> /// </param>
/// <param name="direction">The direction at which to drop <paramref name="newHost"/> relative to <paramref name="targetHost"/>.</param> /// <param name="direction">The direction at which to drop <paramref name="newHost"/> relative to <paramref name="targetHost"/>.</param>
/// <returns>The <see cref="TilingPanel"/> that now contains <paramref name="newHost"/> and <paramref name="targetHost"/>.</returns> /// <returns>The <see cref="TilingPanel"/> that now contains <paramref name="newHost"/> and <paramref name="targetHost"/>.</returns>
public static TilingPanel Attach(TilingPanel rootPanel, public static TilingPanel Attach(TilingPanel rootPanel,
TilingHost targetHost, TilingHost targetHost,
TilingHost? newHost, TilingFrame? newFrame,
TilingDirection direction) TilingDirection direction)
{ {
// This method looks way more complicated than it actually is, // This method looks way more complicated than it actually is,
@ -134,22 +154,26 @@ public class TilingPanel : TilingNode
// Todo: This note is in place for the reviewer to have a bit more context. Remove when done. // Todo: This note is in place for the reviewer to have a bit more context. Remove when done.
TilingPanel? parent = GetParentPanel(rootPanel, targetHost); TilingPanel? parent = GetParentPanel(rootPanel, targetHost);
if (parent == null) { if (parent == null)
{
throw new NullReferenceException($"Detached target: Could not find a parent for {targetHost} in any of {rootPanel}'s child nodes."); throw new NullReferenceException($"Detached target: Could not find a parent for {targetHost} in any of {rootPanel}'s child nodes.");
} }
if (newHost == null) { TilingHost? newHost;
if (newFrame == null)
{
// Substitute for the split. // Substitute for the split.
newHost = new EmptyHost(); 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]);
} }
else
{
if (newFrame.Parent?.Frames.Count < 2)
{
// Detach newFrame's host from its parent.
Detach(rootPanel, newFrame.Parent);
}
newHost = new TilingHost([ newFrame ]);
} }
// I tried formulating an explanation for this step but I simply can't. // I tried formulating an explanation for this step but I simply can't.
@ -157,12 +181,16 @@ public class TilingPanel : TilingNode
bool newHostIsAlpha = direction is TilingDirection.Left or TilingDirection.Top; bool newHostIsAlpha = direction is TilingDirection.Left or TilingDirection.Top;
// Check whether there's a free beta slot in the parent to use // Check whether there's a free beta slot in the parent to use
if (parent.Beta == null) { if (parent.Beta == null || parent.Beta is EmptyHost emptyHost)
if (newHostIsAlpha) { {
if (newHostIsAlpha)
{
// Open beta slot but newHost wants to be in alpha slot, simply swap and attach. // Open beta slot but newHost wants to be in alpha slot, simply swap and attach.
parent.Beta = parent.Alpha; parent.Beta = parent.Alpha;
parent.Alpha = newHost; parent.Alpha = newHost;
} else { }
else
{
// Open beta slot means we just put newHost there, no branching needed. // Open beta slot means we just put newHost there, no branching needed.
parent.Beta = newHost; parent.Beta = newHost;
} }
@ -171,16 +199,22 @@ public class TilingPanel : TilingNode
// Create a new branch to hold both target- and newHost. // Create a new branch to hold both target- and newHost.
TilingPanel branch; TilingPanel branch;
if (direction is TilingDirection.Left or TilingDirection.Top) { if (direction is TilingDirection.Left or TilingDirection.Top)
{
branch = new TilingPanel(newHost, targetHost); branch = new TilingPanel(newHost, targetHost);
} else { }
else
{
branch = new TilingPanel(targetHost, newHost); branch = new TilingPanel(targetHost, newHost);
} }
// Assign new branch to correct chíld node. // Assign new branch to correct chíld node.
if (parent.Alpha == targetHost) { if (parent.Alpha == targetHost)
{
parent.Alpha = branch; parent.Alpha = branch;
} else if (parent.Beta == targetHost) { }
else if (parent.Beta == targetHost)
{
parent.Beta = branch; parent.Beta = branch;
} }
@ -196,21 +230,28 @@ public class TilingPanel : TilingNode
public static void Detach(TilingPanel rootPanel, TilingNode child) public static void Detach(TilingPanel rootPanel, TilingNode child)
{ {
TilingPanel? parent = GetParentPanel(rootPanel, child); TilingPanel? parent = GetParentPanel(rootPanel, child);
if (parent == null) { if (parent == null)
{
return; // Already an orphan. return; // Already an orphan.
} }
if (parent.Alpha == child) { if (parent.Alpha == child)
if (parent.Beta != null) { {
if (parent.Beta != null)
{
// Move beta over to alpha if beta has a value. // Move beta over to alpha if beta has a value.
parent.Alpha = parent.Beta; parent.Alpha = parent.Beta;
parent.Beta = null; parent.Beta = null;
} else { }
else
{
// Both branches have detached, which means we'll detach the parent itself // Both branches have detached, which means we'll detach the parent itself
// in order to prevent empty, trailing panels with no children. // in order to prevent empty, trailing panels with no children.
Detach(rootPanel, child); Detach(rootPanel, child);
} }
} else { }
else
{
// Child was the beta node, so we simply null it. // Child was the beta node, so we simply null it.
parent.Beta = null; parent.Beta = null;
} }

View File

@ -1,4 +1,6 @@
# TilingPanels # TilingPanels
## Tiling window manager control library for WPF
### CopaData.Ui.Wpf.TilingPanels
Cool Library to have tiling panels. Cool Library to have tiling panels.
Is that not cool? Is that not cool?
@ -7,13 +9,15 @@ 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). Data structure resembles a binary tree with uniform branch- and end-nodes (TilingPanels & TilingHosts).
### TilingNode ### Node
### TilingPanel ### Panel
### TilingHost ### Host
### HostFrame ### Frame
### Window
### Node Structure ### Node Structure
```cs ```cs

View File

@ -1,11 +1,9 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:Qrakhen.TilingFrames.Converters" xmlns:converters="clr-namespace:CopaData.FileInspector.GUI.TilingPanels.Converters"
xmlns:local="clr-namespace:Qrakhen.TilingFrames.Controls.DropArea" xmlns:local="clr-namespace:CopaData.FileInspector.GUI.TilingPanels.Controls.DropArea"
xmlns:dnd="clr-namespace:Qrakhen.TilingFrames.Controls.DragAndDrop" xmlns:dnd="clr-namespace:CopaData.FileInspector.GUI.TilingPanels.Controls.DragAndDrop"
xmlns:models="clr-namespace:Qrakhen.TilingFrames.Models"> xmlns:models="clr-namespace:CopaData.FileInspector.GUI.TilingPanels.Models">
<converters:AlphaRatioConverter x:Key="RatioToStarConverter" />
<Style x:Key="DropAreaButton" TargetType="{x:Type Button}"> <Style x:Key="DropAreaButton" TargetType="{x:Type Button}">
<Setter Property="BorderThickness" Value="0" /> <Setter Property="BorderThickness" Value="0" />
@ -42,33 +40,28 @@
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Button x:Name="CenterDrop" <Button x:Name="CenterDrop"
Drop="CenterDrop_Drop" dnd:DragAndDropControl.IsDropZone="True"
dnd:DragAndDropControl.IsDraggable="True" dnd:DragAndDropControl.Handler="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}}"
dnd:DragAndDropControl.HAndler="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}}"
Style="{StaticResource DropAreaButton}" Style="{StaticResource DropAreaButton}"
Grid.Row="1" Grid.Row="1"
Grid.Column="1" /> Grid.Column="1" />
<Button x:Name="LeftDrop" <Button x:Name="LeftDrop"
Drop="LeftDrop_Drop"
Style="{StaticResource DropAreaButton}" Style="{StaticResource DropAreaButton}"
Grid.Row="1" Grid.Row="1"
Grid.Column="0" /> Grid.Column="0" />
<Button x:Name="TopDrop" <Button x:Name="TopDrop"
Drop="TopDrop_Drop"
Style="{StaticResource DropAreaButton}" Style="{StaticResource DropAreaButton}"
Grid.Row="0" Grid.Row="0"
Grid.Column="1" /> Grid.Column="1" />
<Button x:Name="RightDrop" <Button x:Name="RightDrop"
Drop="RightDrop_Drop"
Style="{StaticResource DropAreaButton}" Style="{StaticResource DropAreaButton}"
Grid.Row="1" Grid.Row="1"
Grid.Column="2" /> Grid.Column="2" />
<Button x:Name="BottomDrop" <Button x:Name="BottomDrop"
Drop="BottomDrop_Drop"
Style="{StaticResource DropAreaButton}" Style="{StaticResource DropAreaButton}"
Grid.Row="2" Grid.Row="2"
Grid.Column="1" /> Grid.Column="1" />

View File

@ -1,22 +1,31 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:Qrakhen.TilingFrames.Converters" xmlns:converters="clr-namespace:CopaData.FileInspector.GUI.TilingPanels.Converters"
xmlns:local="clr-namespace:Qrakhen.TilingFrames.Controls" xmlns:local="clr-namespace:CopaData.FileInspector.GUI.TilingPanels.Controls"
xmlns:models="clr-namespace:Qrakhen.TilingFrames.Models"> xmlns:models="clr-namespace:CopaData.FileInspector.GUI.TilingPanels.Models"
xmlns:dnd="clr-namespace:CopaData.FileInspector.GUI.TilingPanels.Controls.DragAndDrop"
xmlns:droparea="clr-namespace:CopaData.FileInspector.GUI.TilingPanels.Controls.DropArea">
<Style TargetType="{x:Type local:HostControl}">
<Style TargetType="{x:Type local:TilingHostControl}">
<Setter Property="Template"> <Setter Property="Template">
<Setter.Value> <Setter.Value>
<ControlTemplate TargetType="{x:Type local:HostControl}"> <ControlTemplate TargetType="{x:Type local:TilingHostControl}">
<Border Padding="0" <Border Padding="0"
CornerRadius="4" CornerRadius="4"
BorderBrush="{DynamicResource Brush_Border_Primary}" BorderBrush="{DynamicResource Brush_Border_Primary}"
BorderThickness="1"> BorderThickness="1"
dnd:DragAndDropControl.Handler="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}}"
dnd:DragAndDropControl.IsDropZone="True">
<TabControl ItemsSource="{Binding Frames}" <TabControl ItemsSource="{Binding Frames}"
SelectedItem="{Binding ActiveFrame, Mode=TwoWay}"> SelectedItem="{Binding ActiveFrame, Mode=TwoWay}">
<TabControl.ItemTemplate> <TabControl.ItemTemplate>
<DataTemplate> <DataTemplate>
<Grid Background="Transparent"> <Grid Background="Transparent"
dnd:DragAndDropControl.Handler="{Binding RelativeSource={
RelativeSource Mode=FindAncestor,
AncestorType={x:Type dnd:DragAndDropControl}}}"
dnd:DragAndDropControl.IsDraggable="True">
<TextBlock Text="{Binding HeaderText}" /> <TextBlock Text="{Binding HeaderText}" />
</Grid> </Grid>
</DataTemplate> </DataTemplate>
@ -56,9 +65,12 @@
Padding="4" Padding="4"
BorderBrush="{DynamicResource Brush_Border_Primary}" BorderBrush="{DynamicResource Brush_Border_Primary}"
Background="Transparent"> Background="Transparent">
<Grid>
<ContentPresenter ContentSource="SelectedContent" <ContentPresenter ContentSource="SelectedContent"
KeyboardNavigation.TabNavigation="Cycle" KeyboardNavigation.TabNavigation="Cycle"
KeyboardNavigation.DirectionalNavigation="Contained" /> KeyboardNavigation.DirectionalNavigation="Contained" />
<droparea:DropArea />
</Grid>
</Border> </Border>
</Grid> </Grid>
</Border> </Border>
@ -75,7 +87,7 @@
</Style> </Style>
<DataTemplate DataType="{x:Type models:TilingHost}"> <DataTemplate DataType="{x:Type models:TilingHost}">
<local:HostControl /> <local:TilingHostControl />
</DataTemplate> </DataTemplate>
<Style TargetType="TabItem"> <Style TargetType="TabItem">

View File

@ -1,13 +1,11 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:Qrakhen.TilingFrames.Converters" xmlns:converters="clr-namespace:CopaData.FileInspector.GUI.TilingPanels.Converters"
xmlns:local="clr-namespace:Qrakhen.TilingFrames.Controls" xmlns:local="clr-namespace:CopaData.FileInspector.GUI.TilingPanels.Controls"
xmlns:models="clr-namespace:Qrakhen.TilingFrames.Models"> xmlns:models="clr-namespace:CopaData.FileInspector.GUI.TilingPanels.Models">
<converters:AlphaRatioConverter x:Key="RatioToStarConverter" />
<!-- NO TEMPLATE OVERRIDE (FALLBACK) --> <!-- NO TEMPLATE OVERRIDE (FALLBACK) -->
<DataTemplate DataType="{x:Type models:HostFrame}"> <DataTemplate DataType="{x:Type models:TilingFrame}">
<Border BorderBrush="{DynamicResource Brush_Text_Error}"> <Border BorderBrush="{DynamicResource Brush_Text_Error}">
<StackPanel> <StackPanel>
<TextBlock FontSize="14" <TextBlock FontSize="14"

View File

@ -1,13 +1,13 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:Qrakhen.TilingFrames.Converters" xmlns:converters="clr-namespace:CopaData.FileInspector.GUI.TilingPanels.Converters"
xmlns:local="clr-namespace:Qrakhen.TilingFrames.Controls" xmlns:local="clr-namespace:CopaData.FileInspector.GUI.TilingPanels.Controls"
xmlns:models="clr-namespace:Qrakhen.TilingFrames.Models"> xmlns:models="clr-namespace:CopaData.FileInspector.GUI.TilingPanels.Models">
<converters:AlphaRatioConverter x:Key="AlphaRatioConverter" /> <converters:AlphaRatioConverter x:Key="AlphaRatioConverter" />
<converters:BetaRatioConverter x:Key="BetaRatioConverter" /> <converters:BetaRatioConverter x:Key="BetaRatioConverter" />
<Style TargetType="{x:Type local:PanelControl}"> <Style TargetType="{x:Type local:TilingPanelControl}">
<Style.Setters> <Style.Setters>
<Setter Property="SplitterWidth" Value="{Binding SplitterWidth, <Setter Property="SplitterWidth" Value="{Binding SplitterWidth,
RelativeSource={RelativeSource RelativeSource={RelativeSource
@ -20,17 +20,13 @@
<DataTrigger Binding="{Binding SplitOrientation}" Value="{x:Static Orientation.Vertical}"> <DataTrigger Binding="{Binding SplitOrientation}" Value="{x:Static Orientation.Vertical}">
<Setter Property="Template"> <Setter Property="Template">
<Setter.Value> <Setter.Value>
<ControlTemplate TargetType="{x:Type local:PanelControl}"> <ControlTemplate TargetType="{x:Type local:TilingPanelControl}">
<Border CornerRadius="4"> <Border CornerRadius="4">
<Grid> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="{Binding SplitRatio, <RowDefinition Height="*" />
Mode=OneWay,
Converter={StaticResource AlphaRatioConverter}}" />
<RowDefinition Height="{TemplateBinding SplitterWidth}" /> <RowDefinition Height="{TemplateBinding SplitterWidth}" />
<RowDefinition Height="{Binding SplitRatio, <RowDefinition Height="*" />
Mode=OneWay,
Converter={StaticResource BetaRatioConverter}}" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<ContentPresenter Grid.Row="0" <ContentPresenter Grid.Row="0"
@ -57,17 +53,13 @@
<DataTrigger Binding="{Binding SplitOrientation}" Value="{x:Static Orientation.Horizontal}"> <DataTrigger Binding="{Binding SplitOrientation}" Value="{x:Static Orientation.Horizontal}">
<Setter Property="Template"> <Setter Property="Template">
<Setter.Value> <Setter.Value>
<ControlTemplate TargetType="{x:Type local:PanelControl}"> <ControlTemplate TargetType="{x:Type local:TilingPanelControl}">
<Border CornerRadius="4"> <Border CornerRadius="4">
<Grid> <Grid>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="{Binding SplitRatio, <ColumnDefinition Width="*" />
Mode=OneWay,
Converter={StaticResource AlphaRatioConverter}}" />
<ColumnDefinition Width="{TemplateBinding SplitterWidth}" /> <ColumnDefinition Width="{TemplateBinding SplitterWidth}" />
<ColumnDefinition Width="{Binding SplitRatio, <ColumnDefinition Width="*" />
Mode=OneWay,
Converter={StaticResource BetaRatioConverter}}" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<ContentPresenter Grid.Column="0" <ContentPresenter Grid.Column="0"
@ -95,7 +87,7 @@
<DataTrigger Binding="{Binding Beta}" Value="{x:Null}"> <DataTrigger Binding="{Binding Beta}" Value="{x:Null}">
<Setter Property="Template"> <Setter Property="Template">
<Setter.Value> <Setter.Value>
<ControlTemplate TargetType="{x:Type local:PanelControl}"> <ControlTemplate TargetType="{x:Type local:TilingPanelControl}">
<Border CornerRadius="4"> <Border CornerRadius="4">
<Grid> <Grid>
<ContentPresenter Content="{Binding Alpha}" /> <ContentPresenter Content="{Binding Alpha}" />
@ -109,7 +101,7 @@
</Style> </Style>
<DataTemplate DataType="{x:Type models:TilingPanel}"> <DataTemplate DataType="{x:Type models:TilingPanel}">
<local:PanelControl /> <local:TilingPanelControl />
</DataTemplate> </DataTemplate>
</ResourceDictionary> </ResourceDictionary>

View File

@ -1,10 +1,8 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:Qrakhen.TilingFrames.Converters" xmlns:converters="clr-namespace:CopaData.FileInspector.GUI.TilingPanels.Converters"
xmlns:local="clr-namespace:Qrakhen.TilingFrames.Controls" xmlns:local="clr-namespace:CopaData.FileInspector.GUI.TilingPanels.Controls"
xmlns:models="clr-namespace:Qrakhen.TilingFrames.Models"> xmlns:models="clr-namespace:CopaData.FileInspector.GUI.TilingPanels.Models">
<converters:AlphaRatioConverter x:Key="RatioToStarConverter" />
<Style TargetType="{x:Type local:TilingRoot}"> <Style TargetType="{x:Type local:TilingRoot}">
<Setter Property="Template"> <Setter Property="Template">
@ -12,7 +10,7 @@
<ControlTemplate TargetType="{x:Type local:TilingRoot}"> <ControlTemplate TargetType="{x:Type local:TilingRoot}">
<Border Padding="{TemplateBinding Padding}" <Border Padding="{TemplateBinding Padding}"
Margin="{TemplateBinding Margin}"> Margin="{TemplateBinding Margin}">
<local:PanelControl DataContext="{Binding}" /> <local:TilingPanelControl DataContext="{Binding}" />
</Border> </Border>
</ControlTemplate> </ControlTemplate>
</Setter.Value> </Setter.Value>

View File

@ -1,6 +1,7 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ResourceDictionary.MergedDictionaries> <ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="./Controls/DropArea.xaml" />
<ResourceDictionary Source="./Controls/TilingRoot.xaml" /> <ResourceDictionary Source="./Controls/TilingRoot.xaml" />
<ResourceDictionary Source="./Controls/Panel.xaml" /> <ResourceDictionary Source="./Controls/Panel.xaml" />
<ResourceDictionary Source="./Controls/Host.xaml" /> <ResourceDictionary Source="./Controls/Host.xaml" />