diff --git a/common/controlsigs.h b/common/controlsigs.h index dc984329..38aa4de9 100644 --- a/common/controlsigs.h +++ b/common/controlsigs.h @@ -7,6 +7,7 @@ #define uiColorButtonSignature 0x436F6C42 #define uiComboboxSignature 0x436F6D62 #define uiDateTimePickerSignature 0x44545069 +#define uiEditableComboboxSignature 0x45644362 #define uiEntrySignature 0x456E7472 #define uiFontButtonSignature 0x466F6E42 #define uiGroupSignature 0x47727062 diff --git a/darwin/GNUfiles.mk b/darwin/GNUfiles.mk index 810ddd16..ae39d70e 100644 --- a/darwin/GNUfiles.mk +++ b/darwin/GNUfiles.mk @@ -15,6 +15,7 @@ MFILES += \ darwin/debug.m \ darwin/draw.m \ darwin/drawtext.m \ + darwin/editablecombo.m \ darwin/entry.m \ darwin/fontbutton.m \ darwin/group.m \ diff --git a/darwin/combobox.m b/darwin/combobox.m index 9f63157a..e956762e 100644 --- a/darwin/combobox.m +++ b/darwin/combobox.m @@ -5,37 +5,17 @@ // NSPopUpButton is fine. #define comboboxWidth 96 -@interface libui_intrinsicWidthNSComboBox : NSComboBox -@end - -@implementation libui_intrinsicWidthNSComboBox - -- (NSSize)intrinsicContentSize -{ - NSSize s; - - s = [super intrinsicContentSize]; - s.width = comboboxWidth; - return s; -} - -@end - struct uiCombobox { uiDarwinControl c; - BOOL editable; NSPopUpButton *pb; NSArrayController *pbac; - NSComboBox *cb; - NSView *handle; // for uiControlHandle() void (*onSelected)(uiCombobox *, void *); void *onSelectedData; }; -@interface comboboxDelegateClass : NSObject { +@interface comboboxDelegateClass : NSObject { struct mapTable *comboboxes; } -- (void)comboBoxSelectionDidChange:(NSNotification *)note; - (IBAction)onSelected:(id)sender; - (void)registerCombobox:(uiCombobox *)c; - (void)unregisterCombobox:(uiCombobox *)c; @@ -57,98 +37,57 @@ struct uiCombobox { [super dealloc]; } -// note: does not trigger when text changed -// TODO not perfect either: -// - triggered when keyboard navigating the open menu -// - does not trigger when the text is changed to a menu item (which normally selects that item; IDK how to inhibit that behavior - TODO) -- (void)comboBoxSelectionDidChange:(NSNotification *)note -{ - [self onSelected:[note object]]; -} - - (IBAction)onSelected:(id)sender { uiCombobox *c; - c = (uiCombobox *) mapGet(self->comboboxes, sender); + c = uiCombobox(mapGet(self->comboboxes, sender)); (*(c->onSelected))(c, c->onSelectedData); } - (void)registerCombobox:(uiCombobox *)c { - mapSet(self->comboboxes, c->handle, c); - if (c->editable) - [c->cb setDelegate:self]; - else { - [c->pb setTarget:self]; - [c->pb setAction:@selector(onSelected:)]; - } + mapSet(self->comboboxes, c->pb, c); + [c->pb setTarget:self]; + [c->pb setAction:@selector(onSelected:)]; } - (void)unregisterCombobox:(uiCombobox *)c { - if (c->editable) - [c->cb setDelegate:nil]; - else - [c->pb setTarget:nil]; - mapDelete(self->comboboxes, c->handle); + [c->pb setTarget:nil]; + mapDelete(self->comboboxes, c->pb); } @end static comboboxDelegateClass *comboboxDelegate = nil; -uiDarwinControlAllDefaultsExceptDestroy(uiCombobox, handle) +uiDarwinControlAllDefaultsExceptDestroy(uiCombobox, pb) static void uiComboboxDestroy(uiControl *cc) { uiCombobox *c = uiCombobox(cc); [comboboxDelegate unregisterCombobox:c]; - if (!c->editable) { - [c->pb unbind:@"contentObjects"]; - [c->pb unbind:@"selectedIndex"]; - [c->pbac release]; - } - [c->handle release]; + [c->pb unbind:@"contentObjects"]; + [c->pb unbind:@"selectedIndex"]; + [c->pbac release]; + [c->pb release]; uiFreeControl(uiControl(c)); } void uiComboboxAppend(uiCombobox *c, const char *text) { - if (c->editable) - [c->cb addItemWithObjectValue:toNSString(text)]; - else - [c->pbac addObject:toNSString(text)]; + [c->pbac addObject:toNSString(text)]; } intmax_t uiComboboxSelected(uiCombobox *c) { - if (c->editable) - return [c->cb indexOfSelectedItem]; return [c->pb indexOfSelectedItem]; } void uiComboboxSetSelected(uiCombobox *c, intmax_t n) { - if (c->editable) { - // see https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ComboBox/Tasks/SettingComboBoxValue.html#//apple_ref/doc/uid/20000256 - id delegate; - - // this triggers the delegate; turn it off for now - delegate = [c->cb delegate]; - [c->cb setDelegate:nil]; - - // this seems to work fine for -1 too - [c->cb selectItemAtIndex:n]; - if (n == -1) - [c->cb setObjectValue:@""]; - else - [c->cb setObjectValue:[c->cb objectValueOfSelectedItem]]; - - [c->cb setDelegate:delegate]; - return; - } [c->pb selectItemAtIndex:n]; } @@ -163,47 +102,35 @@ static void defaultOnSelected(uiCombobox *c, void *data) // do nothing } -static uiCombobox *finishNewCombobox(BOOL editable) +uiCombobox *uiNewCombobox(void) { uiCombobox *c; + NSPopUpButtonCell *pbcell; uiDarwinNewControl(uiCombobox, c); - c->editable = editable; - if (c->editable) { - c->cb = [[libui_intrinsicWidthNSComboBox alloc] initWithFrame:NSZeroRect]; - [c->cb setUsesDataSource:NO]; - [c->cb setButtonBordered:YES]; - [c->cb setCompletes:NO]; - uiDarwinSetControlFont(c->cb, NSRegularControlSize); - c->handle = c->cb; - } else { - NSPopUpButtonCell *pbcell; + c->pb = [[NSPopUpButton alloc] initWithFrame:NSZeroRect pullsDown:NO]; + [c->pb setPreferredEdge:NSMinYEdge]; + pbcell = (NSPopUpButtonCell *) [c->pb cell]; + [pbcell setArrowPosition:NSPopUpArrowAtBottom]; + // TODO font - c->pb = [[NSPopUpButton alloc] initWithFrame:NSZeroRect pullsDown:NO]; - [c->pb setPreferredEdge:NSMinYEdge]; - pbcell = (NSPopUpButtonCell *) [c->pb cell]; - [pbcell setArrowPosition:NSPopUpArrowAtBottom]; - // TODO font - c->handle = c->pb; - - // NSPopUpButton doesn't work like a combobox - // - it automatically selects the first item - // - it doesn't support duplicates - // but we can use a NSArrayController and Cocoa bindings to bypass these restrictions - c->pbac = [NSArrayController new]; - [c->pbac setAvoidsEmptySelection:NO]; - [c->pbac setSelectsInsertedObjects:NO]; - [c->pbac setAutomaticallyRearrangesObjects:NO]; - [c->pb bind:@"contentValues" - toObject:c->pbac - withKeyPath:@"arrangedObjects" - options:nil]; - [c->pb bind:@"selectedIndex" - toObject:c->pbac - withKeyPath:@"selectionIndex" - options:nil]; - } + // NSPopUpButton doesn't work like a combobox + // - it automatically selects the first item + // - it doesn't support duplicates + // but we can use a NSArrayController and Cocoa bindings to bypass these restrictions + c->pbac = [NSArrayController new]; + [c->pbac setAvoidsEmptySelection:NO]; + [c->pbac setSelectsInsertedObjects:NO]; + [c->pbac setAutomaticallyRearrangesObjects:NO]; + [c->pb bind:@"contentValues" + toObject:c->pbac + withKeyPath:@"arrangedObjects" + options:nil]; + [c->pb bind:@"selectedIndex" + toObject:c->pbac + withKeyPath:@"selectionIndex" + options:nil]; if (comboboxDelegate == nil) { comboboxDelegate = [comboboxDelegateClass new]; @@ -214,13 +141,3 @@ static uiCombobox *finishNewCombobox(BOOL editable) return c; } - -uiCombobox *uiNewCombobox(void) -{ - return finishNewCombobox(NO); -} - -uiCombobox *uiNewEditableCombobox(void) -{ - return finishNewCombobox(YES); -} diff --git a/darwin/editablecombo.m b/darwin/editablecombo.m new file mode 100644 index 00000000..5fa868fd --- /dev/null +++ b/darwin/editablecombo.m @@ -0,0 +1,179 @@ +// 14 august 2015 +#import "uipriv_darwin.h" + +// NSComboBoxes have no intrinsic width; we'll use the default Interface Builder width for them. +#define comboboxWidth 96 + +@interface libui_intrinsicWidthNSComboBox : NSComboBox +@end + +@implementation libui_intrinsicWidthNSComboBox + +- (NSSize)intrinsicContentSize +{ + NSSize s; + + s = [super intrinsicContentSize]; + s.width = comboboxWidth; + return s; +} + +@end + +struct uiEditableCombobox { + uiDarwinControl c; + NSComboBox *cb; + void (*onChanged)(uiEditableCombobox *, void *); + void *onChangedData; +}; + +@interface editableComboboxDelegateClass : NSObject { + struct mapTable *comboboxes; +} +- (void)controlTextDidChange:(NSNotification *)note; +- (void)comboBoxSelectionDidChange:(NSNotification *)note; +- (void)registerCombobox:(uiEditableCombobox *)c; +- (void)unregisterCombobox:(uiEditableCombobox *)c; +@end + +@implementation editableComboboxDelegateClass + +- (id)init +{ + self = [super init]; + if (self) + self->comboboxes = newMap(); + return self; +} + +- (void)dealloc +{ + mapDestroy(self->comboboxes); + [super dealloc]; +} + +- (void)controlTextDidChange:(NSNotification *)note +{ + uiEditableCombobox *c; + + c = uiEditableCombobox(mapGet(self->comboboxes, [note object])); + (*(c->onChanged))(c, c->onChangedData); +} + +// the above doesn't handle when an item is selected; this will +- (void)comboBoxSelectionDidChange:(NSNotification *)note +{ + // except this is sent BEFORE the entry is changed, and that doesn't send the above, so + // this is via http://stackoverflow.com/a/21059819/3408572 - it avoids the need to manage selected items + // this still isn't perfect — I get residual changes to the same value while navigating the list — but it's good enough + [self performSelector:@selector(controlTextDidChange:) + withObject:note + afterDelay:0]; +} + +- (void)registerCombobox:(uiEditableCombobox *)c +{ + mapSet(self->comboboxes, c->cb, c); + [c->cb setDelegate:self]; +} + +- (void)unregisterCombobox:(uiEditableCombobox *)c +{ + [c->cb setDelegate:nil]; + mapDelete(self->comboboxes, c->cb); +} + +@end + +static editableComboboxDelegateClass *comboboxDelegate = nil; + +uiDarwinControlAllDefaultsExceptDestroy(uiEditableCombobox, cb) + +static void uiEditableComboboxDestroy(uiControl *cc) +{ + uiEditableCombobox *c = uiEditableCombobox(cc); + + [comboboxDelegate unregisterCombobox:c]; + [c->cb release]; + uiFreeControl(uiControl(c)); +} + +void uiEditableComboboxAppend(uiEditableCombobox *c, const char *text) +{ + [c->cb addItemWithObjectValue:toNSString(text)]; +} + +char *uiEditableComboboxText(uiEditableCombobox *c) +{ + return uiDarwinNSStringToText([c->cb stringValue]); +} + +void uiEditableComboboxSetText(uiEditableCombobox *c, const char *text) +{ + NSString *t; + + t = toNSString(text); + [c->cb setStringValue:t]; + // do this just to keep synchronicity with the behavior of the real control + // for what it's worth, this automatic behavior when typing is the reason why this is separate from uiCombobox + [c->cb selectItemWithObjectValue:t]; +} + +#if 0 +// TODO +void uiEditableComboboxSetSelected(uiEditableCombobox *c, intmax_t n) +{ + if (c->editable) { + // see https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ComboBox/Tasks/SettingComboBoxValue.html#//apple_ref/doc/uid/20000256 + id delegate; + + // this triggers the delegate; turn it off for now + delegate = [c->cb delegate]; + [c->cb setDelegate:nil]; + + // this seems to work fine for -1 too + [c->cb selectItemAtIndex:n]; + if (n == -1) + [c->cb setObjectValue:@""]; + else + [c->cb setObjectValue:[c->cb objectValueOfSelectedItem]]; + + [c->cb setDelegate:delegate]; + return; + } + [c->pb selectItemAtIndex:n]; +} +#endif + +void uiEditableComboboxOnChanged(uiEditableCombobox *c, void (*f)(uiEditableCombobox *c, void *data), void *data) +{ + c->onChanged = f; + c->onChangedData = data; +} + +static void defaultOnChanged(uiEditableCombobox *c, void *data) +{ + // do nothing +} + +uiEditableCombobox *uiNewEditableCombobox(void) +{ + uiEditableCombobox *c; + + uiDarwinNewControl(uiEditableCombobox, c); + + c->cb = [[libui_intrinsicWidthNSComboBox alloc] initWithFrame:NSZeroRect]; + [c->cb setUsesDataSource:NO]; + [c->cb setButtonBordered:YES]; + [c->cb setCompletes:NO]; + uiDarwinSetControlFont(c->cb, NSRegularControlSize); + + if (comboboxDelegate == nil) { + comboboxDelegate = [editableComboboxDelegateClass new]; + [delegates addObject:comboboxDelegate]; + } + [comboboxDelegate registerCombobox:c]; + uiEditableComboboxOnChanged(c, defaultOnChanged, NULL); + + return c; +} diff --git a/test/page4.c b/test/page4.c index bb57ef07..00bc16ec 100644 --- a/test/page4.c +++ b/test/page4.c @@ -44,6 +44,7 @@ static void onCBChanged(uiCombobox *c, void *data) printf("%s combobox changed to %d\n", (char *) data, (int) uiComboboxSelected(c)); + uiEditableComboboxSetText(editable, "changed"); } static void onECBChanged(uiEditableCombobox *c, void *data) diff --git a/ui.h b/ui.h index 66d1fb7e..c1d7bb31 100644 --- a/ui.h +++ b/ui.h @@ -195,7 +195,7 @@ typedef struct uiEditableCombobox uiEditableCombobox; #define uiEditableCombobox(this) ((uiEditableCombobox *) (this)) _UI_EXTERN void uiEditableComboboxAppend(uiEditableCombobox *c, const char *text); _UI_EXTERN char *uiEditableComboboxText(uiEditableCombobox *c); -_UI_EXTERN void uiEditableComboboxSetText(uiEditableCombobox *c, intmax_t n); +_UI_EXTERN void uiEditableComboboxSetText(uiEditableCombobox *c, const char *text); // TODO what do we call a function that sets the currently selected item and fills the text field with it? editable comboboxes have no consistent concept of selected item _UI_EXTERN void uiEditableComboboxOnChanged(uiEditableCombobox *c, void (*f)(uiEditableCombobox *c, void *data), void *data); _UI_EXTERN uiEditableCombobox *uiNewEditableCombobox(void);