ui/cocoa/list.m

changeset 112
c3f2f16fa4b8
parent 109
c3dfcb8f0be7
child 113
dde28a806552
--- a/ui/cocoa/list.m	Sat Oct 04 14:54:25 2025 +0200
+++ b/ui/cocoa/list.m	Sun Oct 19 21:20:08 2025 +0200
@@ -30,6 +30,11 @@
 #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);
@@ -354,3 +359,449 @@
         [combobox selectItemAtIndex: -1];
     }
 }
+
+
+/* --------------------------- SourceList --------------------------- */
+
+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