From e41b1df0ba07b2b954a4bde34d27532570874712 Mon Sep 17 00:00:00 2001 From: Qrakhen Date: Wed, 3 Dec 2025 08:36:36 +0100 Subject: [PATCH] state --- .../DragAndDrop/DragAndDropControl.cs | 276 +++++++++--- .../Controls/DragAndDrop/DragAndDropData.cs | 15 + .../Controls/DropArea/DropArea.cs | 14 +- .../DropArea/DropAreaDecisionEvent.cs | 4 +- .../Controls/DropArea/DropDecision.cs | 14 +- .../Controls/TilingGridAttachment.cs | 64 +-- .../Controls/TilingHostControl.cs | 74 ++++ .../Controls/TilingHostFrameControl.cs | 40 ++ .../Controls/TilingPanelControl.cs | 108 ++--- .../Controls/TilingRootControl.cs | 45 ++ Qrakhen.TilingFrames/Controls/TilingWindow.cs | 45 ++ .../Converters/AlphaRatioConverter.cs | 55 ++- .../Converters/BetaRatioConverter.cs | 55 ++- Qrakhen.TilingFrames/Models/EmptyFrame.cs | 26 +- Qrakhen.TilingFrames/Models/EmptyHost.cs | 8 +- Qrakhen.TilingFrames/Models/FrameButtons.cs | 22 +- .../Models/TilingDirection.cs | 23 +- Qrakhen.TilingFrames/Models/TilingFrame.cs | 60 +++ Qrakhen.TilingFrames/Models/TilingHost.cs | 120 ++--- Qrakhen.TilingFrames/Models/TilingMethod.cs | 18 +- Qrakhen.TilingFrames/Models/TilingNode.cs | 6 +- Qrakhen.TilingFrames/Models/TilingPanel.cs | 413 ++++++++++-------- Qrakhen.TilingFrames/README.md | 12 +- .../Themes/Controls/DropArea.xaml | 19 +- .../Themes/Controls/Host.xaml | 34 +- .../Themes/Controls/HostFrame.xaml | 10 +- .../Themes/Controls/Panel.xaml | 38 +- .../Themes/Controls/TilingRoot.xaml | 10 +- Qrakhen.TilingFrames/Themes/Themes.xaml | 3 +- 29 files changed, 1080 insertions(+), 551 deletions(-) create mode 100644 Qrakhen.TilingFrames/Controls/DragAndDrop/DragAndDropData.cs create mode 100644 Qrakhen.TilingFrames/Controls/TilingHostControl.cs create mode 100644 Qrakhen.TilingFrames/Controls/TilingHostFrameControl.cs create mode 100644 Qrakhen.TilingFrames/Controls/TilingRootControl.cs create mode 100644 Qrakhen.TilingFrames/Controls/TilingWindow.cs create mode 100644 Qrakhen.TilingFrames/Models/TilingFrame.cs diff --git a/Qrakhen.TilingFrames/Controls/DragAndDrop/DragAndDropControl.cs b/Qrakhen.TilingFrames/Controls/DragAndDrop/DragAndDropControl.cs index 4f1b3c4..ae04508 100644 --- a/Qrakhen.TilingFrames/Controls/DragAndDrop/DragAndDropControl.cs +++ b/Qrakhen.TilingFrames/Controls/DragAndDrop/DragAndDropControl.cs @@ -2,97 +2,255 @@ using System.Windows.Controls; using System.Windows.Input; 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 +/// +/// Used to handle drag and drop callbacks via the abstract methods and . +/// Can be used as a parent to any amount of s that refer to the their handler via . +/// If no Handler is provided, the element itself will be considered its own handler, as long as it is a . +/// In order to make an element draggable, set to true within the element's XAML attributes. +/// To mark a FrameworkElement as a drop zone, set to true. +/// If a draggable element is dropped on a drop zone, the handler's is called. +/// +public abstract class DragAndDropControl : Control { - public virtual void OnDragStart(object model, UIElement draggedElement) { } + /// + /// Called when a DragDrop operation starts. + /// + protected virtual void OnElementDragStart(UIElement draggedElement) { } - public virtual void OnDragEnd(object model, UIElement targetElement) { } + /// + /// Called during a DragDrop operation in order to update the cursor, if needed. + /// + protected virtual void OnElementDragGiveCursorFeedback(object sender, GiveFeedbackEventArgs args) { } - public virtual void OnDrag(object sender, MouseEventArgs args) { } + /// + /// Called during a DragDrop operation. + /// + protected virtual void OnElementDrag(UIElement draggedElement, MouseEventArgs args) { } - #region Dependency Properties + /// + /// Called when an element was dropped without being on an element marked as a DropZone. + /// + /// + protected virtual void OnElementDragStop(UIElement draggedElement) { } - public bool AllowDrag { - get => (bool)GetValue(AllowDragProperty); - set => SetValue(AllowDragProperty, value); - } + /// + /// Called when this element is the potential target of a DragDrop operation. + /// + protected virtual void OnElementDragOver(DragAndDropData data, UIElement targetElement) { } - public static DependencyProperty AllowDragProperty = DependencyProperty - .Register( - nameof(AllowDrag), - typeof(bool), - typeof(DragAndDropControl), - new PropertyMetadata(true)); + /// + /// Called when this element stops being a potential target of a DragDrop operation. + /// + protected virtual void OnElementDragLeave(UIElement targetElement) { } - #endregion + /// + /// Required callback to handle , + /// provided with containing both the source element being dragged and it's original data context. + /// + protected abstract void OnElementDropped(DragAndDropData data, UIElement targetElement); - #region Attached Properties + #region Attached Properties - public static readonly DependencyProperty IsDraggableProperty = DependencyProperty + /* === IsDraggable === */ + + 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); + 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) { + /// + /// Adds event listeners to the element's GiveFeedback, MouseLeftButtonUp and MouseMove events. + /// + private static void OnIsDraggableChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) + { + if (sender is not FrameworkElement element) + { + throw new InvalidOperationException($"IsDraggable may only be attached to FrameworkElements, got {sender.GetType()} instead."); + } - } - } + if ((bool)e.NewValue) + { + element.GiveFeedback += HandleGiveFeedback; + element.MouseLeftButtonUp += HandleLeftMouseButtonUp; + element.MouseMove += HandleMouseMove; + } + else + { + element.GiveFeedback -= HandleGiveFeedback; + element.MouseLeftButtonUp -= HandleLeftMouseButtonUp; + element.MouseMove -= HandleMouseMove; + } + } - private static void HandleDragStart(object sender, MouseButtonEventArgs args, DragAndDropControl dndControl) - { - if (sender is UIElement element) { + /* === IsDropZone === */ - } - } + public static readonly DependencyProperty IsDropZoneProperty = DependencyProperty + .RegisterAttached( + "IsDropZone", + typeof(bool), + typeof(DragAndDropControl), + new PropertyMetadata(false, OnIsDropZoneChanged)); - private static void HandleDrop(object sender, MouseButtonEventArgs args, DragAndDropControl dndControl) - { - // moi schaun - } + public static bool GetIsDropZone(DependencyObject obj) => (bool)obj.GetValue(IsDropZoneProperty); + public static void SetIsDropZone(DependencyObject obj, bool value) => obj.SetValue(IsDropZoneProperty, value); - 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. :( + /// + /// Adds an event listener to this element's event. + /// + 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."); + } - DependencyObject parent = VisualTreeHelper.GetParent(child); - while (parent != null) { - if (parent is T expected) - return expected; - parent = VisualTreeHelper.GetParent(parent); - } - 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; + } + } - public static readonly DependencyProperty HandlerProperty = DependencyProperty + /* === Handler === */ + + 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) { - - } + 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) + { + // 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."); - } + } + } - #endregion + #endregion + + /// + /// Attempts to retrieve the correct handler for the provided element.
+ /// This may either be via the defined Handler property, or the element itself if no handler property was attached. + ///
+ 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(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; + } + + /// + /// Dynamic handler for the event. + /// Called when the mouse cursor moves while on an element. + /// This also calls the method in order to handle any specific behaviour during the operation. + /// + 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); + } + } + + /// + /// Dynamic handler for the event. + /// + private static void HandleGiveFeedback(object sender, GiveFeedbackEventArgs args) + { + FrameworkElement element = (FrameworkElement)sender; + DragAndDropControl handler = RetrieveHandler(element); + handler.OnElementDragGiveCursorFeedback(element, args); + } + + /// + /// Dynamic handler for the event. + /// + private static void HandleLeftMouseButtonUp(object sender, MouseButtonEventArgs args) + { + FrameworkElement element = (FrameworkElement)sender; + DragAndDropControl handler = RetrieveHandler(element); + handler.OnElementDragStop(element); + } + + /// + /// Dynamic handler for the event. + /// + 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); + } + + /// + /// Dynamic handler for the event. + /// + private static void HandleDragLeave(object sender, DragEventArgs args) + { + FrameworkElement element = (FrameworkElement)sender; + DragAndDropControl handler = RetrieveHandler(element); + handler.OnElementDragLeave(element); + } + + /// + /// Dynamic handler for the event. + /// + 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(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; + } } diff --git a/Qrakhen.TilingFrames/Controls/DragAndDrop/DragAndDropData.cs b/Qrakhen.TilingFrames/Controls/DragAndDrop/DragAndDropData.cs new file mode 100644 index 0000000..6717f42 --- /dev/null +++ b/Qrakhen.TilingFrames/Controls/DragAndDrop/DragAndDropData.cs @@ -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; + } +} diff --git a/Qrakhen.TilingFrames/Controls/DropArea/DropArea.cs b/Qrakhen.TilingFrames/Controls/DropArea/DropArea.cs index 57d8e5a..7f3dcbd 100644 --- a/Qrakhen.TilingFrames/Controls/DropArea/DropArea.cs +++ b/Qrakhen.TilingFrames/Controls/DropArea/DropArea.cs @@ -1,9 +1,17 @@ -using Qrakhen.TilingFrames.Controls.DragAndDrop; +using System.Windows; 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); } diff --git a/Qrakhen.TilingFrames/Controls/DropArea/DropAreaDecisionEvent.cs b/Qrakhen.TilingFrames/Controls/DropArea/DropAreaDecisionEvent.cs index 58f66f1..dc0686e 100644 --- a/Qrakhen.TilingFrames/Controls/DropArea/DropAreaDecisionEvent.cs +++ b/Qrakhen.TilingFrames/Controls/DropArea/DropAreaDecisionEvent.cs @@ -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); diff --git a/Qrakhen.TilingFrames/Controls/DropArea/DropDecision.cs b/Qrakhen.TilingFrames/Controls/DropArea/DropDecision.cs index b367311..99b9b2d 100644 --- a/Qrakhen.TilingFrames/Controls/DropArea/DropDecision.cs +++ b/Qrakhen.TilingFrames/Controls/DropArea/DropDecision.cs @@ -1,11 +1,11 @@ -namespace Qrakhen.TilingFrames.Controls.DropArea; +namespace CopaData.FileInspector.GUI.TilingPanels.Controls.DropArea; public enum DropDecision { - Cancel = 0, - Center, - Left, - Right, - Top, - Bottom + Center = 0, + Left = 1, + Top = 2, + Right = 3, + Bottom = 4, + Cancel = 5 } \ No newline at end of file diff --git a/Qrakhen.TilingFrames/Controls/TilingGridAttachment.cs b/Qrakhen.TilingFrames/Controls/TilingGridAttachment.cs index 82c8131..d86b1c1 100644 --- a/Qrakhen.TilingFrames/Controls/TilingGridAttachment.cs +++ b/Qrakhen.TilingFrames/Controls/TilingGridAttachment.cs @@ -1,55 +1,57 @@ using System.Windows; using System.Windows.Controls; -namespace Qrakhen.TilingFrames.Controls +namespace CopaData.FileInspector.GUI.TilingPanels.Controls { - /// - /// Used to force-update columns into rows when orientation is vertical - /// - public static class TilingGridAttachment - { - #region AlphaRow - public static readonly DependencyProperty AlphaRowProperty = + /// + /// 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); + 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) + 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.SetColumn(presenter, 0); // Always Column 0 for vertical split - } + Grid.SetRow(presenter, (int)e.NewValue); + Grid.SetColumn(presenter, 0); // Always Column 0 for vertical split } - #endregion + } + #endregion - #region BetaRow - public static readonly DependencyProperty BetaRowProperty = + #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); + 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) + 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.SetColumn(presenter, 0); // Always Column 0 for vertical split - } + Grid.SetRow(presenter, (int)e.NewValue); + Grid.SetColumn(presenter, 0); // Always Column 0 for vertical split } - #endregion - } + } + #endregion + } } diff --git a/Qrakhen.TilingFrames/Controls/TilingHostControl.cs b/Qrakhen.TilingFrames/Controls/TilingHostControl.cs new file mode 100644 index 0000000..21b71f9 --- /dev/null +++ b/Qrakhen.TilingFrames/Controls/TilingHostControl.cs @@ -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; + +/// +/// Control that represents the state of a , +/// with all the necessary interaction logic tied to it. +/// +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 + + /// + private static readonly DependencyPropertyKey IsDragTargetPropertyKey = + DependencyProperty.RegisterReadOnly( + nameof(IsDragTarget), + typeof(bool), + typeof(TilingHostControl), + new PropertyMetadata(false)); + + /// + public static readonly DependencyProperty IsDragTargetProperty = IsDragTargetPropertyKey.DependencyProperty; + + /// + /// Whether this is being a potential drop target at the moment. + /// + public bool IsDragTarget => (bool)GetValue(IsDragTargetProperty); + + #endregion +} diff --git a/Qrakhen.TilingFrames/Controls/TilingHostFrameControl.cs b/Qrakhen.TilingFrames/Controls/TilingHostFrameControl.cs new file mode 100644 index 0000000..969b149 --- /dev/null +++ b/Qrakhen.TilingFrames/Controls/TilingHostFrameControl.cs @@ -0,0 +1,40 @@ +using System.Windows; +using System.Windows.Controls; +using CopaData.FileInspector.GUI.TilingPanels.Models; + +namespace CopaData.FileInspector.GUI.TilingPanels.Controls; + +/// +/// Control that represents the state of a , +/// with all the necessary interaction logic tied to it. +/// +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 +} diff --git a/Qrakhen.TilingFrames/Controls/TilingPanelControl.cs b/Qrakhen.TilingFrames/Controls/TilingPanelControl.cs index ccd2d69..7c44903 100644 --- a/Qrakhen.TilingFrames/Controls/TilingPanelControl.cs +++ b/Qrakhen.TilingFrames/Controls/TilingPanelControl.cs @@ -1,69 +1,55 @@ -using Qrakhen.TilingFrames.Models; +using CopaData.FileInspector.GUI.TilingPanels.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))); - } -} +namespace CopaData.FileInspector.GUI.TilingPanels.Controls; public class TilingPanelControl : Control { - static TilingPanelControl() - { - DefaultStyleKeyProperty.OverrideMetadata( - typeof(TilingPanelControl), - new FrameworkPropertyMetadata(typeof(TilingPanelControl))); - } + static TilingPanelControl() + { + DefaultStyleKeyProperty.OverrideMetadata( + 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))); - } -} \ No newline at end of file diff --git a/Qrakhen.TilingFrames/Controls/TilingRootControl.cs b/Qrakhen.TilingFrames/Controls/TilingRootControl.cs new file mode 100644 index 0000000..7d3fa8e --- /dev/null +++ b/Qrakhen.TilingFrames/Controls/TilingRootControl.cs @@ -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 +} diff --git a/Qrakhen.TilingFrames/Controls/TilingWindow.cs b/Qrakhen.TilingFrames/Controls/TilingWindow.cs new file mode 100644 index 0000000..e6e5e48 --- /dev/null +++ b/Qrakhen.TilingFrames/Controls/TilingWindow.cs @@ -0,0 +1,45 @@ +using System.Windows; + +namespace CopaData.FileInspector.GUI.TilingPanels.Controls; + +/// +/// A dedicated window for hosting its own , to be used for pop-out interactions. +/// +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 +} diff --git a/Qrakhen.TilingFrames/Converters/AlphaRatioConverter.cs b/Qrakhen.TilingFrames/Converters/AlphaRatioConverter.cs index ad23da8..f89feea 100644 --- a/Qrakhen.TilingFrames/Converters/AlphaRatioConverter.cs +++ b/Qrakhen.TilingFrames/Converters/AlphaRatioConverter.cs @@ -3,28 +3,45 @@ using System.Windows; using System.Windows.Data; using System.Windows.Markup; -namespace Qrakhen.TilingFrames.Converters +namespace CopaData.FileInspector.GUI.TilingPanels.Converters; + +/// +/// Converts a double between 0 and 1 to a star-length representation, +/// to be used in conjunction with . +/// +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 (ratio <= double.Epsilon) { - 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."); + // If a ratio near zero, alpha is essentially hidden. + return new GridLength(0, GridUnitType.Star); } - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) - => throw new NotImplementedException(); - } + 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); + } + + if (ratio >= 1) + { + // With a ratio near 1, alpha will be 1. + return new GridLength(1, GridUnitType.Star); + } + + // Returns the parts-per-ratio that alpha represents + 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 index 2e92fe1..be1dc40 100644 --- a/Qrakhen.TilingFrames/Converters/BetaRatioConverter.cs +++ b/Qrakhen.TilingFrames/Converters/BetaRatioConverter.cs @@ -3,28 +3,45 @@ using System.Windows; using System.Windows.Data; using System.Windows.Markup; -namespace Qrakhen.TilingFrames.Converters +namespace CopaData.FileInspector.GUI.TilingPanels.Converters; + +/// +/// Converts a double between 0 and 1 to a star-length representation, +/// to be used in conjunction with . +/// +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 (ratio >= 1) { - 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."); + // If near 1, beta is essentially hidden. + return new GridLength(0, GridUnitType.Star); } - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) - => throw new NotImplementedException(); - } + 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); + } + + if (ratio <= double.Epsilon) + { + // If near zero, beta covers the entire panel. + return new GridLength(1, GridUnitType.Star); + } + + // Return the parts-per-ratio that beta represents. + 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 index 60e0bae..f13ff84 100644 --- a/Qrakhen.TilingFrames/Models/EmptyFrame.cs +++ b/Qrakhen.TilingFrames/Models/EmptyFrame.cs @@ -1,17 +1,17 @@ -namespace Qrakhen.TilingFrames.Models; +namespace CopaData.FileInspector.GUI.TilingPanels.Models; -public class EmptyFrame : HostFrame +/// +/// Initial empty frame, can be turned of by setting to false. +/// +public class EmptyFrame : TilingFrame { - public EmptyFrame() - { - HeaderText = "Empty"; - } + public EmptyFrame() + { + HeaderText = "Empty"; + } } -public class TestOverrideFrame : HostFrame -{ - public TestOverrideFrame() - { - HeaderText = "Testerinho"; - } -} +/// +/// Todo: remove this later, only used to test the non-overriden case of a data template +/// +public class TestOverrideFrame : TilingFrame { public TestOverrideFrame() { HeaderText = "Testerinho"; } } diff --git a/Qrakhen.TilingFrames/Models/EmptyHost.cs b/Qrakhen.TilingFrames/Models/EmptyHost.cs index 5f97b49..0f0e716 100644 --- a/Qrakhen.TilingFrames/Models/EmptyHost.cs +++ b/Qrakhen.TilingFrames/Models/EmptyHost.cs @@ -1,8 +1,8 @@ -namespace Qrakhen.TilingFrames.Models; +namespace CopaData.FileInspector.GUI.TilingPanels.Models; public class EmptyHost : TilingHost { - public EmptyHost() : base(new EmptyFrame(), new TestOverrideFrame()) - { - } + public EmptyHost() : base(new EmptyFrame(), new TestOverrideFrame()) + { + } } diff --git a/Qrakhen.TilingFrames/Models/FrameButtons.cs b/Qrakhen.TilingFrames/Models/FrameButtons.cs index 4c4fa6e..0773fef 100644 --- a/Qrakhen.TilingFrames/Models/FrameButtons.cs +++ b/Qrakhen.TilingFrames/Models/FrameButtons.cs @@ -1,19 +1,19 @@ -namespace Qrakhen.TilingFrames.Models; +namespace CopaData.FileInspector.GUI.TilingPanels.Models; /// -/// Flags for declaring the buttons on a +/// 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, + 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 + Default = Close | Pin | PopOut } diff --git a/Qrakhen.TilingFrames/Models/TilingDirection.cs b/Qrakhen.TilingFrames/Models/TilingDirection.cs index 05fb647..1983b54 100644 --- a/Qrakhen.TilingFrames/Models/TilingDirection.cs +++ b/Qrakhen.TilingFrames/Models/TilingDirection.cs @@ -1,24 +1,25 @@ -namespace Qrakhen.TilingFrames.Models; +namespace CopaData.FileInspector.GUI.TilingPanels.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, + None = 0, /// /// Left side of the target host, the new child becomes alpha and the target host moves to beta. /// - Left = 2, + Left = 1, /// /// The top of the target host, the new child becomes alpha and the target host moves to beta. /// - Top = 3 + Top = 2, + /// + /// Right side of the target host, the new child becomes beta. + /// + Right = 3, + /// + /// The bottom of the target host, the new child becomes beta. + /// + Bottom = 4 } \ No newline at end of file diff --git a/Qrakhen.TilingFrames/Models/TilingFrame.cs b/Qrakhen.TilingFrames/Models/TilingFrame.cs new file mode 100644 index 0000000..13bb199 --- /dev/null +++ b/Qrakhen.TilingFrames/Models/TilingFrame.cs @@ -0,0 +1,60 @@ +using CopaData.FileInspector.GUI.Models; + +namespace CopaData.FileInspector.GUI.TilingPanels.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 TilingFrame : Observable +{ + /// + 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); + } + + /// + private TilingHost? _parent; + /// + /// The parent hosting this frame. + /// + 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; +} diff --git a/Qrakhen.TilingFrames/Models/TilingHost.cs b/Qrakhen.TilingFrames/Models/TilingHost.cs index 546d4bd..72b1341 100644 --- a/Qrakhen.TilingFrames/Models/TilingHost.cs +++ b/Qrakhen.TilingFrames/Models/TilingHost.cs @@ -2,7 +2,7 @@ using System.Collections.ObjectModel; using System.Collections.Specialized; -namespace Qrakhen.TilingFrames.Models; +namespace CopaData.FileInspector.GUI.TilingPanels.Models; /// /// Host node that by paradigm is located only at the very end of the tree's branches.
@@ -12,63 +12,83 @@ namespace Qrakhen.TilingFrames.Models; ///
public class TilingHost : TilingNode { - public ObservableCollection Frames { get; } = []; + public ObservableCollection Frames { get; } = []; - private HostFrame? _activeFrame; - public HostFrame? ActiveFrame { - get => _activeFrame; - set => SetField(ref _activeFrame, value); - } + private TilingFrame? _activeFrame; + public TilingFrame? ActiveFrame + { + get => _activeFrame; + set => SetField(ref _activeFrame, value); + } - public bool ShowTabs => Frames.Count > 1; + public bool ShowTabs => Frames.Count > 1; - public bool IsEmpty => Frames.Count == 0; + public bool IsEmpty => Frames.Count == 0; - public TilingHost(params HostFrame[] frames) - { - if (frames != null && frames.Length > 0) { - Frames = new ObservableCollection(frames); - ActiveFrame = Frames[^1]; + public TilingHost(params TilingFrame[] frames) + { + if (frames != null && frames.Length > 0) + { + Frames = new ObservableCollection(frames); + ActiveFrame = Frames[^1]; + foreach (var frame in Frames) + { + frame.Parent = this; } - 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; - } - } + Frames.CollectionChanged += OnFramesItemsChanged; + } + + private void OnFramesItemsChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + if (e.NewItems != null && e.NewItems.Count > 0) + { + // Make the last item active + ActiveFrame = e.NewItems[^1] as TilingFrame; + foreach (var item in e.NewItems) + { + 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; + } } - } - - /// - /// 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."); + } + 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; + } } + } + } - if (newHost.Frames.Count == 1) { - TilingPanel.Detach(rootPanel, newHost); - } else { - newHost.Frames.Remove(frame); - } + /// + /// 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 Insert(TilingPanel rootPanel, TilingHost targetHost, TilingFrame frame) + { + if (frame.Parent?.Frames.Count < 2) + { + TilingPanel.Detach(rootPanel, frame.Parent); + } + else + { + frame.Parent?.Frames.Remove(frame); + } - targetHost.Frames.Add(frame); - } + targetHost.Frames.Add(frame); + } } diff --git a/Qrakhen.TilingFrames/Models/TilingMethod.cs b/Qrakhen.TilingFrames/Models/TilingMethod.cs index 3346b53..22eba42 100644 --- a/Qrakhen.TilingFrames/Models/TilingMethod.cs +++ b/Qrakhen.TilingFrames/Models/TilingMethod.cs @@ -1,16 +1,16 @@ -namespace Qrakhen.TilingFrames.Models; +namespace CopaData.FileInspector.GUI.TilingPanels.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 + /// + /// 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 index 7c75a7d..0efb773 100644 --- a/Qrakhen.TilingFrames/Models/TilingNode.cs +++ b/Qrakhen.TilingFrames/Models/TilingNode.cs @@ -1,8 +1,10 @@ -namespace Qrakhen.TilingFrames.Models; +using CopaData.FileInspector.GUI.Models; + +namespace CopaData.FileInspector.GUI.TilingPanels.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; +public abstract class TilingNode : Observable; diff --git a/Qrakhen.TilingFrames/Models/TilingPanel.cs b/Qrakhen.TilingFrames/Models/TilingPanel.cs index fb5042f..7e63153 100644 --- a/Qrakhen.TilingFrames/Models/TilingPanel.cs +++ b/Qrakhen.TilingFrames/Models/TilingPanel.cs @@ -1,6 +1,6 @@ using System.Windows.Controls; -namespace Qrakhen.TilingFrames.Models; +namespace CopaData.FileInspector.GUI.TilingPanels.Models; /// /// This class represents a branch in the tree structure, having two children,
@@ -15,204 +15,245 @@ namespace Qrakhen.TilingFrames.Models; /// 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 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 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 double _cutoffLength = 256; + /// + /// 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. + /// + public double CutoffLength + { + get => _cutoffLength; + set => SetField(ref _cutoffLength, Math.Min(1, Math.Max(0, 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); - } + /// + 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); + } - /// - /// Whether this panel is split in two, so whether beta is not null. - /// - public bool IsSplit => Beta != null; + /// + 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); + } - public TilingPanel(TilingNode alpha, TilingNode? beta = null) - : base() - { - _alpha = alpha; - _beta = beta; - } + /// + /// Whether this panel is split in two, so whether beta is not null. + /// + public bool IsSplit => Beta != null; - /// - /// 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; + 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 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, wrappped in a new host.
+ /// 2. If one of the parent's child nodes is an , will be inserted
+ /// 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, that parent will be detached from the branch first.
+ /// If more frames are present in 's parent, only that 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. + /// In order to attach a to a foreign tree, it first and insert it to the foreign host. + ///
+ /// The root to start branch traversal from. + /// The host to be split. + /// + /// The frame to be attached inside a new host 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, + TilingFrame? newFrame, + 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."); + } + + TilingHost? newHost; + if (newFrame == null) + { + // Substitute for the split. + newHost = new EmptyHost(); + } + else + { + if (newFrame.Parent?.Frames.Count < 2) + { + // Detach newFrame's host from its parent. + Detach(rootPanel, newFrame.Parent); } - TilingPanel? parent = null; - if (rootPanel.Alpha is TilingPanel alphaPanel) { - parent = GetParentPanel(alphaPanel, node); - } + newHost = new TilingHost([ newFrame ]); + } - if (parent == null && rootPanel.Beta is TilingPanel betaPanel) { - parent = GetParentPanel(betaPanel, node); - } + // 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 || parent.Beta is EmptyHost emptyHost) + { + 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; - } + } - /// - /// - /// 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. + // 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); + } - 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."); + // 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; } - - 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]); - } + 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); } - - // 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; - } - } + } + else + { + // Child was the beta node, so we simply null it. + parent.Beta = null; + } + } } diff --git a/Qrakhen.TilingFrames/README.md b/Qrakhen.TilingFrames/README.md index 7833a67..815dc7e 100644 --- a/Qrakhen.TilingFrames/README.md +++ b/Qrakhen.TilingFrames/README.md @@ -1,4 +1,6 @@ # TilingPanels +## Tiling window manager control library for WPF +### CopaData.Ui.Wpf.TilingPanels Cool Library to have tiling panels. 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). -### TilingNode +### Node -### TilingPanel +### Panel -### TilingHost +### Host -### HostFrame +### Frame + +### Window ### Node Structure ```cs diff --git a/Qrakhen.TilingFrames/Themes/Controls/DropArea.xaml b/Qrakhen.TilingFrames/Themes/Controls/DropArea.xaml index 07edfc6..43fb407 100644 --- a/Qrakhen.TilingFrames/Themes/Controls/DropArea.xaml +++ b/Qrakhen.TilingFrames/Themes/Controls/DropArea.xaml @@ -1,11 +1,9 @@  - - + xmlns:converters="clr-namespace:CopaData.FileInspector.GUI.TilingPanels.Converters" + xmlns:local="clr-namespace:CopaData.FileInspector.GUI.TilingPanels.Controls.DropArea" + xmlns:dnd="clr-namespace:CopaData.FileInspector.GUI.TilingPanels.Controls.DragAndDrop" + xmlns:models="clr-namespace:CopaData.FileInspector.GUI.TilingPanels.Models"> - + - + \ No newline at end of file diff --git a/Qrakhen.TilingFrames/Themes/Controls/TilingRoot.xaml b/Qrakhen.TilingFrames/Themes/Controls/TilingRoot.xaml index 6ba5fe4..a9e3c13 100644 --- a/Qrakhen.TilingFrames/Themes/Controls/TilingRoot.xaml +++ b/Qrakhen.TilingFrames/Themes/Controls/TilingRoot.xaml @@ -1,10 +1,8 @@  - - + xmlns:converters="clr-namespace:CopaData.FileInspector.GUI.TilingPanels.Converters" + xmlns:local="clr-namespace:CopaData.FileInspector.GUI.TilingPanels.Controls" + xmlns:models="clr-namespace:CopaData.FileInspector.GUI.TilingPanels.Models">