// 11 june 2016 #import "uipriv_darwin.h" // TODO the assorted test doesn't work right at all @interface gridChild : NSView @property uiControl *c; @property int left; @property int top; @property int xspan; @property int yspan; @property int hexpand; @property uiAlign halign; @property int vexpand; @property uiAlign valign; @property (strong) NSLayoutConstraint *leadingc; @property (strong) NSLayoutConstraint *topc; @property (strong) NSLayoutConstraint *trailingc; @property (strong) NSLayoutConstraint *bottomc; @property (strong) NSLayoutConstraint *xcenterc; @property (strong) NSLayoutConstraint *ycenterc; @property NSLayoutPriority oldHorzHuggingPri; @property NSLayoutPriority oldVertHuggingPri; - (void)setC:(uiControl *)c grid:(uiGrid *)g; - (void)onDestroy; - (NSView *)view; @end @interface gridView : NSView { uiGrid *g; NSMutableArray *children; int padded; NSMutableArray *edges; NSMutableArray *inBetweens; NSMutableArray *emptyCellViews; } - (id)initWithG:(uiGrid *)gg; - (void)onDestroy; - (void)removeOurConstraints; - (void)syncEnableStates:(int)enabled; - (CGFloat)paddingAmount; - (void)establishOurConstraints; - (void)append:(gridChild *)gc; - (void)insert:(gridChild *)gc after:(uiControl *)c at:(uiAt)at; - (int)isPadded; - (void)setPadded:(int)p; - (BOOL)hugsTrailing; - (BOOL)hugsBottom; - (int)nhexpand; - (int)nvexpand; @end struct uiGrid { uiDarwinControl c; gridView *view; }; @implementation gridChild - (void)setC:(uiControl *)c grid:(uiGrid *)g { self.c = c; self.oldHorzHuggingPri = uiDarwinControlHuggingPriority(uiDarwinControl(self.c), NSLayoutConstraintOrientationHorizontal); self.oldVertHuggingPri = uiDarwinControlHuggingPriority(uiDarwinControl(self.c), NSLayoutConstraintOrientationVertical); uiControlSetParent(self.c, uiControl(g)); uiDarwinControlSetSuperview(uiDarwinControl(self.c), self); uiDarwinControlSyncEnableState(uiDarwinControl(self.c), uiControlEnabledToUser(uiControl(g))); if (self.halign == uiAlignStart || self.halign == uiAlignFill) { self.leadingc = uiprivMkConstraint(self, NSLayoutAttributeLeading, NSLayoutRelationEqual, [self view], NSLayoutAttributeLeading, 1, 0, @"uiGrid child horizontal alignment start constraint"); [self addConstraint:self.leadingc]; } if (self.halign == uiAlignCenter) { self.xcenterc = uiprivMkConstraint(self, NSLayoutAttributeCenterX, NSLayoutRelationEqual, [self view], NSLayoutAttributeCenterX, 1, 0, @"uiGrid child horizontal alignment center constraint"); [self addConstraint:self.xcenterc]; } if (self.halign == uiAlignEnd || self.halign == uiAlignFill) { self.trailingc = uiprivMkConstraint(self, NSLayoutAttributeTrailing, NSLayoutRelationEqual, [self view], NSLayoutAttributeTrailing, 1, 0, @"uiGrid child horizontal alignment end constraint"); [self addConstraint:self.trailingc]; } if (self.valign == uiAlignStart || self.valign == uiAlignFill) { self.topc = uiprivMkConstraint(self, NSLayoutAttributeTop, NSLayoutRelationEqual, [self view], NSLayoutAttributeTop, 1, 0, @"uiGrid child vertical alignment start constraint"); [self addConstraint:self.topc]; } if (self.valign == uiAlignCenter) { self.ycenterc = uiprivMkConstraint(self, NSLayoutAttributeCenterY, NSLayoutRelationEqual, [self view], NSLayoutAttributeCenterY, 1, 0, @"uiGrid child vertical alignment center constraint"); [self addConstraint:self.ycenterc]; } if (self.valign == uiAlignEnd || self.valign == uiAlignFill) { self.bottomc = uiprivMkConstraint(self, NSLayoutAttributeBottom, NSLayoutRelationEqual, [self view], NSLayoutAttributeBottom, 1, 0, @"uiGrid child vertical alignment end constraint"); [self addConstraint:self.bottomc]; } } - (void)onDestroy { if (self.leadingc != nil) { [self removeConstraint:self.leadingc]; self.leadingc = nil; } if (self.topc != nil) { [self removeConstraint:self.topc]; self.topc = nil; } if (self.trailingc != nil) { [self removeConstraint:self.trailingc]; self.trailingc = nil; } if (self.bottomc != nil) { [self removeConstraint:self.bottomc]; self.bottomc = nil; } if (self.xcenterc != nil) { [self removeConstraint:self.xcenterc]; self.xcenterc = nil; } if (self.ycenterc != nil) { [self removeConstraint:self.ycenterc]; self.ycenterc = nil; } uiControlSetParent(self.c, NULL); uiDarwinControlSetSuperview(uiDarwinControl(self.c), nil); uiDarwinControlSetHuggingPriority(uiDarwinControl(self.c), self.oldHorzHuggingPri, NSLayoutConstraintOrientationHorizontal); uiDarwinControlSetHuggingPriority(uiDarwinControl(self.c), self.oldVertHuggingPri, NSLayoutConstraintOrientationVertical); } - (NSView *)view { return (NSView *) uiControlHandle(self.c); } @end @implementation gridView - (id)initWithG:(uiGrid *)gg { self = [super initWithFrame:NSZeroRect]; if (self != nil) { self->g = gg; self->padded = 0; self->children = [NSMutableArray new]; self->edges = [NSMutableArray new]; self->inBetweens = [NSMutableArray new]; self->emptyCellViews = [NSMutableArray new]; } return self; } - (void)onDestroy { gridChild *gc; [self removeOurConstraints]; [self->edges release]; [self->inBetweens release]; [self->emptyCellViews release]; for (gc in self->children) { [gc onDestroy]; uiControlDestroy(gc.c); [gc removeFromSuperview]; } [self->children release]; } - (void)removeOurConstraints { NSView *v; if ([self->edges count] != 0) { [self removeConstraints:self->edges]; [self->edges removeAllObjects]; } if ([self->inBetweens count] != 0) { [self removeConstraints:self->inBetweens]; [self->inBetweens removeAllObjects]; } for (v in self->emptyCellViews) [v removeFromSuperview]; [self->emptyCellViews removeAllObjects]; } - (void)syncEnableStates:(int)enabled { gridChild *gc; for (gc in self->children) uiDarwinControlSyncEnableState(uiDarwinControl(gc.c), enabled); } - (CGFloat)paddingAmount { if (!self->padded) return 0.0; return uiDarwinPaddingAmount(NULL); } // LONGTERM stop early if all controls are hidden - (void)establishOurConstraints { gridChild *gc; CGFloat padding; int xmin, ymin; int xmax, ymax; int xcount, ycount; BOOL first; int **gg; NSView ***gv; BOOL **gspan; int x, y; int i; NSLayoutConstraint *c; int firstx, firsty; BOOL *hexpand, *vexpand; BOOL doit; BOOL onlyEmptyAndSpanning; [self removeOurConstraints]; if ([self->children count] == 0) return; padding = [self paddingAmount]; // first, figure out the minimum and maximum row and column numbers // ignore hidden controls first = YES; for (gc in self->children) { // this bit is important: it ensures row ymin and column xmin have at least one cell to draw, so the onlyEmptyAndSpanning logic below will never run on those rows if (!uiControlVisible(gc.c)) continue; if (first) { xmin = gc.left; ymin = gc.top; xmax = gc.left + gc.xspan; ymax = gc.top + gc.yspan; first = NO; continue; } if (xmin > gc.left) xmin = gc.left; if (ymin > gc.top) ymin = gc.top; if (xmax < (gc.left + gc.xspan)) xmax = gc.left + gc.xspan; if (ymax < (gc.top + gc.yspan)) ymax = gc.top + gc.yspan; } if (first != NO) // the entire grid is hidden; do nothing return; xcount = xmax - xmin; ycount = ymax - ymin; // now build a topological map of the grid gg[y][x] // also figure out which cells contain spanned views so they can be ignored later // treat hidden controls by keeping the indices -1 gg = (int **) uiprivAlloc(ycount * sizeof (int *), "int[][]"); gspan = (BOOL **) uiprivAlloc(ycount * sizeof (BOOL *), "BOOL[][]"); for (y = 0; y < ycount; y++) { gg[y] = (int *) uiprivAlloc(xcount * sizeof (int), "int[]"); gspan[y] = (BOOL *) uiprivAlloc(xcount * sizeof (BOOL), "BOOL[]"); for (x = 0; x < xcount; x++) gg[y][x] = -1; // empty } for (i = 0; i < [self->children count]; i++) { gc = (gridChild *) [self->children objectAtIndex:i]; if (!uiControlVisible(gc.c)) continue; for (y = gc.top; y < gc.top + gc.yspan; y++) for (x = gc.left; x < gc.left + gc.xspan; x++) { gg[y - ymin][x - xmin] = i; if (x != gc.left || y != gc.top) gspan[y - ymin][x - xmin] = YES; } } // if a row or column only contains emptys and spanning cells of a opposite-direction spannings, remove it by duplicating the previous row or column for (y = 0; y < ycount; y++) { onlyEmptyAndSpanning = YES; for (x = 0; x < xcount; x++) if (gg[y][x] != -1) { gc = (gridChild *) [self->children objectAtIndex:gg[y][x]]; if (gc.yspan == 1 || gc.top - ymin == y) { onlyEmptyAndSpanning = NO; break; } } if (onlyEmptyAndSpanning) for (x = 0; x < xcount; x++) { gg[y][x] = gg[y - 1][x]; gspan[y][x] = YES; } } for (x = 0; x < xcount; x++) { onlyEmptyAndSpanning = YES; for (y = 0; y < ycount; y++) if (gg[y][x] != -1) { gc = (gridChild *) [self->children objectAtIndex:gg[y][x]]; if (gc.xspan == 1 || gc.left - xmin == x) { onlyEmptyAndSpanning = NO; break; } } if (onlyEmptyAndSpanning) for (y = 0; y < ycount; y++) { gg[y][x] = gg[y][x - 1]; gspan[y][x] = YES; } } // now build a topological map of the grid's views gv[y][x] // for any empty cell, create a dummy view gv = (NSView ***) uiprivAlloc(ycount * sizeof (NSView **), "NSView *[][]"); for (y = 0; y < ycount; y++) { gv[y] = (NSView **) uiprivAlloc(xcount * sizeof (NSView *), "NSView *[]"); for (x = 0; x < xcount; x++) if (gg[y][x] == -1) { gv[y][x] = [[NSView alloc] initWithFrame:NSZeroRect]; [gv[y][x] setTranslatesAutoresizingMaskIntoConstraints:NO]; [self addSubview:gv[y][x]]; [self->emptyCellViews addObject:gv[y][x]]; } else { gc = (gridChild *) [self->children objectAtIndex:gg[y][x]]; gv[y][x] = gc; } } // now figure out which rows and columns really expand hexpand = (BOOL *) uiprivAlloc(xcount * sizeof (BOOL), "BOOL[]"); vexpand = (BOOL *) uiprivAlloc(ycount * sizeof (BOOL), "BOOL[]"); // first, which don't span for (gc in self->children) { if (!uiControlVisible(gc.c)) continue; if (gc.hexpand && gc.xspan == 1) hexpand[gc.left - xmin] = YES; if (gc.vexpand && gc.yspan == 1) vexpand[gc.top - ymin] = YES; } // second, which do span // the way we handle this is simple: if none of the spanned rows/columns expand, make all rows/columns expand for (gc in self->children) { if (!uiControlVisible(gc.c)) continue; if (gc.hexpand && gc.xspan != 1) { doit = YES; for (x = gc.left; x < gc.left + gc.xspan; x++) if (hexpand[x - xmin]) { doit = NO; break; } if (doit) for (x = gc.left; x < gc.left + gc.xspan; x++) hexpand[x - xmin] = YES; } if (gc.vexpand && gc.yspan != 1) { doit = YES; for (y = gc.top; y < gc.top + gc.yspan; y++) if (vexpand[y - ymin]) { doit = NO; break; } if (doit) for (y = gc.top; y < gc.top + gc.yspan; y++) vexpand[y - ymin] = YES; } } // now establish all the edge constraints // leading and trailing edges for (y = 0; y < ycount; y++) { c = uiprivMkConstraint(self, NSLayoutAttributeLeading, NSLayoutRelationEqual, gv[y][0], NSLayoutAttributeLeading, 1, 0, @"uiGrid leading edge constraint"); [self addConstraint:c]; [self->edges addObject:c]; c = uiprivMkConstraint(self, NSLayoutAttributeTrailing, NSLayoutRelationEqual, gv[y][xcount - 1], NSLayoutAttributeTrailing, 1, 0, @"uiGrid trailing edge constraint"); [self addConstraint:c]; [self->edges addObject:c]; } // top and bottom edges for (x = 0; x < xcount; x++) { c = uiprivMkConstraint(self, NSLayoutAttributeTop, NSLayoutRelationEqual, gv[0][x], NSLayoutAttributeTop, 1, 0, @"uiGrid top edge constraint"); [self addConstraint:c]; [self->edges addObject:c]; c = uiprivMkConstraint(self, NSLayoutAttributeBottom, NSLayoutRelationEqual, gv[ycount - 1][x], NSLayoutAttributeBottom, 1, 0, @"uiGrid bottom edge constraint"); [self addConstraint:c]; [self->edges addObject:c]; } // now align leading and top edges // do NOT align spanning cells! for (x = 0; x < xcount; x++) { for (y = 0; y < ycount; y++) if (!gspan[y][x]) break; firsty = y; for (y++; y < ycount; y++) { if (gspan[y][x]) continue; c = uiprivMkConstraint(gv[firsty][x], NSLayoutAttributeLeading, NSLayoutRelationEqual, gv[y][x], NSLayoutAttributeLeading, 1, 0, @"uiGrid column leading constraint"); [self addConstraint:c]; [self->edges addObject:c]; } } for (y = 0; y < ycount; y++) { for (x = 0; x < xcount; x++) if (!gspan[y][x]) break; firstx = x; for (x++; x < xcount; x++) { if (gspan[y][x]) continue; c = uiprivMkConstraint(gv[y][firstx], NSLayoutAttributeTop, NSLayoutRelationEqual, gv[y][x], NSLayoutAttributeTop, 1, 0, @"uiGrid row top constraint"); [self addConstraint:c]; [self->edges addObject:c]; } } // now string adjacent views together for (y = 0; y < ycount; y++) for (x = 1; x < xcount; x++) if (gv[y][x - 1] != gv[y][x]) { c = uiprivMkConstraint(gv[y][x - 1], NSLayoutAttributeTrailing, NSLayoutRelationEqual, gv[y][x], NSLayoutAttributeLeading, 1, -padding, @"uiGrid internal horizontal constraint"); [self addConstraint:c]; [self->inBetweens addObject:c]; } for (x = 0; x < xcount; x++) for (y = 1; y < ycount; y++) if (gv[y - 1][x] != gv[y][x]) { c = uiprivMkConstraint(gv[y - 1][x], NSLayoutAttributeBottom, NSLayoutRelationEqual, gv[y][x], NSLayoutAttributeTop, 1, -padding, @"uiGrid internal vertical constraint"); [self addConstraint:c]; [self->inBetweens addObject:c]; } // now set priorities for all widgets that expand or not // if a cell is in an expanding row, OR If it spans, then it must be willing to stretch // otherwise, it tries not to // note we don't use NSLayoutPriorityRequired as that will cause things to squish when they shouldn't for (gc in self->children) { NSLayoutPriority priority; if (!uiControlVisible(gc.c)) continue; if (hexpand[gc.left - xmin] || gc.xspan != 1) priority = NSLayoutPriorityDefaultLow; else priority = NSLayoutPriorityDefaultHigh; uiDarwinControlSetHuggingPriority(uiDarwinControl(gc.c), priority, NSLayoutConstraintOrientationHorizontal); // same for vertical direction if (vexpand[gc.top - ymin] || gc.yspan != 1) priority = NSLayoutPriorityDefaultLow; else priority = NSLayoutPriorityDefaultHigh; uiDarwinControlSetHuggingPriority(uiDarwinControl(gc.c), priority, NSLayoutConstraintOrientationVertical); } // TODO make all expanding rows/columns the same height/width // and finally clean up uiprivFree(hexpand); uiprivFree(vexpand); for (y = 0; y < ycount; y++) { uiprivFree(gg[y]); uiprivFree(gv[y]); uiprivFree(gspan[y]); } uiprivFree(gg); uiprivFree(gv); uiprivFree(gspan); } - (void)append:(gridChild *)gc { BOOL update; int oldnh, oldnv; [gc setTranslatesAutoresizingMaskIntoConstraints:NO]; [self addSubview:gc]; // no need to set priority here; that's done in establishOurConstraints oldnh = [self nhexpand]; oldnv = [self nvexpand]; [self->children addObject:gc]; [self establishOurConstraints]; update = NO; if (gc.hexpand) if (oldnh == 0) update = YES; if (gc.vexpand) if (oldnv == 0) update = YES; if (update) uiDarwinNotifyEdgeHuggingChanged(uiDarwinControl(self->g)); [gc release]; // we don't need the initial reference now } - (void)insert:(gridChild *)gc after:(uiControl *)c at:(uiAt)at { gridChild *other; BOOL found; found = NO; for (other in self->children) if (other.c == c) { found = YES; break; } if (!found) uiprivUserBug("Existing control %p is not in grid %p; you cannot add other controls next to it", c, self->g); switch (at) { case uiAtLeading: gc.left = other.left - gc.xspan; gc.top = other.top; break; case uiAtTop: gc.left = other.left; gc.top = other.top - gc.yspan; break; case uiAtTrailing: gc.left = other.left + other.xspan; gc.top = other.top; break; case uiAtBottom: gc.left = other.left; gc.top = other.top + other.yspan; break; // TODO add error checks to ALL enums } [self append:gc]; } - (int)isPadded { return self->padded; } - (void)setPadded:(int)p { CGFloat padding; NSLayoutConstraint *c; #if 0 /* TODO */ dispatch_after( dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ [[self window] visualizeConstraints:[self constraints]]; } ); #endif self->padded = p; padding = [self paddingAmount]; for (c in self->inBetweens) switch ([c firstAttribute]) { case NSLayoutAttributeLeading: case NSLayoutAttributeTop: [c setConstant:padding]; break; case NSLayoutAttributeTrailing: case NSLayoutAttributeBottom: [c setConstant:-padding]; break; } } - (BOOL)hugsTrailing { // only hug if we have horizontally expanding return [self nhexpand] != 0; } - (BOOL)hugsBottom { // only hug if we have vertically expanding return [self nvexpand] != 0; } - (int)nhexpand { gridChild *gc; int n; n = 0; for (gc in self->children) { if (!uiControlVisible(gc.c)) continue; if (gc.hexpand) n++; } return n; } - (int)nvexpand { gridChild *gc; int n; n = 0; for (gc in self->children) { if (!uiControlVisible(gc.c)) continue; if (gc.vexpand) n++; } return n; } @end static void uiGridDestroy(uiControl *c) { uiGrid *g = uiGrid(c); [g->view onDestroy]; [g->view release]; uiFreeControl(uiControl(g)); } uiDarwinControlDefaultHandle(uiGrid, view) uiDarwinControlDefaultParent(uiGrid, view) uiDarwinControlDefaultSetParent(uiGrid, view) uiDarwinControlDefaultToplevel(uiGrid, view) uiDarwinControlDefaultVisible(uiGrid, view) uiDarwinControlDefaultShow(uiGrid, view) uiDarwinControlDefaultHide(uiGrid, view) uiDarwinControlDefaultEnabled(uiGrid, view) uiDarwinControlDefaultEnable(uiGrid, view) uiDarwinControlDefaultDisable(uiGrid, view) static void uiGridSyncEnableState(uiDarwinControl *c, int enabled) { uiGrid *g = uiGrid(c); if (uiDarwinShouldStopSyncEnableState(uiDarwinControl(g), enabled)) return; [g->view syncEnableStates:enabled]; } uiDarwinControlDefaultSetSuperview(uiGrid, view) static BOOL uiGridHugsTrailingEdge(uiDarwinControl *c) { uiGrid *g = uiGrid(c); return [g->view hugsTrailing]; } static BOOL uiGridHugsBottom(uiDarwinControl *c) { uiGrid *g = uiGrid(c); return [g->view hugsBottom]; } static void uiGridChildEdgeHuggingChanged(uiDarwinControl *c) { uiGrid *g = uiGrid(c); [g->view establishOurConstraints]; } uiDarwinControlDefaultHuggingPriority(uiGrid, view) uiDarwinControlDefaultSetHuggingPriority(uiGrid, view) static void uiGridChildVisibilityChanged(uiDarwinControl *c) { uiGrid *g = uiGrid(c); [g->view establishOurConstraints]; } static gridChild *toChild(uiControl *c, int xspan, int yspan, int hexpand, uiAlign halign, int vexpand, uiAlign valign, uiGrid *g) { gridChild *gc; if (xspan < 0) uiprivUserBug("You cannot have a negative xspan in a uiGrid cell."); if (yspan < 0) uiprivUserBug("You cannot have a negative yspan in a uiGrid cell."); gc = [gridChild new]; gc.xspan = xspan; gc.yspan = yspan; gc.hexpand = hexpand; gc.halign = halign; gc.vexpand = vexpand; gc.valign = valign; [gc setC:c grid:g]; return gc; } void uiGridAppend(uiGrid *g, uiControl *c, int left, int top, int xspan, int yspan, int hexpand, uiAlign halign, int vexpand, uiAlign valign) { gridChild *gc; // LONGTERM on other platforms // or at leat allow this and implicitly turn it into a spacer if (c == NULL) uiprivUserBug("You cannot add NULL to a uiGrid."); gc = toChild(c, xspan, yspan, hexpand, halign, vexpand, valign, g); gc.left = left; gc.top = top; [g->view append:gc]; } void uiGridInsertAt(uiGrid *g, uiControl *c, uiControl *existing, uiAt at, int xspan, int yspan, int hexpand, uiAlign halign, int vexpand, uiAlign valign) { gridChild *gc; gc = toChild(c, xspan, yspan, hexpand, halign, vexpand, valign, g); [g->view insert:gc after:existing at:at]; } int uiGridPadded(uiGrid *g) { return [g->view isPadded]; } void uiGridSetPadded(uiGrid *g, int padded) { [g->view setPadded:padded]; } uiGrid *uiNewGrid(void) { uiGrid *g; uiDarwinNewControl(uiGrid, g); g->view = [[gridView alloc] initWithG:g]; return g; }