libui/darwin/table.m

583 lines
15 KiB
Objective-C

// 21 june 2016
#import "uipriv_darwin.h"
// TODOs
// - initial state of table view is off
// - header cell seems off
// - background color shows up for a line or two below selection
// - editable NSTextFields have no intrinsic width
// - changing a part property does not refresh views
// - is the Y position of checkbox cells correct?
// - progressbars appear ABOVE the table header
// - threaded animation (which was known to have some issues: https://stackoverflow.com/questions/18142801/nstableview-nsprogressindicator-flickering-issues) is NOT the cause; happens regardless of setting (or does the setting not stick? TODO)
// LONGTERM
// - reuse row views instead of creating a new one each time
@interface tableModel : NSObject<NSTableViewDataSource, NSTableViewDelegate> {
uiTableModel *libui_m;
}
- (id)initWithModel:(uiTableModel *)m;
- (IBAction)onAction:(id)sender;
@end
enum {
partText,
partImage,
partButton,
partCheckbox,
partProgressBar,
};
@interface tablePart : NSObject
@property int type;
@property int textColumn;
@property int textColorColumn;
@property int imageColumn;
@property int valueColumn;
@property int expand;
@property int editable;
- (NSView *)mkView:(uiTableModel *)m row:(int)row;
@end
@interface tableColumn : NSTableColumn
@property uiTableColumn *libui_col;
@end
@interface tableView : NSTableView
@property uiTable *libui_t;
@end
struct uiTableModel {
uiTableModelHandler *mh;
tableModel *m;
NSMutableArray *tables;
};
struct uiTableColumn {
tableColumn *c;
NSMutableArray *parts;
};
struct uiTable {
uiDarwinControl c;
NSScrollView *sv;
tableView *tv;
struct scrollViewData *d;
int backgroundColumn;
};
@implementation tableModel
- (id)initWithModel:(uiTableModel *)m
{
self = [super init];
if (self)
self->libui_m = m;
return self;
}
- (NSInteger)numberOfRowsInTableView:(NSTableView *)tv
{
uiTableModelHandler *mh = self->libui_m->mh;
return (*(mh->NumRows))(mh, self->libui_m);
}
// these are according to Interface Builder
#define xleft 2
#define xmiddle 7 /* between images and text, anyway; let's just use it for everything to be simpler */
#define xright 3
- (NSView *)tableView:(NSTableView *)tv viewForTableColumn:(NSTableColumn *)cc row:(NSInteger)row
{
NSTableCellView *v;
tableColumn *c = (tableColumn *) cc;
tablePart *part;
NSMutableArray *views;
NSView *view, *prev;
v = [[NSTableCellView alloc] initWithFrame:NSZeroRect];
views = [NSMutableArray new];
for (part in c.libui_col->parts)
[views addObject:[part mkView:self->libui_m row:row]];
if ([views count] == 0) // empty (TODO allow?)
goto done;
// add to v and arrange horizontally
prev = nil;
for (view in views) {
[v addSubview:view];
// TODO set [v imageView] and [v textField] as appropriate?
if (prev == nil) { // first view
[v addConstraint:mkConstraint(v, NSLayoutAttributeLeading,
NSLayoutRelationEqual,
view, NSLayoutAttributeLeading,
1, -xleft,
@"uiTableColumn first part horizontal constraint")];
prev = view;
continue;
}
[v addConstraint:mkConstraint(prev, NSLayoutAttributeTrailing,
NSLayoutRelationEqual,
view, NSLayoutAttributeLeading,
1, -xmiddle,
@"uiTableColumn middle horizontal constraint")];
prev = view;
}
[v addConstraint:mkConstraint(prev, NSLayoutAttributeTrailing,
NSLayoutRelationEqual,
v, NSLayoutAttributeTrailing,
1, -xright,
@"uiTableColumn last part horizontal constraint")];
// and vertically
for (view in views) {
[v addConstraint:mkConstraint(view, NSLayoutAttributeCenterY,
NSLayoutRelationEqual,
v, NSLayoutAttributeCenterY,
1, 0,
@"uiTableColumn part vertical constraint")];
// TODO avoid the need for this hack
if ([view isKindOfClass:[NSImageView class]])
[v addConstraint:mkConstraint(view, NSLayoutAttributeTop,
NSLayoutRelationEqual,
v, NSLayoutAttributeTop,
1, 0,
@"uiTableColumn part vertical top constraint")];
}
done:
[views release];
[v setTranslatesAutoresizingMaskIntoConstraints:NO];
// TODO autorelease?
return v;
}
- (void)tableView:(NSTableView *)nstv didAddRowView:(NSTableRowView *)rv forRow:(NSInteger)row
{
uiTableModel *m = self->libui_m;
tableView *tv = (tableView *) nstv;
uiTable *t = tv.libui_t;
NSColor *color;
if (t->backgroundColumn == -1)
return;
color = (NSColor *) ((*(m->mh->CellValue))(m->mh, m, row, t->backgroundColumn));
if (color == nil)
return;
[rv setBackgroundColor:color];
// TODO autorelease color? or release it?
}
- (IBAction)onAction:(id)sender
{
uiTableModel *m = self->libui_m;
NSView *view = (NSView *) sender;
NSTableView *tv;
NSInteger row;
const void *data;
row = -1;
for (tv in m->tables) {
row = [tv rowForView:view];
if (row != -1)
break;
}
if (row == -1)
uiprivImplBug("table model action triggered on view with no associated table");
if ([view isKindOfClass:[NSTextField class]])
data = [[((NSTextField *) view) stringValue] UTF8String];
else if ([view isKindOfClass:[NSButton class]]) {
NSButton *b;
b = (NSButton *) view;
// if ([b buttonType] == NSSwitchButton)
data = uiTableModelGiveInt([b state] == NSOnState);
// TODO there is no buttonType getter
if(1); else
data = NULL;
} else
uiprivImplBug("table model editing action triggered on non-editable view");
// note the use of [view tag] — we need the model column, which we store in the view tag for relevant views below
(*(m->mh->SetCellValue))(m->mh, m,
row, [view tag],
data);
// always refresh the value in case the model rejected it
// TODO only affect tv?
uiTableModelRowChanged(m, row);
}
@end
@implementation tablePart
- (id)init
{
self = [super init];
if (self) {
self.textColumn = -1;
self.textColorColumn = -1;
}
return self;
}
- (NSView *)mkView:(uiTableModel *)m row:(int)row
{
void *data;
NSString *str;
NSView *view;
NSTextField *tf;
NSImageView *iv;
NSButton *b;
NSProgressIndicator *p;
int value;
switch (self.type) {
case partText:
data = (*(m->mh->CellValue))(m->mh, m, row, self.textColumn);
str = toNSString((char *) data);
uiprivFree(data);
tf = newLabel(str);
// TODO set wrap and ellipsize modes?
if (self.textColorColumn != -1) {
NSColor *color;
color = (NSColor *) ((*(m->mh->CellValue))(m->mh, m, row, self.textColorColumn));
if (color != nil)
[tf setTextColor:color];
// TODO release color
}
if (self.editable) {
[tf setEditable:YES];
[tf setTarget:m->m];
[tf setAction:@selector(onAction:)];
}
[tf setTag:self.textColumn];
view = tf;
break;
case partImage:
data = (*(m->mh->CellValue))(m->mh, m, row, self.imageColumn);
iv = [[NSImageView alloc] initWithFrame:NSZeroRect];
[iv setImage:imageImage((uiImage *) data)];
[iv setImageFrameStyle:NSImageFrameNone];
[iv setImageAlignment:NSImageAlignCenter];
[iv setImageScaling:NSImageScaleProportionallyDown];
[iv setAnimates:NO];
[iv setEditable:NO];
[iv addConstraint:mkConstraint(iv, NSLayoutAttributeWidth,
NSLayoutRelationEqual,
iv, NSLayoutAttributeHeight,
1, 0,
@"uiTable image squareness constraint")];
[iv setTag:self.imageColumn];
view = iv;
break;
case partButton:
// TODO buttons get clipped
data = (*(m->mh->CellValue))(m->mh, m, row, self.textColumn);
str = toNSString((char *) data);
b = [[NSButton alloc] initWithFrame:NSZeroRect];
[b setTitle:str];
[b setButtonType:NSMomentaryPushInButton];
[b setBordered:YES];
[b setBezelStyle:NSRoundRectBezelStyle];
uiDarwinSetControlFont(b, NSRegularControlSize);
if (self.editable) {
[b setTarget:m->m];
[b setAction:@selector(onAction:)];
} else
[b setEnabled:NO];
[b setTag:self.textColumn];
view = b;
break;
case partCheckbox:
data = (*(m->mh->CellValue))(m->mh, m, row, self.valueColumn);
b = [[NSButton alloc] initWithFrame:NSZeroRect];
[b setTitle:@""];
[b setButtonType:NSSwitchButton];
// doesn't seem to have an associated bezel style
[b setBordered:NO];
[b setTransparent:NO];
uiDarwinSetControlFont(b, NSRegularControlSize);
if (uiTableModelTakeInt(data) != 0)
[b setState:NSOnState];
else
[b setState:NSOffState];
if (self.editable) {
[b setTarget:m->m];
[b setAction:@selector(onAction:)];
} else
[b setEnabled:NO];
[b setTag:self.valueColumn];
view = b;
break;
case partProgressBar:
data = (*(m->mh->CellValue))(m->mh, m, row, self.valueColumn);
value = uiTableModelTakeInt(data);
// TODO no intrinsic width
p = [[NSProgressIndicator alloc] initWithFrame:NSZeroRect];
[p setControlSize:NSRegularControlSize];
[p setBezeled:YES];
[p setStyle:NSProgressIndicatorBarStyle];
if (value == -1) {
[p setIndeterminate:YES];
[p startAnimation:p];
} else if (value == 100) {
[p setIndeterminate:NO];
[p setMaxValue:101];
[p setDoubleValue:101];
[p setDoubleValue:100];
[p setMaxValue:100];
} else {
[p setIndeterminate:NO];
[p setDoubleValue:(value + 1)];
[p setDoubleValue:value];
}
view = p;
break;
}
// if stretchy, don't hug, otherwise hug forcibly
if (self.expand)
[view setContentHuggingPriority:NSLayoutPriorityDefaultLow forOrientation:NSLayoutConstraintOrientationHorizontal];
else
[view setContentHuggingPriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintOrientationHorizontal];
[view setTranslatesAutoresizingMaskIntoConstraints:NO];
// TODO autorelease?
return view;
}
@end
@implementation tableColumn
@end
@implementation tableView
@end
void *uiTableModelStrdup(const char *str)
{
// TODO don't we have this already?
char *dup;
dup = (char *) uiprivAlloc((strlen(str) + 1) * sizeof (char), "char[]");
strcpy(dup, str);
return dup;
}
uiTableModel *uiNewTableModel(uiTableModelHandler *mh)
{
uiTableModel *m;
m = uiprivNew(uiTableModel);
m->mh = mh;
m->m = [[tableModel alloc] initWithModel:m];
m->tables = [NSMutableArray new];
return m;
}
void *uiTableModelGiveColor(double r, double g, double b, double a)
{
return [[NSColor colorWithSRGBRed:r green:g blue:b alpha:a] retain];
}
void uiFreeTableModel(uiTableModel *m)
{
if ([m->tables count] != 0)
uiprivUserBug("You cannot free a uiTableModel while uiTables are using it.");
[m->tables release];
[m->m release];
uiprivFree(m);
}
void uiTableModelRowInserted(uiTableModel *m, int newIndex)
{
NSTableView *tv;
NSIndexSet *set;
set = [NSIndexSet indexSetWithIndex:newIndex];
for (tv in m->tables)
[tv insertRowsAtIndexes:set withAnimation:NSTableViewAnimationEffectNone];
// set is autoreleased
}
void uiTableModelRowChanged(uiTableModel *m, int index)
{
NSTableView *tv;
NSIndexSet *set, *cols;
set = [NSIndexSet indexSetWithIndex:index];
for (tv in m->tables) {
cols = [[NSIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, [[tv tableColumns] count])];
[tv reloadDataForRowIndexes:set columnIndexes:cols];
// TODO this isn't enough
[cols release];
}
// set is autoreleased
}
void uiTableModelRowDeleted(uiTableModel *m, int oldIndex)
{
NSTableView *tv;
NSIndexSet *set;
set = [NSIndexSet indexSetWithIndex:oldIndex];
for (tv in m->tables)
[tv removeRowsAtIndexes:set withAnimation:NSTableViewAnimationEffectNone];
// set is autoreleased
}
void uiTableColumnAppendTextPart(uiTableColumn *c, int modelColumn, int expand)
{
tablePart *part;
part = [tablePart new];
part.type = partText;
part.textColumn = modelColumn;
part.expand = expand;
[c->parts addObject:part];
}
void uiTableColumnAppendImagePart(uiTableColumn *c, int modelColumn, int expand)
{
tablePart *part;
part = [tablePart new];
part.type = partImage;
part.imageColumn = modelColumn;
part.expand = expand;
[c->parts addObject:part];
}
void uiTableColumnAppendButtonPart(uiTableColumn *c, int modelColumn, int expand)
{
tablePart *part;
part = [tablePart new];
part.type = partButton;
part.textColumn = modelColumn;
part.expand = expand;
part.editable = 1; // editable by default
[c->parts addObject:part];
}
void uiTableColumnAppendCheckboxPart(uiTableColumn *c, int modelColumn, int expand)
{
tablePart *part;
part = [tablePart new];
part.type = partCheckbox;
part.valueColumn = modelColumn;
part.expand = expand;
part.editable = 1; // editable by default
[c->parts addObject:part];
}
void uiTableColumnAppendProgressBarPart(uiTableColumn *c, int modelColumn, int expand)
{
tablePart *part;
part = [tablePart new];
part.type = partProgressBar;
part.valueColumn = modelColumn;
part.expand = expand;
[c->parts addObject:part];
}
void uiTableColumnPartSetEditable(uiTableColumn *c, int part, int editable)
{
tablePart *p;
p = (tablePart *) [c->parts objectAtIndex:part];
p.editable = editable;
}
void uiTableColumnPartSetTextColor(uiTableColumn *c, int part, int modelColumn)
{
tablePart *p;
p = (tablePart *) [c->parts objectAtIndex:part];
p.textColorColumn = modelColumn;
}
uiDarwinControlAllDefaultsExceptDestroy(uiTable, sv)
static void uiTableDestroy(uiControl *c)
{
uiTable *t = uiTable(c);
// TODO
[t->sv release];
uiFreeControl(uiControl(t));
}
uiTableColumn *uiTableAppendColumn(uiTable *t, const char *name)
{
uiTableColumn *c;
c = uiprivNew(uiTableColumn);
c->c = [[tableColumn alloc] initWithIdentifier:@""];
c->c.libui_col = c;
// via Interface Builder
[c->c setResizingMask:(NSTableColumnAutoresizingMask | NSTableColumnUserResizingMask)];
// 10.10 adds -[NSTableColumn setTitle:]; before then we have to do this
[[c->c headerCell] setStringValue:toNSString(name)];
// TODO is this sufficient?
[[c->c headerCell] setFont:[NSFont systemFontOfSize:[NSFont systemFontSizeForControlSize:NSSmallControlSize]]];
c->parts = [NSMutableArray new];
[t->tv addTableColumn:c->c];
return c;
}
void uiTableSetRowBackgroundColorModelColumn(uiTable *t, int modelColumn)
{
t->backgroundColumn = modelColumn;
}
uiTable *uiNewTable(uiTableModel *model)
{
uiTable *t;
struct scrollViewCreateParams p;
uiDarwinNewControl(uiTable, t);
t->tv = [[tableView alloc] initWithFrame:NSZeroRect];
t->tv.libui_t = t;
[t->tv setDataSource:model->m];
[t->tv setDelegate:model->m];
[t->tv reloadData];
[model->tables addObject:t->tv];
// TODO is this sufficient?
[t->tv setAllowsColumnReordering:NO];
[t->tv setAllowsColumnResizing:YES];
[t->tv setAllowsMultipleSelection:NO];
[t->tv setAllowsEmptySelection:YES];
[t->tv setAllowsColumnSelection:NO];
[t->tv setUsesAlternatingRowBackgroundColors:YES];
[t->tv setSelectionHighlightStyle:NSTableViewSelectionHighlightStyleRegular];
[t->tv setGridStyleMask:NSTableViewGridNone];
[t->tv setAllowsTypeSelect:YES];
// TODO floatsGroupRows — do we even allow group rows?
memset(&p, 0, sizeof (struct scrollViewCreateParams));
p.DocumentView = t->tv;
// this is what Interface Builder sets it to
// TODO verify
p.BackgroundColor = [NSColor colorWithCalibratedWhite:1.0 alpha:1.0];
p.DrawsBackground = YES;
p.Bordered = YES;
p.HScroll = YES;
p.VScroll = YES;
t->sv = mkScrollView(&p, &(t->d));
t->backgroundColumn = -1;
return t;
}