ui/cocoa/window.m

Sun, 19 Oct 2025 21:20:08 +0200

author
Olaf Wintermann <olaf.wintermann@gmail.com>
date
Sun, 19 Oct 2025 21:20:08 +0200
changeset 112
c3f2f16fa4b8
parent 110
c00e968d018b
child 113
dde28a806552
permissions
-rw-r--r--

update toolkit

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 2024 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 "window.h"

#import "MainWindow.h"
#import "WindowManager.h"
#import "BoxContainer.h"
#import "EventData.h"

#import <objc/runtime.h>

#include "../ui/window.h"
#include "../ui/properties.h"
#include "../common/context.h"
#include "../common/menu.h"
#include "../common/toolbar.h"
#include "../common/object.h"

#include <cx/mempool.h>


static UiObject* create_window(const char *title, BOOL simple, BOOL sidebar, BOOL splitview) {
    UiObject *obj = uic_object_new_toplevel();
    
    MainWindow *window = [[MainWindow alloc] init:obj withSidebar:sidebar withSplitview:splitview];
    [[WindowManager sharedWindowManager] addWindow:window];
    window.releasedWhenClosed = false; // TODO: we still need a cleanup strategy
    
    obj->wobj = (__bridge void*)window;
    
    MainWindowController *controller = [[MainWindowController alloc] initWithWindow:obj window:window];
    window.windowController = controller;
    [window setNextResponder:(NSResponder*)controller];
    objc_setAssociatedObject(window, "windowcontroller", controller, OBJC_ASSOCIATION_RETAIN);
    
    return obj;
}

UiObject* ui_window(const char *title, void *window_data) {
    UiObject *obj = create_window(title, FALSE, FALSE, FALSE);
    obj->window = window_data;
    return obj;
}

UiObject* ui_simple_window(const char *title, void *window_data) {
    UiObject *obj = create_window(title, TRUE, FALSE, FALSE);
    obj->window = window_data;
    return obj;
}

UiObject* ui_sidebar_window(const char *title, void *window_data) {
    UiObject *obj = create_window(title, FALSE, TRUE, FALSE);
    obj->window = window_data;
    return obj;
}

UiObject* ui_splitview_window(const char *title, UiBool sidebar) {
    return create_window(title, FALSE, sidebar, TRUE);
}

/* --------------------------------- File Dialogs --------------------------------- */

void ui_openfiledialog(UiObject *obj, unsigned int mode, ui_callback file_selected_callback, void *cbdata) {
    NSOpenPanel *openPanel = [NSOpenPanel openPanel];
    if((mode & UI_FILEDIALOG_SELECT_MULTI) == UI_FILEDIALOG_SELECT_MULTI) {
        openPanel.allowsMultipleSelection = YES;
    }
    if((mode & UI_FILEDIALOG_SELECT_FOLDER) == UI_FILEDIALOG_SELECT_FOLDER) {
        openPanel.canChooseFiles = NO;
        openPanel.canChooseDirectories = YES;
    }
    
    NSWindow *window = (__bridge NSWindow*)obj->wobj;
    [openPanel beginSheetModalForWindow:window completionHandler:^(NSModalResponse result) {
        UiEvent event;
        event.obj = obj;
        event.window = obj->window;
        event.document = obj->ctx->document;
        event.intval = 0;
        event.set = 0;
        
        UiFileList flist = { NULL, 0 };
        event.eventdata = &flist;
        event.eventdatatype = UI_EVENT_DATA_FILE_LIST;
        
        if(result == NSModalResponseOK) {
            NSArray<NSURL *> *urls = [openPanel URLs];
            flist.files = calloc(urls.count, sizeof(char*));
            for(NSURL *url in urls) {
                if([url isFileURL]) {
                    flist.files[flist.nfiles++] = strdup(url.path.UTF8String);
                }
            }
        }
        
        if(file_selected_callback) {
            file_selected_callback(&event, cbdata);
        }
        ui_filelist_free(flist);
    }];
}

void ui_savefiledialog(UiObject *obj, const char *name, ui_callback file_selected_callback, void *cbdata) {
    NSSavePanel *savePanel = [NSSavePanel savePanel];
    if(name) {
        NSString *nameStr = [[NSString alloc] initWithUTF8String:name];
        [savePanel setNameFieldStringValue: nameStr];
    }
    
    NSWindow *window = (__bridge NSWindow*)obj->wobj;
    [savePanel beginSheetModalForWindow:window completionHandler:^(NSModalResponse result) {
        UiEvent event;
        event.obj = obj;
        event.window = obj->window;
        event.document = obj->ctx->document;
        event.intval = 0;
        event.set = 0;
        
        UiFileList flist = { NULL, 0 };
        event.eventdata = &flist;
        event.eventdatatype = UI_EVENT_DATA_FILE_LIST;
        
        if(result == NSModalResponseOK) {
            NSURL *url = [savePanel URL];
            if([url isFileURL]) {
                NSString *path = url.path;
                flist.files = malloc(sizeof(char*));
                flist.files[0] = strdup(path.UTF8String);
                flist.nfiles = 1;
            }
            file_selected_callback(NULL, NULL);
        }
        if(file_selected_callback) {
            file_selected_callback(&event, cbdata);
        }
        ui_filelist_free(flist);
    }];
}

/* ------------------------------------- Dialog ------------------------------------- */

void ui_dialog_create(UiObject *parent, UiDialogArgs *args) {
    NSAlert *dialog = [[NSAlert alloc] init];
    
    if(args->title) {
        dialog.messageText = [[NSString alloc]initWithUTF8String:args->title];
    }
    if(args->content) {
        dialog.informativeText = [[NSString alloc]initWithUTF8String:args->content];
    }
    NSTextField *textfield = nil;
    if(args->input) {
        NSRect frame = NSMakeRect(0,0,300,22);
        textfield = args->password ? [[NSSecureTextField alloc] initWithFrame:frame] : [[NSTextField alloc]initWithFrame:frame];
        if(args->input_value) {
            textfield.stringValue = [[NSString alloc]initWithUTF8String:args->input_value];
        }
        dialog.accessoryView = textfield;
    }
    
    int b = 0;
    int b1 = -1;
    int b2 = -1;
    if(args->button1_label) {
        [dialog addButtonWithTitle:[[NSString alloc]initWithUTF8String:args->button1_label]];
        b1 = b++;
    }
    if(args->button2_label) {
        [dialog addButtonWithTitle:[[NSString alloc]initWithUTF8String:args->button2_label]];
        b2 = b;
    }
    if(args->closebutton_label) {
        [dialog addButtonWithTitle:[[NSString alloc]initWithUTF8String:args->closebutton_label]];
    }
    
    ui_callback callback = args->result;
    void *userdata = args->resultdata;
    
    NSWindow *window = (__bridge NSWindow*)parent->wobj;
    [dialog beginSheetModalForWindow:window completionHandler:^(NSModalResponse returnCode) {
        UiEvent event;
        event.obj = parent;
        event.window = event.obj->window;
        event.document = event.obj->ctx->document;
        event.eventdata = NULL;
        event.eventdatatype = 0;
        event.set = 0;
        event.intval = 0;
        
        long ret = returnCode - NSAlertFirstButtonReturn;
        if(ret == b1) {
            event.intval = 1;
        } else if(ret == b2) {
            event.intval = 2;
        }
        
        NSString *value = nil;
        if(textfield) {
            value = textfield.stringValue;
            event.eventdata = (void*)value.UTF8String;
            event.eventdatatype = UI_EVENT_DATA_STRING;
        }
        
        if(callback) {
            callback(&event, userdata);
        }
    }];
    
}

/* ------------------------------------- Dialog Window ------------------------------------- */

@implementation UiDialogWindow

- (BOOL) getIsVisible {
    return self.isVisible;
}

- (void) setVisible:(BOOL)visible {
    //[self makeKeyAndOrderFront:nil];
    if(visible) {
        [_parent beginSheet:self completionHandler:^(NSModalResponse returnCode) {
            // TODO: close event
        }];
    } else {
        [self.sheetParent endSheet:self returnCode:NSModalResponseCancel];
    }
}

- (void)cancelOperation:(id)sender {
    [self.sheetParent endSheet:self returnCode:NSModalResponseCancel];
    // TODO: close event
}

@end

UiObject *ui_dialog_window_create(UiObject *parent, UiDialogWindowArgs *args) {
    UiObject *obj = uic_object_new_toplevel();
    UiDialogWindow *panel = [[UiDialogWindow alloc] initWithContentRect:NSMakeRect(0, 0, args->width, args->height)
                                                 styleMask:(NSWindowStyleMaskTitled |
                                                             NSWindowStyleMaskClosable |
                                                             NSWindowStyleMaskResizable |
                                                             NSWindowStyleMaskUtilityWindow)
                                                   backing:NSBackingStoreBuffered
                                                     defer:NO];
    panel.parent = (__bridge NSWindow*)parent->wobj;
    panel.obj = obj;
    panel.modal = args->modal;
    panel.onclick = args->onclick;
    panel.onclickdata = args->onclickdata;
    [panel center];
    [[WindowManager sharedWindowManager] addWindow:panel];
    obj->wobj = (__bridge void*)panel;
    
    NSView *content = panel.contentView;
    
    // Create a view for the dialog window buttons (lbutton1, lbutton2, rbutton3, rbutton4)
    NSView *buttonArea = [[NSView alloc]init];
    buttonArea.translatesAutoresizingMaskIntoConstraints = NO;
    [content addSubview:buttonArea];
    [NSLayoutConstraint activateConstraints:@[
        [buttonArea.bottomAnchor constraintEqualToAnchor:content.bottomAnchor constant:-10],
        [buttonArea.leadingAnchor constraintEqualToAnchor:content.leadingAnchor constant:10],
        [buttonArea.trailingAnchor constraintEqualToAnchor:content.trailingAnchor constant:-10],
        [buttonArea.heightAnchor constraintEqualToConstant:20]
    ]];
    
    NSButton *lbutton1 = nil;
    if(args->lbutton1) {
        lbutton1 = [[NSButton alloc]init];
        lbutton1.title = [[NSString alloc]initWithUTF8String:args->lbutton1];
        lbutton1.translatesAutoresizingMaskIntoConstraints = NO;
        [buttonArea addSubview:lbutton1];
        [NSLayoutConstraint activateConstraints:@[
            [lbutton1.topAnchor constraintEqualToAnchor:buttonArea.topAnchor constant:0],
            [lbutton1.leadingAnchor constraintEqualToAnchor:buttonArea.leadingAnchor constant:0]
        ]];
        
        EventData *event = [[EventData alloc] init:args->onclick userdata:args->onclickdata];
        event.obj = obj;
        event.value = 1;
        lbutton1.target = event;
        lbutton1.action = @selector(handleEvent:);
        objc_setAssociatedObject(lbutton1, "eventdata", event, OBJC_ASSOCIATION_RETAIN);
    }
    NSButton *lbutton2 = nil;
    if(args->lbutton2) {
        lbutton2 = [[NSButton alloc]init];
        lbutton2.title = [[NSString alloc]initWithUTF8String:args->lbutton2];
        lbutton2.translatesAutoresizingMaskIntoConstraints = NO;
        [buttonArea addSubview:lbutton2];
        NSLayoutXAxisAnchor *anchor = lbutton1 != nil ? lbutton1.trailingAnchor : buttonArea.leadingAnchor;
        int off = lbutton1 != nil ? 4 : 0;
        [NSLayoutConstraint activateConstraints:@[
            [lbutton2.topAnchor constraintEqualToAnchor:buttonArea.topAnchor constant:0],
            [lbutton2.leadingAnchor constraintEqualToAnchor:anchor constant:off]
        ]];
        
        EventData *event = [[EventData alloc] init:args->onclick userdata:args->onclickdata];
        event.obj = obj;
        event.value = 2;
        lbutton2.target = event;
        lbutton2.action = @selector(handleEvent:);
        objc_setAssociatedObject(lbutton2, "eventdata", event, OBJC_ASSOCIATION_RETAIN);
    }
    
    NSButton *rbutton4 = nil;
    if(args->rbutton4) {
        rbutton4 = [[NSButton alloc]init];
        rbutton4.title = [[NSString alloc]initWithUTF8String:args->rbutton4];
        rbutton4.translatesAutoresizingMaskIntoConstraints = NO;
        [buttonArea addSubview:rbutton4];
        [NSLayoutConstraint activateConstraints:@[
            [rbutton4.topAnchor constraintEqualToAnchor:buttonArea.topAnchor constant:0],
            [rbutton4.trailingAnchor constraintEqualToAnchor:buttonArea.trailingAnchor constant:0]
        ]];
        
        EventData *event = [[EventData alloc] init:args->onclick userdata:args->onclickdata];
        event.obj = obj;
        event.value = 2;
        rbutton4.target = event;
        rbutton4.action = @selector(handleEvent:);
        objc_setAssociatedObject(rbutton4, "eventdata", event, OBJC_ASSOCIATION_RETAIN);
    }
    NSButton *rbutton3 = nil;
    if(args->rbutton3) {
        rbutton3 = [[NSButton alloc]init];
        rbutton3.title = [[NSString alloc]initWithUTF8String:args->rbutton3];
        rbutton3.translatesAutoresizingMaskIntoConstraints = NO;
        [buttonArea addSubview:rbutton3];
        NSLayoutXAxisAnchor *anchor = rbutton4 != nil ? rbutton4.leadingAnchor : buttonArea.trailingAnchor;
        int off = rbutton4 != nil ? -4 : 0;
        [NSLayoutConstraint activateConstraints:@[
            [rbutton3.topAnchor constraintEqualToAnchor:buttonArea.topAnchor constant:0],
            [rbutton3.trailingAnchor constraintEqualToAnchor:anchor constant:off]
        ]];
        
        EventData *event = [[EventData alloc] init:args->onclick userdata:args->onclickdata];
        event.obj = obj;
        event.value = 2;
        rbutton3.target = event;
        rbutton3.action = @selector(handleEvent:);
        objc_setAssociatedObject(rbutton3, "eventdata", event, OBJC_ASSOCIATION_RETAIN);
    }
    switch(args->default_button) {
        default: break;
        case 1: if(lbutton1 != nil) lbutton1.keyEquivalent = @"\r"; break;
        case 2: if(lbutton2 != nil) lbutton2.keyEquivalent = @"\r"; break;
        case 3: if(rbutton3 != nil) rbutton3.keyEquivalent = @"\r"; break;
        case 4: if(rbutton4 != nil) rbutton4.keyEquivalent = @"\r"; break;
    }
    
    // space between left and right buttons
    NSView *space = [[NSView alloc]init];
    space.translatesAutoresizingMaskIntoConstraints = NO;
    [buttonArea addSubview:space];
    NSLayoutXAxisAnchor *leftAnchor = buttonArea.leadingAnchor;
    NSLayoutXAxisAnchor *rightAnchor = buttonArea.trailingAnchor;
    if(lbutton2 != nil) {
        leftAnchor = lbutton2.trailingAnchor;
    } else if(lbutton1 != nil) {
        leftAnchor = lbutton1.trailingAnchor;
    }
    
    if(rbutton3 != nil) {
        rightAnchor = rbutton3.leadingAnchor;
    } else if(rbutton4 != nil) {
        rightAnchor = rbutton4.leadingAnchor;
    }
    [NSLayoutConstraint activateConstraints:@[
        [space.topAnchor constraintEqualToAnchor:buttonArea.topAnchor],
        [space.leadingAnchor constraintEqualToAnchor:leftAnchor constant:10],
        [space.trailingAnchor constraintEqualToAnchor:rightAnchor constant:-10]
    ]];
    
    // dialog window main content
    BoxContainer *vbox = [[BoxContainer alloc] init:NSUserInterfaceLayoutOrientationVertical spacing:0];
    vbox.translatesAutoresizingMaskIntoConstraints = NO;
    [content addSubview:vbox];
    
    [NSLayoutConstraint activateConstraints:@[
        [vbox.topAnchor constraintEqualToAnchor:content.topAnchor constant:0],
        [vbox.leadingAnchor constraintEqualToAnchor:content.leadingAnchor],
        [vbox.trailingAnchor constraintEqualToAnchor:content.trailingAnchor],
        [vbox.bottomAnchor constraintEqualToAnchor:buttonArea.topAnchor constant:0]
    ]];
     
    UiContainerX *container = ui_create_container(obj, vbox);
    vbox.container = container;
    uic_object_push_container(obj, container);
    
    return obj;
}

mercurial