5
// Lluis Sanchez <lluis@xamarin.com>
7
// Copyright (c) 2011 Xamarin Inc
9
// Permission is hereby granted, free of charge, to any person obtaining a copy
10
// of this software and associated documentation files (the "Software"), to deal
11
// in the Software without restriction, including without limitation the rights
12
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
// copies of the Software, and to permit persons to whom the Software is
14
// furnished to do so, subject to the following conditions:
16
// The above copyright notice and this permission notice shall be included in
17
// all copies or substantial portions of the Software.
19
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
29
using System.Collections.Generic;
30
using System.ComponentModel;
33
using System.Windows.Markup;
38
public class Table: Widget
40
ChildrenCollection<TablePlacement> children;
41
double defaultRowSpacing = 6;
42
double defaultColSpacing = 6;
43
Dictionary<int,double> rowSpacing;
44
Dictionary<int,double> colSpacing;
46
protected new class WidgetBackendHost: Widget.WidgetBackendHost, ICollectionEventSink<TablePlacement>, IContainerEventSink<TablePlacement>
48
public void AddedItem (TablePlacement item, int index)
50
((Table)Parent).OnAdd (item.Child, item);
53
public void RemovedItem (TablePlacement item, int index)
55
((Table)Parent).OnRemove (item.Child);
58
public void ChildChanged (TablePlacement child, string hint)
60
((Table)Parent).OnChildChanged (child, hint);
63
public void ChildReplaced (TablePlacement child, Widget oldWidget, Widget newWidget)
65
((Table)Parent).OnReplaceChild (child, oldWidget, newWidget);
69
protected override BackendHost CreateBackendHost ()
71
return new WidgetBackendHost ();
75
get { return (IBoxBackend) BackendHost.Backend; }
80
children = new ChildrenCollection<TablePlacement> ((WidgetBackendHost)BackendHost);
81
// For some reason the table has a black background by default in WPF. Lets work around that
82
BackgroundColor = Colors.White;
86
public double DefaultRowSpacing {
87
get { return defaultRowSpacing; }
88
set { defaultRowSpacing = value; OnPreferredSizeChanged (); }
92
public double DefaultColumnSpacing {
93
get { return defaultColSpacing; }
94
set { defaultColSpacing = value; OnPreferredSizeChanged (); }
97
public void SetRowSpacing (int row, double spacing)
99
if (rowSpacing == null)
100
rowSpacing = new Dictionary<int, double> ();
101
rowSpacing [row] = spacing;
102
OnPreferredSizeChanged ();
105
public void SetColumnSpacing (int col, double spacing)
107
if (colSpacing == null)
108
colSpacing = new Dictionary<int, double> ();
109
colSpacing [col] = spacing;
110
OnPreferredSizeChanged ();
113
public ChildrenCollection<TablePlacement> Placements {
114
get { return children; }
117
public IEnumerable<Widget> Children {
118
get { return children.Select (c => c.Child); }
121
public void Attach (Widget widget, int left, int top)
123
Attach (widget, left, top, AttachOptions.Fill | AttachOptions.Expand, AttachOptions.Fill | AttachOptions.Expand);
126
public void Attach (Widget widget, int left, int top, AttachOptions xOptions, AttachOptions yOptions)
128
Attach (widget, left, left + 1, top, top + 1, xOptions, yOptions);
131
public void Attach (Widget widget, int left, int right, int top, int bottom)
133
Attach (widget, left, right, top, bottom, AttachOptions.Fill | AttachOptions.Expand, AttachOptions.Fill | AttachOptions.Expand);
136
public void Attach (Widget widget, int left, int right, int top, int bottom, AttachOptions xOptions, AttachOptions yOptions)
138
var p = new TablePlacement ((WidgetBackendHost)BackendHost, widget) {
149
public bool Remove (Widget widget)
151
for (int n=0; n<children.Count; n++) {
152
if (children[n].Child == widget) {
153
children.RemoveAt (n);
160
public void InsertRow (int top, int bottom)
162
var potentials = children.Where (c => c.Top >= top);
163
var shift = bottom - top;
164
foreach (var toShift in potentials) {
165
toShift.Top += shift;
166
toShift.Bottom += shift;
171
/// Removes all children
178
void OnAdd (Widget child, TablePlacement placement)
180
RegisterChild (child);
181
Backend.Add ((IWidgetBackend)GetBackend (child));
182
OnPreferredSizeChanged ();
185
void OnRemove (Widget child)
187
UnregisterChild (child);
188
Backend.Remove ((IWidgetBackend)GetBackend (child));
189
OnPreferredSizeChanged ();
192
void OnChildChanged (TablePlacement placement, object hint)
194
OnPreferredSizeChanged ();
197
internal protected virtual void OnReplaceChild (TablePlacement placement, Widget oldWidget, Widget newWidget)
199
if (oldWidget != null)
200
OnRemove (oldWidget);
201
OnAdd (newWidget, placement);
204
protected override void OnReallocate ()
206
var size = Backend.Size;
207
var mode = Surface.SizeRequestMode;
208
if (mode == SizeRequestMode.HeightForWidth) {
209
CalcDefaultSizes (mode, size.Width, false, true);
210
CalcDefaultSizes (mode, size.Height, true, true);
212
CalcDefaultSizes (mode, size.Height, true, true);
213
CalcDefaultSizes (mode, size.Width, false, true);
216
var visibleChildren = children.Where (c => c.Child.Visible).ToArray ();
217
IWidgetBackend[] widgets = new IWidgetBackend [visibleChildren.Length];
218
Rectangle[] rects = new Rectangle [visibleChildren.Length];
219
for (int n=0; n<visibleChildren.Length; n++) {
220
var bp = visibleChildren [n];
221
widgets [n] = (IWidgetBackend)GetBackend (bp.Child);
222
rects [n] = new Rectangle (bp.NextX, bp.NextY, bp.NextWidth, bp.NextHeight);
225
Backend.SetAllocation (widgets, rects);
227
if (!Application.EngineBackend.HandlesSizeNegotiation) {
228
foreach (var bp in visibleChildren)
229
bp.Child.Surface.Reallocate ();
233
double GetSpacing (int cell, bool isRow)
237
if (rowSpacing != null && rowSpacing.TryGetValue (cell, out sp))
240
return defaultRowSpacing;
242
if (colSpacing != null && colSpacing.TryGetValue (cell, out sp))
245
return defaultColSpacing;
249
void CalcDefaultSizes (SizeRequestMode mode, bool calcHeights, out TablePlacement[] visibleChildren, out Dictionary<int,WidgetSize> fixedSizesByCell, out HashSet<int> cellsWithExpand, out WidgetSize[] sizes, out double spacing)
251
bool useLengthConstraint = mode == SizeRequestMode.HeightForWidth && calcHeights || mode == SizeRequestMode.WidthForHeight && !calcHeights;
252
visibleChildren = children.Where (b => b.Child.Visible).ToArray ();
255
fixedSizesByCell = new Dictionary<int, WidgetSize> ();
256
cellsWithExpand = new HashSet<int> ();
257
HashSet<int> cellsWithWidget = new HashSet<int> ();
258
sizes = new WidgetSize [visibleChildren.Length];
260
// Get the size of each widget and store the fixed sizes for widgets which don't span more than one cell
262
for (int n=0; n<visibleChildren.Length; n++) {
263
var bp = visibleChildren[n];
264
int start = GetStartAttach (bp, calcHeights);
265
int end = GetEndAttach (bp, calcHeights);
270
// Check if the cell is expandable and store the value
271
AttachOptions ops = calcHeights ? bp.YOptions : bp.XOptions;
272
for (int i=start; i < end; i++) {
273
cellsWithWidget.Add (i);
274
if ((ops & AttachOptions.Expand) != 0)
275
cellsWithExpand.Add (i);
279
if (useLengthConstraint)
280
s = GetPreferredLengthForSize (mode, bp.Child, calcHeights ? bp.NextWidth : bp.NextHeight);
282
s = GetPreferredSize (calcHeights, bp.Child);
285
if (end == start + 1) {
286
// The widget only takes one cell. Store its size if it is the biggest
287
bool changed = false;
289
fixedSizesByCell.TryGetValue (start, out fs);
290
if (s.MinSize > fs.MinSize) {
291
fs.MinSize = s.MinSize;
294
if (s.NaturalSize > fs.NaturalSize) {
295
fs.NaturalSize = s.NaturalSize;
299
fixedSizesByCell [start] = fs;
303
// For widgets that span more than one cell, calculate the floating size, that is, the size
304
// which is not taken by other fixed size widgets
306
List<TablePlacement> widgetsToAdjust = new List<TablePlacement> ();
307
Dictionary<TablePlacement,WidgetSize[]> growSizes = new Dictionary<TablePlacement, WidgetSize[]> ();
309
for (int n=0; n<visibleChildren.Length; n++) {
310
var bp = visibleChildren[n];
311
int start = GetStartAttach (bp, calcHeights);
312
int end = GetEndAttach (bp, calcHeights);
313
if (end == start + 1)
315
widgetsToAdjust.Add (bp);
317
WidgetSize fixedSize = new WidgetSize (0);
319
// We are going to calculate the spacing included in the widget's span of cells
320
// (there is spacing between each cell)
321
double spanSpacing = 0;
323
for (int c = start; c < end; c++) {
325
fixedSizesByCell.TryGetValue (c, out fs);
327
if (c != start && c != end)
328
spanSpacing += GetSpacing (c, calcHeights);
331
// sizeToGrow is the size that the whole cell span has to grow in order to fit
332
// this widget. We substract the spacing between cells because that space will
333
// be used by the widget, so we don't need to allocate more size for it
335
WidgetSize sizeToGrow = sizes [n] - fixedSize - new WidgetSize (spanSpacing);
337
WidgetSize sizeToGrowPart = new WidgetSize (sizeToGrow.MinSize / (end - start), sizeToGrow.NaturalSize / (end - start));
339
// Split the size to grow between the cells of the widget. We need to know how much size the widget
340
// requires for each cell it covers.
342
WidgetSize[] widgetGrowSizes = new WidgetSize [end - start];
343
for (int i=0; i<widgetGrowSizes.Length; i++)
344
widgetGrowSizes [i] = sizeToGrowPart;
345
growSizes[bp] = widgetGrowSizes;
348
// Now size-to-grow values have to be adjusted. For example, let's say widget A requires 100px for column 1 and 100px for column 2, and widget B requires
349
// 60px for column 2 and 60px for column 3. So the widgets are overlapping at column 2. Since A requires at least 100px in column 2, it means that B can assume
350
// that it will have 100px available in column 2, which means 40px more than it requested. Those extra 40px can then be substracted from the 60px that
351
// it required for column 3.
353
foreach (var n in cellsWithWidget) {
354
// Get a list of all widgets that cover this cell
355
var colCells = widgetsToAdjust.Where (bp => GetStartAttach (bp, calcHeights) <= n && GetEndAttach (bp, calcHeights) > n).ToArray ();
356
WidgetSize maxv = new WidgetSize (0);
357
TablePlacement maxtMin = null;
358
TablePlacement maxtNatural = null;
360
// Find the widget that requires the maximum size for this cell
361
foreach (var bp in colCells) {
362
WidgetSize cv = growSizes[bp][n - GetStartAttach (bp, calcHeights)];
363
if (cv.MinSize > maxv.MinSize) {
364
maxv.MinSize = cv.MinSize;
367
if (cv.NaturalSize > maxv.NaturalSize) {
368
maxv.NaturalSize = cv.NaturalSize;
373
// Adjust the required size of all widgets of the cell (excluding the widget with the max size)
374
foreach (var bp in colCells) {
375
WidgetSize[] widgetGrows = growSizes[bp];
376
int cellIndex = n - GetStartAttach (bp, calcHeights);
378
double cv = widgetGrows[cellIndex].MinSize;
379
// splitExtraSpace is the additional space that the widget can take from this cell (because there is a widget
380
// that is requiring more space), split among all other cells of the widget
381
double splitExtraSpace = (maxv.MinSize - cv) / (widgetGrows.Length - 1);
382
for (int i=0; i<widgetGrows.Length; i++)
383
widgetGrows[i].MinSize -= splitExtraSpace;
385
if (bp != maxtNatural) {
386
double cv = widgetGrows[cellIndex].NaturalSize;
387
double splitExtraSpace = (maxv.NaturalSize - cv) / (widgetGrows.Length - 1);
388
for (int i=0; i<widgetGrows.Length; i++)
389
widgetGrows[i].NaturalSize -= splitExtraSpace;
394
// Find the maximum size-to-grow for each cell
396
Dictionary<int,WidgetSize> finalGrowTable = new Dictionary<int, WidgetSize> ();
398
foreach (var bp in widgetsToAdjust) {
399
int start = GetStartAttach (bp, calcHeights);
400
int end = GetEndAttach (bp, calcHeights);
401
WidgetSize[] widgetGrows = growSizes[bp];
402
for (int n=start; n<end; n++) {
404
finalGrowTable.TryGetValue (n, out curGrow);
405
var val = widgetGrows [n - start];
406
if (val.MinSize > curGrow.MinSize)
407
curGrow.MinSize = val.MinSize;
408
if (val.NaturalSize > curGrow.NaturalSize)
409
curGrow.NaturalSize = val.NaturalSize;
410
finalGrowTable [n] = curGrow;
414
// Add the final size-to-grow to the fixed sizes calculated at the begining
416
foreach (var it in finalGrowTable) {
418
fixedSizesByCell.TryGetValue (it.Key, out ws);
419
fixedSizesByCell [it.Key] = it.Value + ws;
423
for (int n=1; n<lastCell; n++) {
424
if (cellsWithWidget.Contains (n))
425
spacing += GetSpacing (n, calcHeights);
430
void CalcDefaultSizes (SizeRequestMode mode, double totalSize, bool calcHeights, bool calcOffsets)
432
TablePlacement[] visibleChildren;
433
Dictionary<int,WidgetSize> fixedSizesByCell;
434
HashSet<int> cellsWithExpand;
438
CalcDefaultSizes (mode, calcHeights, out visibleChildren, out fixedSizesByCell, out cellsWithExpand, out sizes, out spacing);
440
double naturalSize = 0;
442
// Get the total natural size
443
foreach (var ws in fixedSizesByCell.Values)
444
naturalSize += ws.NaturalSize;
446
double remaining = totalSize - naturalSize - spacing;
449
// The box is not big enough to fit the widgets using its natural size.
450
// We have to shrink the cells
452
// List of cell indexes that we have to shrink
453
var toShrink = new List<int> (fixedSizesByCell.Keys);
455
// The total amount we have to shrink
456
double shrinkSize = -remaining;
458
while (toShrink.Count > 0 && shrinkSize > 0) {
459
SizeSplitter sizePart = new SizeSplitter (shrinkSize, toShrink.Count);
461
for (int i=0; i < toShrink.Count; i++) {
463
double reduction = sizePart.NextSizePart ();
465
fixedSizesByCell.TryGetValue (n, out size);
466
size.NaturalSize -= reduction;
468
if (size.NaturalSize < size.MinSize) {
469
// If the widget can't be shrinked anymore, we remove it from the shrink list
470
// and increment the remaining shrink size. We'll loop again and this size will be
471
// substracted from the cells which can still be reduced
472
shrinkSize += (size.MinSize - size.NaturalSize);
473
size.NaturalSize = size.MinSize;
474
toShrink.RemoveAt (i);
477
fixedSizesByCell [n] = size;
482
int nexpands = cellsWithExpand.Count;
483
var expandRemaining = new SizeSplitter (remaining, nexpands);
484
foreach (var c in cellsWithExpand) {
486
fixedSizesByCell.TryGetValue (c, out ws);
487
ws.NaturalSize += expandRemaining.NextSizePart ();
488
fixedSizesByCell [c] = ws;
492
for (int n=0; n<visibleChildren.Length; n++) {
493
var bp = visibleChildren[n];
494
double allocatedSize = 0;
495
double cellOffset = 0;
496
AttachOptions ops = calcHeights ? bp.YOptions : bp.XOptions;
498
int start = GetStartAttach (bp, calcHeights);
499
int end = GetEndAttach (bp, calcHeights);
500
for (int i=start; i<end; i++) {
502
fixedSizesByCell.TryGetValue (i, out ws);
503
allocatedSize += ws.NaturalSize;
505
allocatedSize += GetSpacing (i, calcHeights);
508
if ((ops & AttachOptions.Fill) == 0) {
509
double s = sizes[n].NaturalSize;
510
if (s < allocatedSize) {
511
cellOffset = (allocatedSize - s) / 2;
516
// cellOffset is the offset of the widget inside the cell. We store it in NextX/Y, and
517
// will be used below to calculate the total offset of the widget
520
bp.NextHeight = allocatedSize;
521
bp.NextY = cellOffset;
524
bp.NextWidth = allocatedSize;
525
bp.NextX = cellOffset;
530
var sortedChildren = visibleChildren.OrderBy (c => GetStartAttach (c, calcHeights)).ToArray();
531
var cells = fixedSizesByCell.OrderBy (c => c.Key);
534
foreach (var c in cells) {
536
offset += GetSpacing (c.Key, calcHeights);
537
while (n < sortedChildren.Length && GetStartAttach (sortedChildren[n], calcHeights) == c.Key) {
538
// In the loop above we store the offset of the widget inside the cell in the NextX/Y field
539
// so now we have to add (not just assign) the offset of the cell to NextX/Y
541
sortedChildren[n].NextY += offset;
543
sortedChildren[n].NextX += offset;
546
offset += c.Value.NaturalSize;
551
WidgetSize CalcSize (bool calcHeights)
553
TablePlacement[] visibleChildren;
554
Dictionary<int,WidgetSize> fixedSizesByCell;
555
HashSet<int> cellsWithExpand;
558
SizeRequestMode mode = calcHeights ? SizeRequestMode.WidthForHeight : SizeRequestMode.HeightForWidth;
560
CalcDefaultSizes (mode, calcHeights, out visibleChildren, out fixedSizesByCell, out cellsWithExpand, out sizes, out spacing);
562
WidgetSize size = new WidgetSize (spacing);
563
foreach (var s in fixedSizesByCell.Values)
568
protected override WidgetSize OnGetPreferredWidth ()
570
return CalcSize (false);
573
protected override WidgetSize OnGetPreferredHeight ()
575
return CalcSize (true);
578
protected override WidgetSize OnGetPreferredHeightForWidth (double width)
580
CalcDefaultSizes (SizeRequestMode.HeightForWidth, width, false, false);
581
return CalcSize (true);
584
protected override WidgetSize OnGetPreferredWidthForHeight (double height)
586
CalcDefaultSizes (SizeRequestMode.WidthForHeight, height, true, false);
587
return CalcSize (false);
590
int GetStartAttach (TablePlacement tp, bool calcHeight)
598
int GetEndAttach (TablePlacement tp, bool calcHeight)
606
WidgetSize GetPreferredSize (bool calcHeight, Widget w)
609
return w.Surface.GetPreferredHeight ();
611
return w.Surface.GetPreferredWidth ();
614
WidgetSize GetPreferredLengthForSize (SizeRequestMode mode, Widget w, double width)
616
if (mode == SizeRequestMode.WidthForHeight)
617
return w.Surface.GetPreferredWidthForHeight (width);
619
return w.Surface.GetPreferredHeightForWidth (width);
623
[ContentProperty("Child")]
624
public class TablePlacement
626
IContainerEventSink<TablePlacement> parent;
627
int left, right, top, bottom;
628
AttachOptions xOptions, yOptions;
631
internal TablePlacement (IContainerEventSink<TablePlacement> parent, Widget child)
633
this.parent = parent;
637
internal double NextWidth;
638
internal double NextHeight;
639
internal double NextX;
640
internal double NextY;
648
parent.ChildChanged (this, "Left");
658
parent.ChildChanged (this, "Right");
668
parent.ChildChanged (this, "Top");
678
parent.ChildChanged (this, "Bottom");
682
public Widget Child {
683
get { return child; }
687
parent.ChildReplaced (this, old, value);
691
public AttachOptions XOptions {
692
get { return xOptions; }
695
parent.ChildChanged (this, "XOptions");
699
public AttachOptions YOptions {
700
get { return yOptions; }
703
parent.ChildChanged (this, "YOptions");
709
public enum AttachOptions