/*
* 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 "entry.h"
#import <objc/runtime.h>
@implementation UiSpinBox
- (UiSpinBox*)init {
return self;
}
- (void)valueChanged {
float value = _stepper.doubleValue;
UiEvent e;
e.obj = _obj;
e.window = e.obj->window;
e.document = e.obj->ctx->document;
e.eventdata = NULL;
e.eventdatatype = 0;
e.intval = (int)value;
e.set = ui_get_setop();
if(_onchange) {
_onchange(&e, _onchangedata);
}
if(_observers) {
UiObserver *observer = *_observers;
ui_notify_evt(observer, &e);
}
}
- (void)stepperChanged:(id)sender {
if(_isInteger) {
_textfield.integerValue = _stepper.integerValue;
} else {
_textfield.doubleValue = _stepper.doubleValue;
}
[self valueChanged];
}
- (void) controlTextDidChange:(NSNotification *)obj {
if(_isInteger) {
_stepper.integerValue = _textfield.integerValue;
} else {
_stepper.doubleValue = _textfield.doubleValue;
}
[self valueChanged];
}
@end
UIWIDGET ui_spinbox_create(UiObject *obj, UiSpinBoxArgs *args) {
double min = args->min;
double max = args->max != 0 ? args->max : 1000;
UiVar *var = NULL;
UiVarType vartype = 0;
if(args->varname) {
var = uic_get_var(obj->ctx, args->varname);
if(var) {
vartype = var->type;
} else {
var = uic_widget_var(obj->ctx, obj->ctx, args->rangevalue, args->varname, UI_VAR_RANGE);
vartype = UI_VAR_RANGE;
}
}
if(!var) {
if(args->intvalue) {
var = uic_widget_var(obj->ctx, obj->ctx, args->intvalue, NULL, UI_VAR_INTEGER);
vartype = UI_VAR_INTEGER;
} else if(args->doublevalue) {
var = uic_widget_var(obj->ctx, obj->ctx, args->doublevalue, NULL, UI_VAR_DOUBLE);
vartype = UI_VAR_DOUBLE;
} else if(args->rangevalue) {
var = uic_widget_var(obj->ctx, obj->ctx, args->rangevalue, NULL, UI_VAR_RANGE);
vartype = UI_VAR_RANGE;
}
}
if(vartype == UI_VAR_RANGE) {
UiRange *r = var->value;
min = r->min;
max = r->max;
}
if(args->step == 0) {
args->step = 1;
}
// create and setup textfield for number input
NSTextField *textfield = [[NSTextField alloc] init];
textfield.translatesAutoresizingMaskIntoConstraints = NO;
if(!args->hfill || args->width > 0) {
textfield.translatesAutoresizingMaskIntoConstraints = NO;
int width = args->width > 0 ? args->width : 100;
[[textfield.widthAnchor constraintEqualToConstant:width] setActive:YES];
}
NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
formatter.numberStyle = NSNumberFormatterDecimalStyle;
formatter.allowsFloats = vartype != UI_VAR_INTEGER;
formatter.minimumFractionDigits = args->digits;
formatter.maximumFractionDigits = args->digits;
textfield.formatter = formatter;
// create view containing the textfield and stepper
NSView *view = [[NSView alloc]init];
view.translatesAutoresizingMaskIntoConstraints = NO;
NSStepper *stepper = [[NSStepper alloc] init];
stepper.translatesAutoresizingMaskIntoConstraints = NO;
[view addSubview:textfield];
[view addSubview:stepper];
[NSLayoutConstraint activateConstraints:@[
[textfield.leadingAnchor constraintEqualToAnchor:view.leadingAnchor],
[textfield.topAnchor constraintEqualToAnchor:view.topAnchor],
[textfield.bottomAnchor constraintEqualToAnchor:view.bottomAnchor],
[stepper.trailingAnchor constraintEqualToAnchor:view.trailingAnchor],
[stepper.topAnchor constraintEqualToAnchor:view.topAnchor],
[stepper.bottomAnchor constraintEqualToAnchor:view.bottomAnchor],
[textfield.trailingAnchor constraintEqualToAnchor:stepper.leadingAnchor]
]];
UiLayout layout = UI_INIT_LAYOUT(args);
ui_container_add(obj, view, &layout);
// create the spinbox object, that handles all textfield and stepper events
UiSpinBox *spinbox = [[UiSpinBox alloc]init];
spinbox.obj = obj;
spinbox.textfield = textfield;
spinbox.stepper = stepper;
spinbox.onchange = args->onchange;
spinbox.onchangedata = args->onchangedata;
spinbox.isInteger = vartype == UI_VAR_INTEGER;
objc_setAssociatedObject(stepper, "ui_spinbox", spinbox, OBJC_ASSOCIATION_RETAIN);
stepper.minValue = min;
stepper.maxValue = max;
stepper.increment = args->step;
stepper.target = spinbox;
stepper.action = @selector(stepperChanged:);
textfield.delegate = spinbox;
UiObserver **obs = NULL;
if(var) {
void *varObj = (__bridge void*)spinbox;
switch(vartype) {
default: break;
case UI_VAR_INTEGER: {
UiInteger *i = var->value;
i->get = ui_spinbutton_getint;
i->set = ui_spinbutton_setint;
i->obj = varObj;
obs = &i->observers;
stepper.integerValue = i->value;
textfield.integerValue = i->value;
break;
}
case UI_VAR_DOUBLE: {
UiDouble *d = var->value;
d->get = ui_spinbutton_getdouble;
d->set = ui_spinbutton_setdouble;
d->obj = varObj;
obs = &d->observers;
stepper.doubleValue = d->value;
textfield.doubleValue = d->value;
break;
}
case UI_VAR_RANGE: {
UiRange *r = var->value;
r->get = ui_spinbutton_getrangeval;
r->set = ui_spinbutton_setrangeval;
r->setrange = ui_spinbutton_setrange;
r->setextent = ui_spinbutton_setextent;
r->obj = varObj;
obs = &r->observers;
stepper.doubleValue = r->value;
textfield.doubleValue = r->value;
break;
}
}
}
spinbox.observers = obs;
return (__bridge void*)textfield;
}
int64_t ui_spinbutton_getint(UiInteger *i) {
UiSpinBox *spinbox = (__bridge UiSpinBox*)i->obj;
i->value = spinbox.stepper.integerValue;
return i->value;
}
void ui_spinbutton_setint(UiInteger *i, int64_t val) {
UiSpinBox *spinbox = (__bridge UiSpinBox*)i->obj;
i->value = val;
spinbox.stepper.integerValue = val;
spinbox.textfield.integerValue = val;
}
double ui_spinbutton_getdouble(UiDouble *d) {
UiSpinBox *spinbox = (__bridge UiSpinBox*)d->obj;
d->value = spinbox.stepper.doubleValue;
return d->value;
}
void ui_spinbutton_setdouble(UiDouble *d, double val) {
UiSpinBox *spinbox = (__bridge UiSpinBox*)d->obj;
d->value = val;
spinbox.stepper.doubleValue = val;
spinbox.textfield.doubleValue = val;
}
double ui_spinbutton_getrangeval(UiRange *r) {
UiSpinBox *spinbox = (__bridge UiSpinBox*)r->obj;
r->value = spinbox.stepper.doubleValue;
return r->value;
}
void ui_spinbutton_setrangeval(UiRange *r, double val) {
UiSpinBox *spinbox = (__bridge UiSpinBox*)r->obj;
r->value = val;
spinbox.stepper.doubleValue = val;
spinbox.textfield.doubleValue = val;
}
void ui_spinbutton_setrange(UiRange *r, double min, double max) {
UiSpinBox *spinbox = (__bridge UiSpinBox*)r->obj;
spinbox.stepper.minValue = min;
spinbox.stepper.maxValue = max;
}
void ui_spinbutton_setextent(UiRange *r, double extent) {
UiSpinBox *spinbox = (__bridge UiSpinBox*)r->obj;
spinbox.stepper.increment = extent;
}