-
Notifications
You must be signed in to change notification settings - Fork 0
/
AnchorPointPanel.cs
280 lines (233 loc) · 11.5 KB
/
AnchorPointPanel.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
//-----------------------------------------------------------------------
// <copyright>
// Created by Matt Weber <matt@badecho.com>
// Copyright @ 2024 Bad Echo LLC. All rights reserved.
//
// Bad Echo Technologies are licensed under the
// GNU Affero General Public License v3.0.
//
// See accompanying file LICENSE.md or a copy at:
// https://www.gnu.org/licenses/agpl-3.0.html
// </copyright>
//-----------------------------------------------------------------------
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Threading;
using BadEcho.Presentation;
using BadEcho.Presentation.Extensions;
using BadEcho.Configuration;
using BadEcho.Extensibility.Hosting;
using BadEcho.Vision.Extensibility;
namespace BadEcho.Vision.Controls;
/// <summary>
/// Provides an area where you can attach child elements to specific anchor points encompassing the area.
/// </summary>
public sealed class AnchorPointPanel : Panel
{
private VisionConfiguration _configuration;
/// <summary>
/// Identifies the attached property indicating the anchor point of a child element within a parent
/// <see cref="AnchorPointPanel"/>.
/// </summary>
public static readonly DependencyProperty LocationProperty
= DependencyProperty.RegisterAttached(NameOf.ReadDependencyPropertyName(() => LocationProperty),
typeof(AnchorPointLocation),
typeof(AnchorPointPanel),
new FrameworkPropertyMetadata(AnchorPointLocation.TopLeft,
OnAnchorPointChanged),
OnValidateAnchorPoint);
/// <summary>
/// Initializes a new instance of the <see cref="AnchorPointPanel"/> class.
/// </summary>
public AnchorPointPanel()
{
IConfigurationProvider configurationProvider
= PluginHost.LoadFromProcess<IConfigurationProvider>();
configurationProvider.ConfigurationChanged += HandleConfigurationChanged;
_configuration = configurationProvider.GetConfiguration<VisionConfiguration>();
}
/// <summary>
/// Gets the value of the <see cref="LocationProperty"/> attached property for a given <see cref="UIElement"/>.
/// </summary>
/// <param name="element">The element from which the property value is read.</param>
/// <returns>The anchor point that <c>element</c> is attached to within a parent <see cref="AnchorPointPanel"/>.</returns>
public static AnchorPointLocation GetLocation(UIElement element)
{
Require.NotNull(element, nameof(element));
return (AnchorPointLocation) element.GetValue(LocationProperty);
}
/// <summary>
/// Sets the value of the <see cref="LocationProperty"/> attached property on a given <see cref="UIElement"/>.
/// </summary>
/// <param name="element">The element to which the attached property is written.</param>
/// <param name="anchorPoint">The anchor point location to set.</param>
public static void SetLocation(UIElement element, AnchorPointLocation anchorPoint)
{
Require.NotNull(element, nameof(element));
element.SetValue(LocationProperty, anchorPoint);
}
/// <inheritdoc/>
/// <remarks>
/// Currently, elements are measured out so that they are allowed all the size that they desire. As Vision evolves and more
/// modules are added to it, we'll be able to reevaluate this so the best approach can be decided on as far as how modules
/// that are requesting more size than is available are handled.
/// </remarks>
protected override Size MeasureOverride(Size availableSize)
{
var panelSize = new Size();
var elements = InternalChildren.OfType<UIElement>()
.ToList();
var leftElements
= GetElementsAtLocations(AnchorPointLocation.TopLeft, AnchorPointLocation.BottomLeft);
var leftColumnSize
= MeasureColumnedAnchorPoints(leftElements, availableSize);
var centerElements
= GetElementsAtLocations(AnchorPointLocation.TopCenter, AnchorPointLocation.BottomCenter);
var centerColumnSize
= MeasureColumnedAnchorPoints(centerElements, availableSize);
var rightElements
= GetElementsAtLocations(AnchorPointLocation.TopRight, AnchorPointLocation.BottomRight);
var rightColumnSize
= MeasureColumnedAnchorPoints(rightElements, availableSize);
panelSize.Height = Math.Max(Math.Max(leftColumnSize.Height, centerColumnSize.Height),
rightColumnSize.Height);
panelSize.Width = leftColumnSize.Width + centerColumnSize.Width + rightColumnSize.Width;
return panelSize;
IEnumerable<UIElement> GetElementsAtLocations(AnchorPointLocation topLocation, AnchorPointLocation bottomLocation)
{
return elements
.Select(e => (Element: e, Location: GetLocation(e)))
.Where(el => el.Location == topLocation || el.Location == bottomLocation)
.Select(el => el.Element)
.ToList();
}
}
/// <inheritdoc/>
/// <remarks>
/// <para>
/// Elements attached to anchor points are arranged into three separate columns, corresponding to the anchor points
/// located at the left, center, and right of the panel area. Elements attached to the same anchor point are laid out in
/// order, from top to bottom.
/// </para>
/// <para>
/// Elements attached to the top variety of anchor are arranged so that the topmost element attached to the anchor point
/// begin at the top of the panel area. Elements attached to the bottom variety of anchor points are arranged so that the
/// bottommost element attached to the anchor point ends at the bottom of the panel area.
/// </para>
/// </remarks>
protected override Size ArrangeOverride(Size finalSize)
{
var elements = InternalChildren.OfType<UIElement>()
.ToList();
var topLeftElements
= GetElementsAtLocation(AnchorPointLocation.TopLeft);
var bottomLeftElements
= GetElementsAtLocation(AnchorPointLocation.BottomLeft);
var topCenterElements
= GetElementsAtLocation(AnchorPointLocation.TopCenter);
var bottomCenterElements
= GetElementsAtLocation(AnchorPointLocation.BottomCenter);
var topRightElements
= GetElementsAtLocation(AnchorPointLocation.TopRight);
var bottomRightElements
= GetElementsAtLocation(AnchorPointLocation.BottomRight);
ArrangeColumnedAnchorPoints(topLeftElements,
bottomLeftElements,
0,
finalSize.Height,
_configuration.LeftAnchorMargin);
ArrangeColumnedAnchorPoints(topCenterElements,
bottomCenterElements,
finalSize.Width / 2,
finalSize.Height,
_configuration.CenterAnchorMargin,
GetXOffsetForCenter);
ArrangeColumnedAnchorPoints(topRightElements,
bottomRightElements,
finalSize.Width,
finalSize.Height,
_configuration.RightAnchorMargin,
GetXOffsetForRight);
return finalSize;
static double GetXOffsetForCenter(UIElement element)
{
return element.DesiredSize.Width / 2;
}
static double GetXOffsetForRight(UIElement element)
{
return element.DesiredSize.Width;
}
IReadOnlyCollection<UIElement> GetElementsAtLocation(AnchorPointLocation location)
{
return elements.Where(e => GetLocation(e) == location)
.ToList();
}
}
private static Size MeasureColumnedAnchorPoints(IEnumerable<UIElement> columnedElements, Size availableSize)
{
var columnSize = new Size();
foreach (UIElement element in columnedElements)
{
element.Measure(availableSize);
columnSize.Height += element.DesiredSize.Height;
columnSize.Width = Math.Max(element.DesiredSize.Width, columnSize.Width);
}
return columnSize;
}
private static void ArrangeColumnedAnchorPoints(IReadOnlyCollection<UIElement> topElements,
IReadOnlyCollection<UIElement> bottomElements,
double startingX,
double panelHeight,
Thickness columnMargin,
Func<UIElement, double>? xOffsetForElement = null)
{
double yMargin = columnMargin.Top;
startingX += columnMargin.Left - columnMargin.Right;
ArrangeAnchorPoint(topElements, startingX, yMargin, xOffsetForElement);
double topHeight = topElements.Sum(e => e.DesiredSize.Height);
double bottomHeight = bottomElements.Sum(e => e.DesiredSize.Height);
double startingY = Math.Max(topHeight, panelHeight - bottomHeight - columnMargin.Bottom);
ArrangeAnchorPoint(bottomElements, startingX, startingY, xOffsetForElement);
}
private static void ArrangeAnchorPoint(IEnumerable<UIElement> elements,
double startingX,
double startingY,
Func<UIElement, double>? xOffsetForElement = null)
{
foreach (var element in elements)
{
double effectiveX = startingX;
if (xOffsetForElement != null)
effectiveX -= xOffsetForElement(element);
var elementRect = new Rect(effectiveX, startingY, element.DesiredSize.Width, element.DesiredSize.Height);
element.Arrange(elementRect);
startingY += element.DesiredSize.Height;
}
}
private static void OnAnchorPointChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
UIElement element = (UIElement) sender;
var panel = VisualTreeHelper.GetParent(element) as UIElement;
panel?.InvalidateMeasure();
}
private static bool OnValidateAnchorPoint(object value)
{
AnchorPointLocation location = (AnchorPointLocation)value;
return location
is AnchorPointLocation.TopLeft
or AnchorPointLocation.BottomLeft
or AnchorPointLocation.TopCenter
or AnchorPointLocation.BottomCenter
or AnchorPointLocation.TopRight
or AnchorPointLocation.BottomRight;
}
private void HandleConfigurationChanged(object? sender, EventArgs e)
{
if (sender is not IConfigurationProvider configurationProvider)
return;
_configuration = configurationProvider.GetConfiguration<VisionConfiguration>();
// We can expect the configuration change events to often originate on non-UI threads.
this.Invoke(InvalidateMeasure, DispatcherPriority.Normal);
}
}