UNIXworkcode

1 /* 2 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. 3 * 4 * Copyright 2024 Olaf Wintermann. All rights reserved. 5 * 6 * Redistribution and use in source and binary forms, with or without 7 * modification, are permitted provided that the following conditions are met: 8 * 9 * 1. Redistributions of source code must retain the above copyright 10 * notice, this list of conditions and the following disclaimer. 11 * 12 * 2. Redistributions in binary form must reproduce the above copyright 13 * notice, this list of conditions and the following disclaimer in the 14 * documentation and/or other materials provided with the distribution. 15 * 16 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 19 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 20 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 21 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 22 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 23 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 24 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 25 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 * POSSIBILITY OF SUCH DAMAGE. 27 */ 28 29 #import "MainWindow.h" 30 #import "Container.h" 31 #import "GridLayout.h" 32 #import "BoxContainer.h" 33 #import "../common/object.h" 34 #import "../ui/properties.h" 35 #import <objc/runtime.h> 36 37 #import "EventData.h" 38 #import "menu.h" 39 #import "Toolbar.h" 40 41 @implementation MainWindow 42 43 - (MainWindow*)init:(UiObject*)obj withSidebar:(BOOL)hasSidebar withSplitview:(BOOL)hasSplitview{ 44 NSRect frame = NSMakeRect(300, 200, 600, 500); 45 46 self = [self initWithContentRect:frame 47 styleMask:NSWindowStyleMaskTitled | 48 NSWindowStyleMaskResizable | 49 NSWindowStyleMaskClosable | 50 NSWindowStyleMaskMiniaturizable 51 backing:NSBackingStoreBuffered 52 defer:false]; 53 _obj = obj; 54 55 56 int top = 4; 57 NSView *content = self.contentView; 58 59 // A sidebar or splitview window need a NSSplitView 60 NSSplitView *splitview; 61 if(hasSidebar || hasSplitview) { 62 self.styleMask |= NSWindowStyleMaskFullSizeContentView; 63 self.titleVisibility = NSWindowTitleHidden; 64 self.titlebarAppearsTransparent = YES; 65 66 splitview = [[NSSplitView alloc]init]; 67 splitview.vertical = YES; 68 splitview.dividerStyle = NSSplitViewDividerStyleThin; 69 splitview.translatesAutoresizingMaskIntoConstraints = false; 70 [self.contentView addSubview:splitview]; 71 _splitview = splitview; 72 73 [NSLayoutConstraint activateConstraints:@[ 74 [splitview.topAnchor constraintEqualToAnchor:self.contentView.topAnchor constant:0], 75 [splitview.leadingAnchor constraintEqualToAnchor:self.contentView.leadingAnchor], 76 [splitview.trailingAnchor constraintEqualToAnchor:self.contentView.trailingAnchor], 77 [splitview.bottomAnchor constraintEqualToAnchor:self.contentView.bottomAnchor] 78 ]]; 79 80 top = 34; 81 } 82 83 if(hasSidebar) { 84 // add the sidebar 85 const char *sidebarMaterialProperty = ui_get_property("ui.cocoa.sidebar.usematerial"); 86 BOOL useMaterial = YES; 87 if(sidebarMaterialProperty && (sidebarMaterialProperty[0] == 'f' || sidebarMaterialProperty[0] == 'F')) { 88 useMaterial = NO; 89 } 90 91 if(useMaterial) { 92 NSVisualEffectView *v = [[NSVisualEffectView alloc] initWithFrame:NSMakeRect(0,0,0,0)]; 93 v.material = NSVisualEffectMaterialSidebar; 94 v.blendingMode = NSVisualEffectBlendingModeBehindWindow; 95 v.state = NSVisualEffectStateActive; 96 _sidebar = v; 97 } else { 98 _sidebar = [[NSView alloc]initWithFrame:NSMakeRect(0,0,0,0)]; 99 } 100 _sidebar.translatesAutoresizingMaskIntoConstraints = NO; 101 [splitview addArrangedSubview:_sidebar]; 102 [_sidebar.widthAnchor constraintGreaterThanOrEqualToConstant:250].active = YES; 103 } 104 if(hasSplitview) { 105 // add the splitview window left/right panels 106 _leftPanel = [[NSView alloc]initWithFrame:NSMakeRect(0,0,100,100)]; 107 [splitview addArrangedSubview:_leftPanel]; 108 _rightPanel = [[NSView alloc]initWithFrame:NSMakeRect(0,0,100,100)]; 109 [splitview addArrangedSubview:_rightPanel]; 110 } else if(hasSidebar) { 111 // sidebar only window: add content view 112 content = [[NSView alloc]initWithFrame:NSMakeRect(0,0,100,100)]; 113 [splitview addArrangedSubview:content]; 114 } 115 116 // normal or sidebar-only windows get a container 117 if(!hasSplitview) { 118 // create a vertical stackview as default container 119 BoxContainer *vbox = [[BoxContainer alloc] init:NSUserInterfaceLayoutOrientationVertical spacing:0]; 120 //GridLayout *vbox = [[GridLayout alloc] init]; 121 vbox.translatesAutoresizingMaskIntoConstraints = false; 122 [content addSubview:vbox]; 123 [NSLayoutConstraint activateConstraints:@[ 124 [vbox.topAnchor constraintEqualToAnchor:content.topAnchor constant:top], 125 [vbox.leadingAnchor constraintEqualToAnchor:content.leadingAnchor], 126 [vbox.trailingAnchor constraintEqualToAnchor:content.trailingAnchor], 127 [vbox.bottomAnchor constraintEqualToAnchor:content.bottomAnchor], 128 ]]; 129 UiContainerX *container = ui_create_container(obj, vbox); 130 vbox.container = container; 131 uic_object_push_container(obj, container); 132 } 133 _topOffset = top; 134 135 if(uic_toolbar_isenabled()) { 136 UiToolbar *toolbar = [[UiToolbar alloc]initWithWindow:self]; 137 [self setToolbar:toolbar]; 138 } 139 140 141 return self; 142 } 143 144 - (BOOL) getIsVisible { 145 return [self isVisible]; 146 } 147 148 - (void) setVisible:(BOOL)visible { 149 if(visible) { 150 [self makeKeyAndOrderFront:nil]; 151 } else { 152 [self close]; 153 } 154 } 155 156 157 @end 158 159 160 @implementation MainWindowController 161 162 - (MainWindowController*)initWithWindow:(UiObject*)obj window:(NSWindow*)window { 163 self = [super initWithWindow:window]; 164 _uiobj = obj; 165 166 self.checkItemStates = [[NSMutableDictionary alloc] init]; 167 self.radioItems = [[NSMutableDictionary alloc] init]; 168 169 // bind all stateful menu items (checkbox, radiobuttons, lists) 170 NSArray *menuBindItems = ui_get_binding_items(); // returns all items that require binding 171 for(MenuItem *item in menuBindItems) { 172 if(item.checkItem || item.radioItem) { 173 // simple check item (ui_menu_toggleitem_create) 174 UiVar *var = uic_widget_var(obj->ctx, obj->ctx, NULL, item.checkItem ? item.checkItem->varname : item.radioItem->varname, UI_VAR_INTEGER); 175 // create the state object for this item/window 176 MenuItemState *state = [[MenuItemState alloc] init]; 177 state.mainWindow = self; 178 state.var = var; 179 if(var) { 180 UiInteger *i = var->value; 181 if(item.checkItem) { 182 // bind toggle item 183 state.state = (int)i->value; 184 i->obj = (__bridge void*)state; 185 i->get = ui_menu_check_item_get; 186 i->set = ui_menu_check_item_set; 187 } else { 188 // bind radio item 189 NSMutableArray *rgroup = nil; 190 if(i->obj) { 191 rgroup = (__bridge NSMutableArray*)i->obj; 192 } else { 193 // create a new rgroup array and register it in the window 194 rgroup = [[NSMutableArray alloc] init]; 195 NSString *varname = [[NSString alloc] initWithUTF8String:item.radioItem->varname]; 196 [_radioItems setObject:rgroup forKey:varname]; 197 i->obj = (__bridge void*)rgroup; 198 } 199 i->get = ui_menu_radio_item_get; 200 i->set = ui_menu_radio_item_set; 201 [rgroup addObject:state]; // add this item state to the radio group 202 // i->value can contain a non-zero value, which means a specific radiobutton 203 // should be pre-selected 204 if(i->value == rgroup.count) { 205 state.state = NSControlStateValueOn; 206 } 207 } 208 } else { 209 state.state = 0; 210 } 211 [_checkItemStates setObject:state forKey:item.itemId]; 212 } 213 } 214 215 return self; 216 } 217 218 - (void) windowDidLoad { 219 [self.window setNextResponder:self]; 220 } 221 222 - (void)menuItemAction:(id)sender { 223 EventData *event = objc_getAssociatedObject(sender, "eventdata"); 224 if(event) { 225 if(event.obj) { 226 [event handleEvent:sender]; 227 } else { 228 event.obj = self.uiobj; 229 [event handleEvent:sender]; 230 event.obj = NULL; 231 } 232 } 233 } 234 235 - (void)menuCheckItemAction:(id)sender { 236 NSMenuItem *menuItem = sender; 237 MenuItem *item = objc_getAssociatedObject(sender, "menuitem"); 238 if(!item || !item.checkItem) { 239 return; 240 } 241 242 MenuItemState *state = [_checkItemStates objectForKey:item.itemId]; 243 state.state = state.state == NSControlStateValueOff ? NSControlStateValueOn : NSControlStateValueOff; 244 menuItem.state = state.state; 245 246 UiMenuCheckItem *it = item.checkItem; 247 if(it->callback) { 248 UiEvent event; 249 event.obj = _uiobj; 250 event.window = event.obj->window; 251 event.document = event.obj->ctx->document; 252 event.eventdata = state.var ? state.var->value : NULL; 253 event.intval = state.state; 254 it->callback(&event, it->userdata); 255 } 256 } 257 258 - (void)menuRadioItemAction:(id)sender { 259 NSMenuItem *menuItem = sender; 260 MenuItem *item = objc_getAssociatedObject(sender, "menuitem"); 261 if(!item || !item.radioItem) { 262 return; 263 } 264 265 UiMenuRadioItem *it = item.radioItem; 266 if(!it->varname) { 267 return; 268 } 269 270 MenuItemState *state = [_checkItemStates objectForKey:item.itemId]; // current state of this menu item 271 272 NSString *varname = [[NSString alloc] initWithUTF8String:it->varname]; 273 NSArray *radioGroup = [_radioItems objectForKey:varname]; 274 if(!radioGroup) { 275 return; 276 } 277 int index = 1; 278 int value = 0; 279 for(MenuItemState *g in radioGroup) { 280 if(g == state) { 281 menuItem.state = NSControlStateValueOn; 282 g.state = NSControlStateValueOn; 283 value = index; 284 } else { 285 menuItem.state = NSControlStateValueOff; 286 g.state = NSControlStateValueOff; 287 } 288 } 289 290 if(it->callback) { 291 UiEvent event; 292 event.obj = _uiobj; 293 event.window = event.obj->window; 294 event.document = event.obj->ctx->document; 295 event.eventdata = state.var ? state.var->value : NULL; 296 event.intval = value; 297 it->callback(&event, it->userdata); 298 } 299 } 300 301 302 - (BOOL) validateMenuItem:(NSMenuItem *) menuItem { 303 MenuItem *item = objc_getAssociatedObject(menuItem, "menuitem"); 304 if(item) { 305 MenuItemState *state = [_checkItemStates objectForKey:item.itemId]; 306 if(state) { 307 menuItem.state = state.state; 308 } else { 309 menuItem.state = NSControlStateValueOff; 310 } 311 } 312 313 return YES; 314 } 315 316 @end 317 318 @implementation MenuItemState 319 320 @end 321 322 int64_t ui_menu_check_item_get(UiInteger *i) { 323 MenuItemState *state = (__bridge MenuItemState*)i->obj; 324 i->value = state.state; 325 return i->value; 326 } 327 328 void ui_menu_check_item_set(UiInteger *i, int64_t value) { 329 MenuItemState *state = (__bridge MenuItemState*)i->obj; 330 i->value = value; 331 state.state = (int)value; 332 } 333 334 int64_t ui_menu_radio_item_get(UiInteger *i) { 335 NSArray *rgroup = (__bridge NSArray*)i->obj; 336 i->value = 0; 337 int index = 1; 338 for(MenuItemState *state in rgroup) { 339 if(state.state == NSControlStateValueOn) { 340 i->value = index; 341 break; 342 } 343 index++; 344 } 345 return i->value; 346 } 347 348 void ui_menu_radio_item_set(UiInteger *i, int64_t value) { 349 NSArray *rgroup = (__bridge NSArray*)i->obj; 350 i->value = 0; 351 int index = 1; 352 for(MenuItemState *state in rgroup) { 353 state.state = value == index; 354 index++; 355 } 356 } 357 358 359 UIWIDGET ui_sidebar_create(UiObject *obj, UiSidebarArgs *args) { 360 MainWindow *window = (__bridge MainWindow*)obj->wobj; 361 if(window.sidebar == nil) { 362 return NULL; 363 } 364 NSView *sidebar = window.sidebar; 365 366 // create a vertical stackview as default container 367 BoxContainer *vbox = [[BoxContainer alloc] init:NSUserInterfaceLayoutOrientationVertical spacing:args->spacing]; 368 vbox.container = ui_create_container(obj, vbox); 369 //GridLayout *vbox = [[GridLayout alloc] init]; 370 vbox.translatesAutoresizingMaskIntoConstraints = false; 371 [sidebar addSubview:vbox]; 372 [NSLayoutConstraint activateConstraints:@[ 373 [vbox.topAnchor constraintEqualToAnchor:sidebar.topAnchor constant:34], 374 [vbox.leadingAnchor constraintEqualToAnchor:sidebar.leadingAnchor], 375 [vbox.trailingAnchor constraintEqualToAnchor:sidebar.trailingAnchor], 376 [vbox.bottomAnchor constraintEqualToAnchor:sidebar.bottomAnchor] 377 ]]; 378 uic_object_push_container(obj, vbox.container); 379 380 return NULL; 381 } 382 383 static UIWIDGET splitview_window_add_panel(UiObject *obj, NSView *panel, UiSidebarArgs *args) { 384 MainWindow *window = (__bridge MainWindow*)obj->wobj; 385 BoxContainer *vbox = [[BoxContainer alloc] init:NSUserInterfaceLayoutOrientationVertical spacing:0]; 386 //GridLayout *vbox = [[GridLayout alloc] init]; 387 vbox.container = ui_create_container(obj, vbox); 388 vbox.translatesAutoresizingMaskIntoConstraints = false; 389 [panel addSubview:vbox]; 390 [NSLayoutConstraint activateConstraints:@[ 391 [vbox.topAnchor constraintEqualToAnchor:panel.topAnchor constant:window.topOffset], 392 [vbox.leadingAnchor constraintEqualToAnchor:panel.leadingAnchor], 393 [vbox.trailingAnchor constraintEqualToAnchor:panel.trailingAnchor], 394 [vbox.bottomAnchor constraintEqualToAnchor:panel.bottomAnchor], 395 ]]; 396 uic_object_push_container(obj, vbox.container); 397 return (__bridge void*)vbox; 398 } 399 400 UIWIDGET ui_left_panel_create(UiObject *obj, UiSidebarArgs *args) { 401 MainWindow *window = (__bridge MainWindow*)obj->wobj; 402 if(window.leftPanel == nil) { 403 return NULL; 404 } 405 return splitview_window_add_panel(obj, window.leftPanel, args); 406 } 407 408 UIWIDGET ui_right_panel_create(UiObject *obj, UiSidebarArgs *args) { 409 MainWindow *window = (__bridge MainWindow*)obj->wobj; 410 if(window.rightPanel == nil) { 411 return NULL; 412 } 413 return splitview_window_add_panel(obj, window.rightPanel, args); 414 } 415 416