ui/cocoa/list.m

Mon, 10 Nov 2025 21:52:51 +0100

author
Olaf Wintermann <olaf.wintermann@gmail.com>
date
Mon, 10 Nov 2025 21:52:51 +0100
changeset 113
dde28a806552
parent 112
c3f2f16fa4b8
permissions
-rw-r--r--

update ucx

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 2025 Olaf Wintermann. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *   1. Redistributions of source code must retain the above copyright
 *      notice, this list of conditions and the following disclaimer.
 *
 *   2. Redistributions in binary form must reproduce the above copyright
 *      notice, this list of conditions and the following disclaimer in the
 *      documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#import "list.h"
#import "ListDelegate.h"
#import <objc/runtime.h>

#import <inttypes.h>
#import <limits.h>

#import <cx/array_list.h>

static void* getvalue_wrapper(UiList *list, void *elm, int row, int col, void *userdata, UiBool *freeResult) {
    ui_getvaluefunc getvalue = (ui_getvaluefunc)userdata;
    return getvalue(elm, col);
}

static void* str_getvalue(UiList *list, void *elm, int row, int col, void *userdata, UiBool *freeResult) {
    return elm;
}

/* --------------------------- ListView --------------------------- */

/*
 * adds a NSTableViewDelegate that handles all events and calls
 * callbacks specified in the UiListArgs
 */
static void add_listdelegate(UiObject *obj, NSTableView *tableview, UiListArgs *args) {
    ListDelegate *delegate = [[ListDelegate alloc] init:tableview obj:obj];
    delegate.onactivate = args->onactivate;
    delegate.onactivatedata = args->onactivatedata;
    delegate.onselection = args->onselection;
    delegate.onselectiondata = args->onselectiondata;
    tableview.delegate = delegate;
    objc_setAssociatedObject(tableview, "ui_listdelegate", delegate, OBJC_ASSOCIATION_RETAIN);
    tableview.doubleAction = @selector(activateEvent:);
    tableview.target = delegate;
}

static void bind_list_to_tableview(UiList *list, NSTableView *tableview) {
    list->obj = (__bridge void*)tableview;
    list->update = ui_tableview_update;
    list->getselection = ui_tableview_getselection;
    list->setselection = ui_tableview_setselection;
}

UIWIDGET ui_listview_create(UiObject* obj, UiListArgs *args) {
    NSScrollView *scrollview = [[NSScrollView alloc] init];
    
    NSTableView *tableview = [[NSTableView alloc] init];
    tableview.autoresizingMask = NSViewWidthSizable;
    tableview.headerView = nil;
    
    if(args->multiselection) {
        tableview.allowsMultipleSelection = YES;
    }
    
    scrollview.documentView = tableview;
    
    UiLayout layout = UI_INIT_LAYOUT(args);
    ui_container_add(obj, scrollview, &layout);
    
    add_listdelegate(obj, tableview, args);
    
    UiVar *var = uic_widget_var(obj->ctx, obj->ctx, args->list, args->varname, UI_VAR_LIST);
    if(var) {
        UiList *list = var->value;
        bind_list_to_tableview(list, tableview);
        
        ui_getvaluefunc2 getvalue = args->getvalue2;
        void *getvaluedata = args->getvalue2data;
        if(!getvalue) {
            if(args->getvalue) {
                getvalue = getvalue_wrapper;
                getvaluedata = (void*)args->getvalue;
            } else {
                getvalue = str_getvalue; // by default list values are interpreted as strings
            }
        }
        
        NSTableColumn *column = [[NSTableColumn alloc] initWithIdentifier:@"column"];
        [tableview addTableColumn:column];
        
        ListDataSource *dataSource = [[ListDataSource alloc] init:tableview.tableColumns var:var getvalue:getvalue getvaluedata:getvaluedata];
        
        tableview.dataSource = dataSource;
        [tableview reloadData];
        
        objc_setAssociatedObject(tableview, "ui_datasource", dataSource, OBJC_ASSOCIATION_RETAIN);
    }

    return (__bridge void*)scrollview;
}

/* --------------------------- TableView --------------------------- */

UIWIDGET ui_table_create(UiObject* obj, UiListArgs *args) {
    NSScrollView *scrollview = [[NSScrollView alloc] init];
    
    NSTableView *tableview = [[NSTableView alloc] init];
    tableview.autoresizingMask = NSViewWidthSizable;
    tableview.columnAutoresizingStyle = NSTableViewSequentialColumnAutoresizingStyle;
    
    if(args->multiselection) {
        tableview.allowsMultipleSelection = YES;
    }
    
    UiLayout layout = UI_INIT_LAYOUT(args);
    ui_container_add(obj, scrollview, &layout);
    
    add_listdelegate(obj, tableview, args);
    
    // convert model
    NSMutableArray<NSTableColumn*> *cols = [[NSMutableArray alloc] init];
    UiModel *model = args->model;
    if(model) {
        for(int i=0;i<model->columns;i++) {
            char *title = model->titles[i];
            UiModelType type = model->types[i];
            int width = model->columnsize[i];
            NSString *identifier = [[NSString alloc] initWithUTF8String:title];
            NSTableColumn *column = [[NSTableColumn alloc] initWithIdentifier:identifier];
            column.title = identifier;
            column.resizingMask = NSTableColumnUserResizingMask;
            if(width > 0) {
                column.width = width;
            } else if(width < 0) {
                column.resizingMask = NSTableColumnAutoresizingMask | NSTableColumnUserResizingMask;
            }
            if(type >= UI_ICON) {
                // TODO
            }
            [tableview addTableColumn:column];
            [cols addObject:column];
        }
    }
    
    UiVar *var = uic_widget_var(obj->ctx, obj->ctx, args->list, args->varname, UI_VAR_LIST);
    if(var) {
        UiList *list = var->value;
        bind_list_to_tableview(list, tableview);
        
        ui_getvaluefunc2 getvalue = args->getvalue2;
        void *getvaluedata = args->getvalue2data;
        if(!getvalue) {
            if(args->getvalue) {
                getvalue = getvalue_wrapper;
                getvaluedata = (void*)args->getvalue;
            } else {
                fprintf(stderr, "Error: tableview requires getvalue or getvalue2 func\n");
                return (__bridge void*)scrollview;
            }
        }
        
        ListDataSource *dataSource = [[ListDataSource alloc] init:cols var:var getvalue:getvalue getvaluedata:getvaluedata];
        if(model) {
            dataSource.model = ui_model_copy(obj->ctx, model);
        }
        
        tableview.dataSource = dataSource;
        [tableview reloadData];
        
        objc_setAssociatedObject(tableview, "ui_datasource", dataSource, OBJC_ASSOCIATION_RETAIN);
    }
    
    scrollview.documentView = tableview;

    return (__bridge void*)scrollview;
}

/* ------ common functions ------ */

void ui_tableview_update(UiList *list, int i) {
    NSTableView *tableview = (__bridge NSTableView*)list->obj;
    if(i < 0) {
        [tableview reloadData];
    } else {
        [tableview reloadData]; // TODO: optimize
    }
}

UiListSelection ui_tableview_getselection(UiList *list) {
    NSTableView *tableview = (__bridge NSTableView*)list->obj;
    return ui_tableview_selection(tableview);
}

void ui_tableview_setselection(UiList *list, UiListSelection selection) {
    NSTableView *tableview = (__bridge NSTableView*)list->obj;
    NSMutableIndexSet *indexSet = [NSMutableIndexSet indexSet];
    for(int i=0;i<selection.count;i++) {
        [indexSet addIndex:selection.rows[i]];
    }
    [tableview selectRowIndexes:indexSet byExtendingSelection:NO];
}


/* --------------------------- DropDown --------------------------- */

@implementation UiDropDown

- (id)init:(UiObject*)obj {
    _obj = obj;
    return self;
}

- (void) comboBoxSelectionDidChange:(NSNotification *) notification {
    int index = (int)_combobox.indexOfSelectedItem;
    
    void *eventdata = NULL;
    if(_var) {
        UiList *list = _var->value;
        if(index >= 0) {
            eventdata = list->get(list, index);
        }
    } else {
        NSString *str = _combobox.objectValueOfSelectedItem;
        if(str) {
            eventdata = (void*)str.UTF8String;
        }
    }
    
    UiEvent event;
    event.obj = _obj;
    event.window = event.obj->window;
    event.document = event.obj->ctx->document;
    event.eventdata = eventdata;
    event.eventdatatype = UI_EVENT_DATA_LIST_ELM;
    event.intval = index;
    
    if(_onselection) {
        _onselection(&event, _onselectiondata);
    }
    
    if(_onactivate) {
        _onactivate(&event, _onactivatedata);
    }
}

@end

UIWIDGET ui_combobox_create(UiObject* obj, UiListArgs *args) {
    NSComboBox *dropdown = [[NSComboBox alloc] init];
    dropdown.editable = NO;
    
    UiDropDown *uidropdown = [[UiDropDown alloc] init:obj];
    objc_setAssociatedObject(dropdown, "ui_dropdown", uidropdown, OBJC_ASSOCIATION_RETAIN);
    uidropdown.onactivate = args->onactivate;
    uidropdown.onactivatedata = args->onactivatedata;
    uidropdown.onselection = args->onselection;
    uidropdown.onselectiondata = args->onselectiondata;
    uidropdown.combobox = dropdown;
    
    if(!args->getvalue2) {
        if(args->getvalue) {
            args->getvalue2 = getvalue_wrapper;
            args->getvalue2data = (void*)args->getvalue;
        } else {
            args->getvalue2 = str_getvalue;
        }
    }
    uidropdown.getvalue = args->getvalue2;
    uidropdown.getvaluedata = args->getvalue2data;
    
    UiLayout layout = UI_INIT_LAYOUT(args);
    ui_container_add(obj, dropdown, &layout);
    
    UiVar *var = uic_widget_var(obj->ctx, obj->ctx, args->list, args->varname, UI_VAR_LIST);
    if(var) {
        UiList *list = var->value;
        list->obj = (__bridge void*)dropdown;
        list->update = ui_dropdown_update;
        list->getselection = ui_dropdown_getselection;
        list->setselection = ui_dropdown_setselection;
        ui_dropdown_update(list, -1);
    } else {
        for(int i=0;i<args->static_nelm;i++) {
            char *str = args->static_elements[i];
            NSString *item = [[NSString alloc] initWithUTF8String:str];
            [dropdown addItemWithObjectValue:item];
        }
    }
    
    uidropdown.var = var;
    
    return (__bridge void*)dropdown;
}

void ui_dropdown_update(UiList *list, int i) {
    NSComboBox *combobox = (__bridge NSComboBox*)list->obj;
    UiDropDown *dropdown = objc_getAssociatedObject(combobox, "ui_dropdown");
    if(dropdown) {
        [combobox removeAllItems];
        
        ui_getvaluefunc2 getvalue = dropdown.getvalue;
        void *getvaluedata = dropdown.getvaluedata;
        
        int index = 0;
        void *elm = list->first(list);
        while(elm) {
            UiBool freeResult = FALSE;
            char *str = getvalue(list, elm, index, 0, getvaluedata, &freeResult);
            if(str) {
                NSString *item = [[NSString alloc] initWithUTF8String:str];
                [combobox addItemWithObjectValue:item];
            }
            if(freeResult) {
                free(str);
            }
            elm = list->next(list);
            index++;
        }
    } else {
        fprintf(stderr, "Error: obj is not a dropdown\n");
    }
}

UiListSelection ui_dropdown_getselection(UiList *list) {
    UiListSelection sel = { 0, NULL };
    NSComboBox *combobox = (__bridge NSComboBox*)list->obj;
    NSInteger index = combobox.indexOfSelectedItem;
    if(index >= 0) {
        sel.rows = malloc(sizeof(int));
        sel.count = 1;
        sel.rows[0] = (int)index;
    }
    return sel;
}

void ui_dropdown_setselection(UiList *list, UiListSelection selection) {
    NSComboBox *combobox = (__bridge NSComboBox*)list->obj;
    if(selection.count > 0) {
        [combobox selectItemAtIndex:selection.rows[0]];
    } else {
        [combobox selectItemAtIndex: -1];
    }
}


/* --------------------------- SourceList --------------------------- */

static ui_sourcelist_update_func sclist_update_callback = NULL;

void ui_sourcelist_set_update_callback(ui_sourcelist_update_func cb) {
    sclist_update_callback = cb;
}

void ui_sourcelist_updated(void) {
    if(sclist_update_callback) {
        sclist_update_callback();
    }
}

static void sublist_free(const CxAllocator *a, UiSubList *sl) {
    cxFree(a, (char*)sl->varname);
    cxFree(a, (char*)sl->header);
}

static UiSubList copy_sublist(const CxAllocator *a, UiSubList *sl) {
    UiSubList new_sl;
    new_sl.value = sl->value;
    new_sl.varname = sl->varname ? cx_strdup_a(a, cx_str(sl->varname)).ptr : NULL;
    new_sl.header = sl->header ? cx_strdup_a(a, cx_str(sl->header)).ptr : NULL;
    new_sl.separator = sl->separator;
    new_sl.userdata = sl->userdata;
    return new_sl;
}

static CxList* copy_sublists(const CxAllocator *a, UiSourceListArgs *args) {
    if(args->sublists) {
        size_t max = args->numsublists;
        if(max == 0) {
            max = INT_MAX;
        }
        
        CxList *sublists = cxArrayListCreate(a, NULL, sizeof(UiSubList), args->numsublists);
        sublists->collection.advanced_destructor = (cx_destructor_func2)sublist_free;
        
        for(int i=0;i<max;i++) {
            UiSubList *sl = &args->sublists[i];
            if(sl->value == NULL && sl->varname == NULL) {
                break;
            }
            
            UiSubList new_sl = copy_sublist(a, sl);
            cxListAdd(sublists, &new_sl);
        }
        
        return sublists;
    }
    return NULL;
}

UIWIDGET ui_sourcelist_create(UiObject *obj, UiSourceListArgs *args) {
    // create views
    NSScrollView *scrollview = [[NSScrollView alloc] init];
    scrollview.autoresizingMask = NSViewWidthSizable;
    scrollview.hasVerticalScroller = YES;
    scrollview.hasHorizontalScroller = NO;
    scrollview.autohidesScrollers = YES;
    
    NSOutlineView *outline = [[NSOutlineView alloc]init];
    NSTableColumn *column = [[NSTableColumn alloc] initWithIdentifier:@"x"];
    [outline addTableColumn:column];
    outline.outlineTableColumn = column;
    outline.headerView = NULL;
    outline.rowSizeStyle = NSTableViewRowSizeStyleDefault;
    outline.usesAutomaticRowHeights = YES;
    outline.indentationPerLevel = 0;
    
    outline.style = NSTableViewStyleSourceList;
    
    // Make background transparent so vibrancy shows through
    scrollview.drawsBackground = NO;

    scrollview.documentView = outline;
    
    UiLayout layout = UI_ARGS2LAYOUT(args);
    ui_container_add(obj, scrollview, &layout);
    
    // datasource and delegate
    UiSourceList *data = [[UiSourceList alloc] init:obj outline:outline];
    data.sublists = copy_sublists(obj->ctx->allocator, args);
    UiVar* var = uic_widget_var(obj->ctx, obj->ctx, args->dynamic_sublist, args->varname, UI_VAR_LIST);
    if(var) {
        UiList *list = var->value;
        list->obj = (__bridge void*)data;
        list->update = ui_sourcelist_update;
    }
    data.dynamic_sublists = var;
    data.getvalue = args->getvalue;
    data.getvaluedata = args->getvaluedata;
    data.onactivate = args->onactivate;
    data.onactivatedata = args->onactivatedata;
    data.onbuttonclick = args->onbuttonclick;
    data.onactivatedata = args->onbuttonclickdata;
    [data update:-1];
 
    outline.dataSource = data;
    outline.delegate = data;
    
    [data update:-1];
    
    objc_setAssociatedObject(outline, "ui_datasource", data, OBJC_ASSOCIATION_RETAIN);
    
    return (__bridge void*)scrollview;
}

void ui_sourcelist_update(UiList *list, int row) {
    UiSourceList *sourcelist = (__bridge UiSourceList*)list->obj;
    [sourcelist update:row];
}


/*
 * Data Source and Delegate for the sourcelist NSOutlineView
 */
@implementation UiSourceList

- (id)init:(UiObject*)obj outline:(NSOutlineView*)view {
    _obj = obj;
    _outlineView = view;
    _sections = [[NSMutableArray alloc] initWithCapacity:16];
    return self;
}

- (void)dealloc {
    cxListFree(_sublists);
}

- (void)update:(int)row {
    // TODO: check row
    
    [_sections removeAllObjects];
    
    CxIterator i = cxListIterator(_sublists);
    int index = 0;
    int rownum = 0;
    cx_foreach(UiSubList *, sl, i) {
        UiSourceListItem *section = [[UiSourceListItem alloc] init:self sublist:sl];
        section.sublistIndex = index;
        section.rownum = rownum;
        section.sublistStartRow = rownum;
        [section update:-1];
        [_sections addObject:section];
        index++;
        rownum += 1 + section.items.count;
    }
    
    [_outlineView reloadData];
    [_outlineView expandItem:nil expandChildren:YES];
}

// NSOutlineViewDataSource implementation

- (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item {
    if(item == nil) {
        return _sections.count;
    } else {
        UiSourceListItem *i = item;
        return i.items.count;
    }
}

- (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item {
    UiSourceListItem *i = item;
    return [i isSection] ? YES : NO;
}

- (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item {
    UiSourceListItem *i = item;
    if(i) {
        return [i.items objectAtIndex:index];
    }
    return [_sections objectAtIndex:index];
}

- (void)outlineView:(NSOutlineView *)outlineView
     setObjectValue:(id)object
     forTableColumn:(NSTableColumn *)tableColumn
             byItem:(id)item
{
    
}

// NSOutlineViewDelegate implementation

- (NSView *)outlineView:(NSOutlineView *)outlineView
     viewForTableColumn:(NSTableColumn *)tableColumn
                   item:(id)item
{
    UiSourceListItem *i = item;
    
    NSTableCellView *cell = [[NSTableCellView alloc] init];
    cell.identifier = @"cell";
    // Icon
    NSImageView *iconView = [[NSImageView alloc] initWithFrame:NSZeroRect];
    iconView.translatesAutoresizingMaskIntoConstraints = NO;
    [cell addSubview:iconView];
    cell.imageView = iconView;

    // Label
    //NSTextField *textField = [NSTextField labelWithString:@""];
    NSTextField *textField = [[NSTextField alloc] initWithFrame:NSZeroRect];
    textField.translatesAutoresizingMaskIntoConstraints = NO;
    textField.bezeled = NO;
    textField.editable = NO;
    textField.drawsBackground = NO;
    textField.selectable = NO;
    textField.lineBreakMode = NSLineBreakByTruncatingTail;
    
    
    [cell addSubview:textField];
    cell.textField = textField;
    
    if([i isSection]) {
        NSFont *font = [NSFont boldSystemFontOfSize:[NSFont systemFontSize]*0.85];
        //NSFont *font = [NSFont preferredFontForTextStyle:NSFontTextStyleCaption1 options:@{}];
        NSDictionary *attrs = @{
            NSFontAttributeName: font,
            NSForegroundColorAttributeName: [NSColor tertiaryLabelColor]
        };
        textField.attributedStringValue = [[NSAttributedString alloc] initWithString:i.label attributes:attrs];
        
        // Layout constraints
        [NSLayoutConstraint activateConstraints:@[
            [iconView.leadingAnchor constraintEqualToAnchor:cell.leadingAnchor constant:0],
            [iconView.bottomAnchor constraintEqualToAnchor:cell.bottomAnchor constant:-1],

            [textField.leadingAnchor constraintEqualToAnchor:cell.leadingAnchor constant:0],
            [textField.bottomAnchor constraintEqualToAnchor:cell.bottomAnchor constant:-1],
            [textField.trailingAnchor constraintEqualToAnchor:cell.trailingAnchor constant:0],
        ]];
    } else {
        textField.stringValue = i.label;
        
        // Layout constraints
        [NSLayoutConstraint activateConstraints:@[
            [iconView.leadingAnchor constraintEqualToAnchor:cell.leadingAnchor constant:0],
            [iconView.centerYAnchor constraintEqualToAnchor:cell.centerYAnchor],

            [textField.leadingAnchor constraintEqualToAnchor:cell.leadingAnchor constant:0],
            [textField.centerYAnchor constraintEqualToAnchor:cell.centerYAnchor],
            [textField.trailingAnchor constraintEqualToAnchor:cell.trailingAnchor constant:0],
        ]];
    }
    
    return cell;
}

- (NSTableRowView *) outlineView:(NSOutlineView *) outlineView
                  rowViewForItem:(id)item {
    UiSourceListItem *it = item;
    UiSourceListRow *row = [[UiSourceListRow alloc]init];
    if([it isSection] && it.sublist->header) {
        row.showDisclosureButton = YES;
    }
    return row;
}

- (BOOL) outlineView:(NSOutlineView *) outlineView
    shouldSelectItem:(id)item
{
    UiSourceListItem *i = item;
    return [i isSection] ? NO : YES;
}

- (CGFloat) outlineView:(NSOutlineView *) outlineView
      heightOfRowByItem:(id) item
{
    UiSourceListItem *i = item;
    CGFloat rowHeight = outlineView.rowHeight;
    if([i isSection]) {
        if(i.sublist->header) {
            rowHeight += i.sublistIndex == 0 ? -12 : 4;
        } else {
            rowHeight = i.sublistIndex == 0 ? 0.1 : 12;
        }
    }
    return rowHeight;
}

- (void) outlineViewSelectionDidChange:(NSNotification *) notification {
    UiEvent event;
    event.obj = _obj;
    event.window = event.obj->window;
    event.document = event.obj->ctx->document;
    event.eventdata = NULL;
    event.eventdatatype = 0;
    event.intval = 0;
    event.set = ui_get_setop();
    
    UiSubListEventData sublistEvent;
    
    NSInteger selectedRow = _outlineView.selectedRow;
    if(selectedRow >= 0) {
        UiSourceListItem *item = [_outlineView itemAtRow:selectedRow];
        UiSourceListItem *parent = item.parent;
        UiSubList *sublist = parent != nil ? parent.sublist : item.sublist;
        UiVar *var = parent != nil ? parent.var : item.var;
        if(item && var) {
            sublistEvent.list = var->value;
            sublistEvent.sublist_index = parent ? parent.sublistIndex : item.sublistIndex;
            sublistEvent.row_index = (int)selectedRow - item.sublistStartRow - 1;
            sublistEvent.sublist_userdata = sublist ? sublist->userdata : NULL;
            sublistEvent.event_data = item.eventdata;
            sublistEvent.row_data = sublistEvent.list->get(sublistEvent.list, sublistEvent.row_index);
            
            event.eventdata = &sublistEvent;
            event.eventdatatype = UI_EVENT_DATA_SUBLIST;
        }
    }
    
    if(_onactivate) {
        _onactivate(&event, _onactivatedata);
    }
}

@end

/*
 * Outline datasource item
 * Is used for sections (sublists) and individual items
 */
@implementation UiSourceListItem

- (id)init:(UiSourceList*)sourcelist sublist:(UiSubList*)sublist {
    _sourcelist = sourcelist;
    _sublist = sublist;
    _items = [[NSMutableArray alloc]initWithCapacity:16];
    if(sublist->header) {
        _label = [[NSString alloc]initWithUTF8String:sublist->header];
    } else {
        _label = @"";
    }
    UiVar *var = uic_widget_var(sourcelist.obj->ctx,
                                sourcelist.obj->ctx,
                                sublist->value,
                                sublist->varname,
                                UI_VAR_LIST);
    _var = var;
    return self;
}

- (id)init:(UiSubListItem*)item parent:(UiSourceListItem*)parent {
    _parent = parent;
    if(item->label) {
        _label = [[NSString alloc]initWithUTF8String:item->label];
    } else {
        _label = @"";
    }
    _eventdata = item->eventdata;
    return self;
}

- (BOOL)isSection {
    return _sublist != NULL;
}

- (void)update:(int)row {
    // TODO: check row
    
    [_items removeAllObjects];
    if(_var == NULL) {
        return;
    }
    UiList *list = _var->value;
    void *elm = list->first(list);
    int index = 0;
    while(elm) {
        UiSubListItem item = { NULL, NULL, NULL, NULL, NULL, NULL };
        if(_sourcelist.getvalue) {
            _sourcelist.getvalue(list, _sublist->userdata, elm, index, &item, _sourcelist.getvaluedata);
        } else {
            item.label = strdup(elm);
        }
        
        UiSourceListItem *it = [[UiSourceListItem alloc] init:&item parent:self];
        it.sublistIndex = index;
        it.rownum = self.rownum + index;
        it.sublistStartRow = _parent ? _parent.sublistStartRow : _sublistStartRow;
        [_items addObject:it];
        
        elm = list->next(list);
        index++;
    }
}

@end

/*
 * Custom NSTableRowView implementation
 * Moves the disclosure button to the right side
 * Handles mouse hover events (for hiding the disclosure button)
 */
@implementation UiSourceListRow

- (void)layout {
    [super layout];

    for (NSView *subview in self.subviews) {
        if ([subview.identifier isEqualToString:NSOutlineViewDisclosureButtonKey] ||
            [subview.identifier isEqualToString:NSOutlineViewShowHideButtonKey])
        {
            NSRect frame = subview.frame;
            frame.origin.x = self.bounds.size.width - frame.size.width - 16.0;
            subview.frame = frame;
            
            if(!_hover) {
                subview.hidden = YES;
            }
            
            if(subview != _disclosureButton) {
                // init disclosure button
                _disclosureButton = (NSButton*)subview;
                if ([subview isKindOfClass:[NSButton class]]) {
                    NSButton *button = (NSButton*)subview;
                    button.contentTintColor = [NSColor tertiaryLabelColor];
                }
            }
            
            
        } else if ([subview.identifier isEqualToString:@"cell"]) {
            NSRect frame = subview.frame;
            frame.origin.x = 16;
            subview.frame = frame;
        }
    }
}

- (void)updateTrackingAreas {
    [super updateTrackingAreas];
    if(_trackingArea != nil) {
        [self removeTrackingArea:_trackingArea];
    }
    _trackingArea = [[NSTrackingArea alloc] initWithRect:self.bounds
                                                 options:NSTrackingMouseEnteredAndExited |
                                                         NSTrackingActiveInActiveApp |
                                                         NSTrackingInVisibleRect
                                                   owner:self
                                                userInfo:nil];
    [self addTrackingArea:_trackingArea];
}

- (void)mouseEntered:(NSEvent *)event {
    _hover = YES;
    _disclosureButton.hidden = _showDisclosureButton ? NO : YES;
}

- (void)mouseExited:(NSEvent *)event {
    _hover = NO;
    _disclosureButton.hidden = YES;
}

@end

mercurial