/** NSComboBoxCell Copyright (C) 1999 Free Software Foundation, Inc. Author: Gerrit van Dyk Date: 1999 Author: Quentin Mathe Date: 2004 This file is part of the GNUstep GUI Library. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; see the file COPYING.LIB. If not, see or write to the Free Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #import #import #import #import #import #import #import #import "AppKit/NSApplication.h" #import "AppKit/NSBox.h" #import "AppKit/NSBrowser.h" #import "AppKit/NSBrowserCell.h" #import "AppKit/NSButtonCell.h" #import "AppKit/NSComboBox.h" #import "AppKit/NSComboBoxCell.h" #import "AppKit/NSEvent.h" #import "AppKit/NSGraphicsContext.h" #import "AppKit/NSImage.h" #import "AppKit/NSPanel.h" #import "AppKit/NSScreen.h" #import "AppKit/NSScroller.h" #import "AppKit/NSScrollView.h" #import "AppKit/NSTableColumn.h" #import "AppKit/NSTableView.h" #import "AppKit/NSTextView.h" #import "GNUstepGUI/GSTheme.h" #import "GSGuiPrivate.h" static NSNotificationCenter *nc; @interface GSComboBoxTableView : NSTableView { } @end @implementation GSComboBoxTableView - (BOOL) acceptsFirstMouse: (NSEvent *)event { return YES; } @end @interface GSComboWindow : NSPanel { NSBrowser *_browser; GSComboBoxTableView *_tableView; NSComboBoxCell *_cell; BOOL _stopped; } + (GSComboWindow *) defaultPopUp; - (void) layoutWithComboBoxCell:(NSComboBoxCell *)comboBoxCell; - (void) positionWithComboBoxCell:(NSComboBoxCell *)comboBoxCell; - (void) popUpForComboBoxCell: (NSComboBoxCell *)comboBoxCell; - (void) runModalPopUpWithComboBoxCell:(NSComboBoxCell *)comboBoxCell; - (void) runLoopWithComboBoxCell:(NSComboBoxCell *)comboBoxCell; - (void) onWindowEdited: (NSNotification *)notification; - (void) clickItem: (id)sender; - (void) reloadData; - (void) noteNumberOfItemsChanged; - (void) scrollItemAtIndexToTop: (NSInteger)index; - (void) scrollItemAtIndexToVisible: (NSInteger)index; - (void) selectItemAtIndex: (NSInteger)index; - (void) deselectItemAtIndex: (NSInteger)index; - (void) moveUpSelection; - (void) moveDownSelection; - (void) validateSelection; @end @interface NSComboBoxCell (GNUstepPrivate) - (NSString *) _stringValueAtIndex: (NSInteger)index; - (void) _performClickWithFrame: (NSRect)cellFrame inView: (NSView *)controlView; - (void) _didClickWithinButton: (id)sender; - (BOOL) _isWantedEvent: (NSEvent *)event; - (GSComboWindow *) _popUp; - (NSRect) _textCellFrame; - (void) _setSelectedItem: (NSInteger)index; - (void) _loadButtonCell; - (void) _selectCompleted; @end // --- static GSComboWindow *gsWindow = nil; @implementation GSComboWindow + (GSComboWindow *) defaultPopUp { if (gsWindow == nil) gsWindow = [[self alloc] initWithContentRect: NSMakeRect(0,0,200,200) styleMask: NSBorderlessWindowMask backing: NSBackingStoreNonretained // NSBackingStoreBuffered defer: YES]; return gsWindow; } - (id) initWithContentRect: (NSRect)contentRect styleMask: (NSUInteger)aStyle backing: (NSBackingStoreType)bufferingType defer: (BOOL)flag { NSBox *box; NSRect borderRect; NSScrollView *scrollView; NSTableColumn *column; NSCell *cell; self = [super initWithContentRect: contentRect styleMask: aStyle backing: bufferingType defer: flag]; if (nil == self) return self; [self setLevel: NSPopUpMenuWindowLevel]; [self setBecomesKeyOnlyIfNeeded: YES]; box = [[NSBox alloc] initWithFrame: contentRect]; [box setAutoresizingMask: NSViewWidthSizable | NSViewHeightSizable]; [box setBorderType: NSLineBorder]; [box setTitlePosition: NSNoTitle]; [box setContentViewMargins: NSMakeSize(0, 0)]; [self setContentView: box]; borderRect = contentRect; RELEASE(box); _tableView = [[GSComboBoxTableView alloc] initWithFrame: NSMakeRect(0, 0, 100, 100)]; [_tableView setAutoresizingMask: NSViewWidthSizable | NSViewHeightSizable]; //[_tableView setBackgroundColor: [NSColor whiteColor]]; [_tableView setDrawsGrid: NO]; [_tableView setAllowsEmptySelection: YES]; [_tableView setAllowsMultipleSelection: NO]; [_tableView setAutoresizesAllColumnsToFit: YES]; [_tableView setHeaderView: nil]; [_tableView setCornerView: nil]; column = [[NSTableColumn alloc] initWithIdentifier: @"content"]; cell = [[NSCell alloc] initTextCell: @""]; [column setDataCell: cell]; RELEASE(cell); [_tableView addTableColumn: column]; RELEASE(column); [_tableView setDataSource: self]; [_tableView setDelegate: self]; [_tableView setAction: @selector(clickItem:)]; [_tableView setTarget: self]; scrollView = [[NSScrollView alloc] initWithFrame: NSMakeRect(borderRect.origin.x, borderRect.origin.y, borderRect.size.width, borderRect.size.height)]; [scrollView setHasVerticalScroller: YES]; [scrollView setDocumentView: _tableView]; [_tableView release]; [box setContentView: scrollView]; RELEASE(scrollView); [_tableView reloadData]; return self; } - (BOOL) canBecomeKeyWindow { return YES; } - (void)dealloc { // Browser, table view and scroll view were not retained so don't release them [super dealloc]; } - (void) layoutWithComboBoxCell: (NSComboBoxCell *)comboBoxCell { NSSize bsize = [[GSTheme theme] sizeForBorderType: NSLineBorder]; NSSize size; CGFloat itemHeight; CGFloat textCellWidth; CGFloat popUpWidth; NSSize intercellSpacing; NSInteger num = [comboBoxCell numberOfItems]; NSInteger max = [comboBoxCell numberOfVisibleItems]; // Manage table view or browser cells height itemHeight = [comboBoxCell itemHeight]; if (itemHeight <= 0) { // FIX ME : raise NSException itemHeight = [_tableView rowHeight]; } size.height = itemHeight; // Manage table view or browser cells width textCellWidth = [comboBoxCell _textCellFrame].size.width; if ([comboBoxCell hasVerticalScroller]) { popUpWidth = textCellWidth + [NSScroller scrollerWidth]; } else { popUpWidth = textCellWidth; } size.width = textCellWidth - bsize.width; if (size.width < 0) { size.width = 0; } [_tableView setRowHeight: size.height]; // Just check intercell spacing intercellSpacing = [comboBoxCell intercellSpacing]; if (intercellSpacing.height <= 0) { // FIX ME : raise NSException intercellSpacing.height = [_tableView intercellSpacing].height; } else { [_tableView setIntercellSpacing: intercellSpacing]; } if (num > max) num = max; [self setFrame: [self frameRectForContentRect: NSMakeRect(0, 0, popUpWidth, 2 * bsize.height + (itemHeight + intercellSpacing.height) * (num - 1) + itemHeight)] display: NO]; } - (void) positionWithComboBoxCell: (NSComboBoxCell *)comboBoxCell { NSView *viewWithComboCell = [comboBoxCell controlView]; NSRect screenFrame; NSRect comboWindowFrame; NSRect viewWithComboCellFrame; NSRect rect; NSPoint point, oldPoint; [self layoutWithComboBoxCell: comboBoxCell]; // Now we can ask for the size comboWindowFrame = [self frame]; if (comboWindowFrame.size.width == 0 || comboWindowFrame.size.height == 0) return; screenFrame = [[[viewWithComboCell window] screen] frame]; viewWithComboCellFrame = [comboBoxCell _textCellFrame]; if ([viewWithComboCell isFlipped]) { point = viewWithComboCellFrame.origin; point.y = NSMaxY(viewWithComboCellFrame); } else { point = viewWithComboCellFrame.origin; } // Switch to the window coordinates point = [viewWithComboCell convertPoint: point toView: nil]; // Switch to the screen coordinates point = [[viewWithComboCell window] convertBaseToScreen: point]; point.y -= 1 + NSHeight(comboWindowFrame); if (point.y < 0) { // Off screen, so move it oldPoint = point; point = viewWithComboCellFrame.origin; point.y = NSMaxY(viewWithComboCellFrame); // Switch to the window coordinates point = [viewWithComboCell convertPoint: point toView: nil]; // Switch to the screen coordiantes point = [[viewWithComboCell window] convertBaseToScreen: point]; point.y += 1; if (point.y + NSHeight(comboWindowFrame) > NSHeight(screenFrame)) point = oldPoint; } rect.size.width = NSWidth(comboWindowFrame); rect.size.height = NSHeight(comboWindowFrame); rect.origin.x = point.x; rect.origin.y = point.y; [self setFrame: rect display: NO]; } - (void) popUpForComboBoxCell: (NSComboBoxCell *)comboBoxCell { _cell = comboBoxCell; [self positionWithComboBoxCell: _cell]; [_cell _selectCompleted]; [self reloadData]; [self enableKeyEquivalentForDefaultButtonCell]; [self runModalPopUpWithComboBoxCell: _cell]; _cell = nil; [self deselectItemAtIndex: 0]; } - (void) runModalPopUpWithComboBoxCell: (NSComboBoxCell *)comboBoxCell { NSWindow *onWindow; onWindow = [[_cell controlView] window]; [nc addObserver: self selector: @selector(onWindowEdited:) name: NSWindowWillCloseNotification object: onWindow]; [nc addObserver: self selector: @selector(onWindowEdited:) name: NSWindowWillMoveNotification object: onWindow]; [nc addObserver: self selector: @selector(onWindowEdited:) name: NSWindowWillMiniaturizeNotification object: onWindow]; // FIX ME: The notification below doesn't exist currently // [nc addObserver: self selector: @selector(onWindowEdited:) // name: NSWindowWillResizeNotification object: onWindow]; // FIXME: The code below must be removed when the notifications over will work [nc addObserver: self selector: @selector(onWindowEdited:) name: NSWindowDidMoveNotification object: onWindow]; [nc addObserver: self selector: @selector(onWindowEdited:) name: NSWindowDidMiniaturizeNotification object: onWindow]; [nc addObserver: self selector: @selector(onWindowEdited:) name: NSWindowDidResizeNotification object: onWindow]; // End of the code to remove [self orderFront: self]; [self makeFirstResponder: _tableView]; [self runLoopWithComboBoxCell: comboBoxCell]; [nc removeObserver: self name: nil object: onWindow]; [self close]; [onWindow makeFirstResponder: [_cell controlView]]; } - (void) runLoopWithComboBoxCell: (NSComboBoxCell *)comboBoxCell { NSEvent *event; NSDate *limit = [NSDate distantFuture]; unichar key; CREATE_AUTORELEASE_POOL (pool); while (YES) { event = [NSApp nextEventMatchingMask: NSAnyEventMask untilDate: limit inMode: NSDefaultRunLoopMode dequeue: YES]; if ([event type] == NSLeftMouseDown || [event type] == NSRightMouseDown) { if ([comboBoxCell _isWantedEvent: event] == NO && [event window] != self) { break; } else { [NSApp sendEvent: event]; } } else if ([event type] == NSKeyDown) { key = [[event characters] characterAtIndex: 0]; if (key == NSUpArrowFunctionKey) { [self moveUpSelection]; } else if (key == NSDownArrowFunctionKey) { [self moveDownSelection]; } else if (key == NSNewlineCharacter || key == NSEnterCharacter || key == NSCarriageReturnCharacter) { [self validateSelection]; } else if (key == 0x001b) { break; } else { [NSApp sendEvent: event]; } } else { [NSApp sendEvent: event]; } if (_stopped) break; } _stopped = NO; [pool drain]; } // onWindow notifications - (void) onWindowEdited: (NSNotification *)notification { _stopped = YES; } - (void) reloadData { [_tableView reloadData]; [self selectItemAtIndex: [_cell indexOfSelectedItem]]; } - (void) noteNumberOfItemsChanged { // FIXME: Probably should only load the additional items [self reloadData]; } - (void) scrollItemAtIndexToTop: (NSInteger)index { NSRect rect; rect = [_tableView frameOfCellAtColumn: 0 row: index]; [_tableView scrollPoint: rect.origin]; } - (void) scrollItemAtIndexToVisible: (NSInteger)index { [_tableView scrollRowToVisible: index]; } - (void) selectItemAtIndex: (NSInteger)index { if (index < 0) return; if ([_tableView selectedRow] == index || [_tableView numberOfRows] <= index) return; [_tableView selectRow: index byExtendingSelection: NO]; } - (void) deselectItemAtIndex: (NSInteger)index { [_tableView deselectAll: self]; } // Target/Action method - (void) clickItem: (id)sender { if (_cell == nil) return; [_cell _setSelectedItem: [sender selectedRow]]; [self validateSelection]; [nc postNotificationName: NSComboBoxSelectionDidChangeNotification object: [_cell controlView] userInfo: nil]; } // Browser delegate methods - (NSInteger) browser: (NSBrowser *)sender numberOfRowsInColumn: (NSInteger)column { if (_cell == nil) return 0; return [_cell numberOfItems]; } - (void) browser: (NSBrowser *)sender willDisplayCell: (id)aCell atRow: (NSInteger)row column: (NSInteger)column { if (_cell == nil) return; [aCell setStringValue: [_cell _stringValueAtIndex: row]]; [aCell setLeaf: YES]; } // Table view data source methods - (NSInteger) numberOfRowsInTableView: (NSTableView *)tv { return [_cell numberOfItems]; } - (id) tableView: (NSTableView *)tv objectValueForTableColumn: (NSTableColumn *)tc row: (NSInteger)row { return [_cell _stringValueAtIndex: row]; } // Table view delegate methods - (void) tableViewSelectionDidChange: (NSNotification *)notification { [_cell _setSelectedItem: [[notification object] selectedRow]]; [nc postNotificationName: NSComboBoxSelectionDidChangeNotification object: [_cell controlView] userInfo: nil]; } // Key actions methods - (void) moveUpSelection { NSInteger index = [_tableView selectedRow] - 1; if (index > -1 && index < [_tableView numberOfRows]) { [_tableView selectRow: index byExtendingSelection: NO]; [_tableView scrollRowToVisible: index]; } } - (void) moveDownSelection { NSInteger index = [_tableView selectedRow] + 1; if (index > -1 && index < [_tableView numberOfRows]) { [_tableView selectRow: index byExtendingSelection: NO]; [_tableView scrollRowToVisible: index]; } } - (void) validateSelection { if (_cell != nil) { NSText *textObject = nil; id cv = [_cell controlView]; NSInteger index = [_cell indexOfSelectedItem]; if ([cv isKindOfClass: [NSControl class]]) { textObject = [(NSControl *)cv currentEditor]; } if (index != -1) { [_cell setStringValue: [_cell _stringValueAtIndex: [_cell indexOfSelectedItem]]]; // Will update the editor when needed } if (textObject != nil) { NSRange selectionRange = NSMakeRange(0, [[textObject string] length]); [textObject setSelectedRange: selectionRange]; [textObject scrollRangeToVisible: selectionRange]; } [cv sendAction: [_cell action] to: [_cell target]]; _stopped = YES; } } @end // --- /** Class Description

An NSComboBoxCell is what we can call a completion/choices box cell, derived from NSTextFieldCell, it allows you to enter text like in a text field but also to click in the ellipsis button (indicating the fact other user inputs are possible) on the right of it to obtain a list of choices, you can use them as the text field value by selecting a row in this list. You can also obtain direct completion when it is enabled via setCompletes: to get a suggested text field value updated as you type.

Like other NSCell classes, NSComboBoxCell has a matching NSControl named NSComboBox which is relying on it to implement the combo box behavior in a standalone control.

*/ /** *

No special instructions to use NSComboBoxCell or text to detail the implementation.

*/ @implementation NSComboBoxCell /* * Class methods */ + (void) initialize { if (self == [NSComboBoxCell class]) { [NSComboBoxCell setVersion: 2]; nc = [NSNotificationCenter defaultCenter]; } } - (id) initTextCell: (NSString *)aString { self = [super initTextCell: aString]; // Implicitly set by allocation: // //_dataSource = nil; //_buttonCell = nil; //_usesDataSource = NO; //_completes = NO; _popUpList = [[NSMutableArray alloc] init]; _hasVerticalScroller = YES; _visibleItems = 10; _intercellSpacing = NSMakeSize(3.0, 2.0); _itemHeight = 16; _selectedItem = -1; [self _loadButtonCell]; return self; } - (void) dealloc { RELEASE(_buttonCell); RELEASE(_popUpList); [super dealloc]; } - (id) copyWithZone: (NSZone*)zone { NSComboBoxCell *c = [super copyWithZone: zone]; c->_buttonCell = [_buttonCell copyWithZone: zone]; [c->_buttonCell setTarget: c]; c->_popUpList = [_popUpList copyWithZone: zone]; return c; } /** * Returns YES when the combo box cell displays a vertical scroller for its * list, returns NO otherwise. * Take note that the scroller will be displayed even when the sum of the items * height in the list is inferior to the minimal height of the list displayed * area. */ - (BOOL) hasVerticalScroller { return _hasVerticalScroller; } /** * Sets whether the combo box cell list displays a vertical scroller, by default * it is the case. When flag is NO and the combo cell list has more * items (either in its default list or from its data source) than the number * returned by numberOfVisibleItems, only a subset of them will be * displayed. Uses scroll related methods to position this subset in the combo * box cell list. * Take note that the scroller will be displayed even when the sum of the items * height in the list is inferior to the minimal height of the list displayed * area. */ - (void) setHasVerticalScroller: (BOOL)flag { _hasVerticalScroller = flag; } /** * Returns the width and the height (as the values of an NSSize variable) * between each item of the combo box cell list. */ - (NSSize) intercellSpacing { return _intercellSpacing; } /** * Sets the width and the height between each item of the combo box cell list to * the values in aSize. */ - (void) setIntercellSpacing: (NSSize)aSize { _intercellSpacing = aSize; } /** * Returns the height of the items in the combo box cell list. */ - (CGFloat) itemHeight { return _itemHeight; } /** * Sets the height of the items in the combo box cell list to * itemHeight. */ - (void) setItemHeight: (CGFloat)itemHeight { if (itemHeight > 14.0) _itemHeight = itemHeight; } /** * Returns the maximum number of allowed items to be displayed in the combo box * cell list. */ - (NSInteger) numberOfVisibleItems { return _visibleItems; } /** * Sets the maximum number of allowed items to be displayed in the combo box * cell list. */ - (void) setNumberOfVisibleItems: (NSInteger)visibleItems { if (visibleItems > 10) _visibleItems = visibleItems; } /** * Marks the combo box cell in order to have its items list reloaded in the * case it uses a data source, and to have it redisplayed. */ - (void) reloadData { [_popup reloadData]; } /** * Informs the combo box cell that the number of items in its data source has * changed, in order to permit to the scrollers in its displayed list being * updated without needing the reload of the data. * It is recommended to use this method with a data source that continually * receives data in the background, to keep the the combo box cell responsive to * the user while the data is received. * Take a look at the NSComboBoxDataSource informal protocol * specification to know more on the messages NSComboBox sends to its data * source. */ - (void) noteNumberOfItemsChanged { [_popup noteNumberOfItemsChanged]; } /** * Returns YES when the combo box cell uses a data source (which is external) to * populate its items list, otherwise returns NO in the case it uses its default * list. */ - (BOOL) usesDataSource { return _usesDataSource; } /** * Sets according to flag whether the combo box cell uses a data * source (which is external) to populate its items list. */ - (void) setUsesDataSource: (BOOL)flag { _usesDataSource = flag; } /** * Scrolls the combo box cell list vertically in order to have the item at * index in the closest position relative to the top. There is no * need to have the list displayed when this method is invoked. */ - (void) scrollItemAtIndexToTop: (NSInteger)index { [_popup scrollItemAtIndexToTop: index]; } /** * Scrolls the combo box cell list vertically in order to have the item at * index visible. There is no need to have the list displayed when * this method is invoked. */ - (void) scrollItemAtIndexToVisible: (NSInteger)index { [_popup scrollItemAtIndexToVisible: index]; } /** * Selects the combo box cell list row at index. * Take note no changes occurs in the combo box cell list when this method is * called. * Posts an NSComboBoxSelectionDidChangeNotification to the default notification * center when there is a new selection different from the previous one. */ - (void) selectItemAtIndex: (NSInteger)index { // Method called by GSComboWindow when a selection is done in the table view or // the browser if (index < 0 || [self numberOfItems] <= index) return; // FIXME: Probably we should raise an exception if (_selectedItem != index) { [self _setSelectedItem: index]; [_popup selectItemAtIndex: index]; // This method call will not create a infinite loop when the index has been // already set by a mouse click because the method is not completed when the // current index is not different from the index parameter [nc postNotificationName: NSComboBoxSelectionDidChangeNotification object: [self controlView] userInfo: nil]; } } /** * Deselects the combo box cell list row at index in the case this * row is selected. * Posts an NSComboBoxSelectionDidChangeNotification to the default notification * center, when there is a new selection. */ - (void) deselectItemAtIndex: (NSInteger)index { if (_selectedItem == index) { [self _setSelectedItem: -1]; [_popup deselectItemAtIndex: index]; [nc postNotificationName: NSComboBoxSelectionDidChangeNotification object: [self controlView] userInfo: nil]; } } /** * Returns the index of the selected item in the combo box cell list or -1 when * there is no selection, the selected item can be related to the data source * object in the case usesDataSource returns YES else to the * default items list. */ - (NSInteger) indexOfSelectedItem { return _selectedItem; } /** * Returns the number of items in the the combo box cell list, the numbers of * items can be be related to the data source object in the case * usesDataSource returns YES else to the default items list. */ - (NSInteger) numberOfItems { if (_usesDataSource) { if (_dataSource == nil) { NSLog(@"%@: No data source currently specified", self); } else if ([_dataSource respondsToSelector: @selector(numberOfItemsInComboBox:)]) { return [_dataSource numberOfItemsInComboBox: (NSComboBox *)[self controlView]]; } else if ([_dataSource respondsToSelector: @selector(numberOfItemsInComboBoxCell:)]) { return [_dataSource numberOfItemsInComboBoxCell: self]; } } else { return [_popUpList count]; } return 0; } /** * Returns the combo box cell data source object which is reponsible to provide * the data to be displayed. To know how to implement a data source object, * take a look at the NSComboBoxDataSource informal protocol description. In * the case usesDataSource returns NO, this method logs a warning. */ - (id) dataSource { return _dataSource; } /** * Sets the combo box cell data source to aSource. Just calling this * method doesn't set usesDataSource to return YES, you must call * setUsesDataSource: with YES before or a warning will be logged. * To know how to implement a data source objects, take a look at the * NSComboBoxDataSource informal protocol description. When aSource * doesn't respond to the methods numberOfItemsInComboBox: * comboBox:objectValueForItemAtIndex:, this method * logs a warning. */ - (void) setDataSource: (id)aSource { if (_usesDataSource == NO) { NSLog(@"%@: This method is invalid, this combo box is not set to use a data source", self); } else { _dataSource = aSource; } } /** * Adds an item to the combo box cell default items list which is used when * usesDataSource returns NO. In the case * usesDataSource returns YES, this method logs a warning. */ - (void) addItemWithObjectValue: (id)object { if (_usesDataSource) { NSLog(@"%@: This method is invalid, this combo box is set to use a data source", self); } else { [_popUpList addObject: object]; } [self reloadData]; } /** * Adds several items in an array to the combo box cell default items list which * is used when usesDataSource returns NO. In the case * usesDataSource returns YES, this method logs a warning. */ - (void) addItemsWithObjectValues: (NSArray *)objects { if (_usesDataSource) { NSLog(@"%@: This method is invalid, this combo box is set to use a data source", self); } else { [_popUpList addObjectsFromArray: objects]; } [self reloadData]; } /** * Inserts an item in the combo box cell default items list which * is used when usesDataSource returns NO. In the case * usesDataSource returns YES, this method logs a warning. */ - (void) insertItemWithObjectValue: (id)object atIndex: (NSInteger)index { if (_usesDataSource) { NSLog(@"%@: This method is invalid, this combo box is set to use a data source", self); } else { [_popUpList insertObject: object atIndex: index]; } [self reloadData]; } /** * Removes an item in the combo box cell default items list which * is used when usesDataSource returns NO. In the case * usesDataSource returns YES, this method logs a warning. */ - (void) removeItemWithObjectValue: (id)object { if (_usesDataSource) { NSLog(@"%@: This method is invalid, this combo box is set to use a data source", self); } else { [_popUpList removeObject: object]; } [self reloadData]; } /** * Removes the item with the specified index in the combo box cell * default items list which is used when usesDataSource returns NO. * In the case usesDataSource returns YES, this method logs a warning. */ - (void) removeItemAtIndex: (NSInteger)index { if (_usesDataSource) { NSLog(@"%@: This method is invalid, this combo box is set to use a data source", self); } else { [_popUpList removeObjectAtIndex: index]; } [self reloadData]; } /** * Removes all the items in the combo box cell default items list which is used * when usesDataSource returns NO. In the case * usesDataSource returns YES, this method logs a warning. */ - (void) removeAllItems { if (_usesDataSource) { NSLog(@"%@: This method is invalid, this combo box is set to use a data source", self); } else { [_popUpList removeAllObjects]; } [self reloadData]; } /** * Selects the first item in the default combo box cell list which is equal to * object. In the case usesDataSource returns YES, this * method logs a warning. * Take note that this method doesn't update the text field part value. * Posts an NSComboBoxSelectionDidChange notification to the default * notification center when the new selection is different than the previous * one. */ - (void) selectItemWithObjectValue: (id)object { if (_usesDataSource) { NSLog(@"%@: This method is invalid, this combo box is set to use a data source", self); } else { NSInteger i = [_popUpList indexOfObject: object]; if (i == NSNotFound) i = -1; [self selectItemAtIndex: i]; } } /** * Returns the object value at index within combo box cell default * items list. When the index is beyond the end of the list, an NSRangeException is * raised. In the case usesDataSource returns YES, this method logs * a warning. */ - (id) itemObjectValueAtIndex: (NSInteger)index { if (_usesDataSource) { NSLog(@"%@: This method is invalid, this combo box is set to use a data source", self); return nil; } else { return [_popUpList objectAtIndex: index]; } } /* FIXME: Not sure, if this is the best way to implement objectValue, * perhaps it would be better to store the current value with setObjectValue: * whenever it changes. */ - (id) objectValue { NSInteger index = [self indexOfSelectedItem]; if (index == -1) { return nil; } else { if (_usesDataSource) { if (_dataSource == nil) { NSLog(@"%@: No data source currently specified", self); return nil; } if ([_dataSource respondsToSelector: @selector(comboBox:objectValueForItemAtIndex:)]) { return [_dataSource comboBox: (NSComboBox *)[self controlView] objectValueForItemAtIndex: index]; } else if ([_dataSource respondsToSelector: @selector(comboBoxCell:objectValueForItemAtIndex:)]) { return [_dataSource comboBoxCell: self objectValueForItemAtIndex: index]; } } else { return [self itemObjectValueAtIndex: index]; } } return nil; } /** * Returns the object value of the selected item in the combo box cell default * items list or nil when there is no selection. In the case * usesDataSource returns YES, this method logs a warning. */ - (id) objectValueOfSelectedItem { if (_usesDataSource) { NSLog(@"%@: This method is invalid, this combo box is set to use a data source", self); return nil; } else { NSInteger index = [self indexOfSelectedItem]; if (index == -1) { return nil; } else { return [_popUpList objectAtIndex: index]; } } } /** * Returns the lowest index associated with a value in the combo box * cell default items list, which is equal to object, and returns * NSNotFound when there is no such value. In the case * usesDataSource returns YES, this method logs a warning. */ - (NSInteger) indexOfItemWithObjectValue: (id)object { if (_usesDataSource) { NSLog(@"%@: This method is invalid, this combo box is set to use a data source", self); return 0; } return [_popUpList indexOfObject: object]; } /** * Returns the combo box cell default items list in an array. */ - (NSArray *) objectValues { if (_usesDataSource) { NSLog(@"%@: This method is invalid, this combo box is set to use a data source", self); return nil; } return _popUpList; } // Text completion /** * Returns a string by looking in the combo box cell list for an item wich * starts with substring, or nil when there is no such string. * substring is equal to what the user entered in the text field * part. * You rarely needs to call this method explicitly in your code. * By default, the implementation of this method first checks whether the combo * box cell uses a data source and whether the data source responds to * comboBox:completedString: or comboBoxCell:completedString:. * When it is the case, it uses this method to return str, else this * method goes through the combo box cell items one by one and returns the first * item found starting with substring. * In the case, you want another behavior, you can override this method without * need to call the superclass method. */ - (NSString *) completedString: (NSString *)substring { if (nil == substring) { return nil; } if (_usesDataSource) { if (_dataSource == nil) { NSLog(@"%@: No data source currently specified", self); } else if ([_dataSource respondsToSelector: @selector(comboBox:completedString:)]) { return [_dataSource comboBox: (NSComboBox *)[self controlView] completedString: substring]; } else if ([_dataSource respondsToSelector: @selector(comboBoxCell:completedString:)]) { return [_dataSource comboBoxCell: self completedString: substring]; } else { NSInteger i; for (i = 0; i < [self numberOfItems]; i++) { NSString *str = [self _stringValueAtIndex: i]; if ([str length] > [substring length] && [str hasPrefix: substring]) return str; } } } else { NSUInteger i; for (i = 0; i < [_popUpList count]; i++) { NSString *str = [[_popUpList objectAtIndex: i] description]; if ([str length] > [substring length] && [str hasPrefix: substring]) return str; } } return substring; } /** * Returns YES when the combo box cell automatic completion is active, returns * NO otherwise. * Take a look at the setCompletes: method documentation to know * how the automatic completion works. */ - (BOOL) completes { return _completes; } /** * Sets whether the combo box cell automatic completion is active or not. * The automatic completion tries to complete what the user types in the text * field part, it tries to complete only when the the user adds characters at * the end of the string, not when it deletes characters or when the insertion * point precedes the end of the string. * To do the automatic completion, the completedString: method is * called, and when the returned string is longer than the current one in the text * field, the completion occurs and the completed part gets selected. */ - (void) setCompletes: (BOOL)completes { _completes = completes; } - (BOOL) isButtonBordered { return [_buttonCell isBordered]; } - (void) setButtonBordered:(BOOL)flag { [_buttonCell setBordered: flag]; } #define ComboBoxHeight 21 // FIX ME: All this stuff shouldn't be hardcoded #define ButtonWidth 17 #define ButtonHeight 17 #define BorderSize 2 // The inset border for the top and the bottom of the button /* * Inlined methods */ static inline NSRect textCellFrameFromRect(NSRect cellRect) // Not the drawed part, precises just the part which receives events { return NSMakeRect(NSMinX(cellRect), NSMinY(cellRect), NSWidth(cellRect) - ButtonWidth - BorderSize, NSHeight(cellRect)); } static inline NSRect buttonCellFrameFromRect(NSRect cellRect) { return NSMakeRect(NSMaxX(cellRect) - ButtonWidth - BorderSize, NSMinY(cellRect) + BorderSize, ButtonWidth, ButtonHeight); } // Overridden + (BOOL) prefersTrackingUntilMouseUp { return YES; /* Needed to have the clickability of the button take in account when the tracking happens. This method is call by the NSControl -mouseDown: method with the code : [_cell trackMouse: e inRect: _bounds ofView: self untilMouseUp: [[_cell class] prefersTrackingUntilMouseUp]] */ } - (NSSize) cellSize { NSSize textSize; NSSize buttonSize; NSSize mySize; /* Simple version takes the size from text field. A more useful one could loop over the strings of the combo box and calculate the maximal width of all strings. */ textSize = [super cellSize]; // Or should we use the hard coded values from above here? buttonSize = [_buttonCell cellSize]; mySize.height = MAX(textSize.height, buttonSize.height); mySize.width = textSize.width + BorderSize + buttonSize.width; return mySize; } - (void) drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView { NSRect rect = cellFrame; // FIX ME: Is this test case below with the method call really needed ? if ([GSCurrentContext() isDrawingToScreen]) { [super drawInteriorWithFrame: textCellFrameFromRect(rect) inView: controlView]; [_buttonCell drawWithFrame: buttonCellFrameFromRect(rect) inView: controlView]; } else { [super drawInteriorWithFrame: rect inView: controlView]; } // Used by GSComboWindow to appear in the right position _lastValidFrame = cellFrame; } - (void) highlight: (BOOL)flag withFrame: (NSRect)cellFrame inView: (NSView *)controlView { NSRect rect = cellFrame; // FIX ME: Is this test case below with the method call really needed ? if ([GSCurrentContext() isDrawingToScreen]) { [super highlight: flag withFrame: textCellFrameFromRect(rect) inView: controlView]; [_buttonCell highlight: flag withFrame: buttonCellFrameFromRect(rect) inView: controlView]; } else { [super highlight: flag withFrame: rect inView: controlView]; } } /** Overrides NSCell trackMouse:inRect:ofView:untilMouseUp: method to establish a * new method behavior. * In the case flag is NO, returns NO when the mouse down occurs in the text * cell part or when the mouse down occurs in the button cell part followed by a * mouse up outside, otherwise returns YES (when both the mouse down and the * mouse up occurs in the button cell part). * In the case flag is YES, returns NO when the mouse occurs in the text * cell part, otherwise returns YES (when the mouse down occurs in the button cell * part). */ - (BOOL) trackMouse: (NSEvent *)theEvent inRect: (NSRect)cellFrame ofView: (NSView *)controlView untilMouseUp: (BOOL)flag { NSPoint point; BOOL isFlipped = [controlView isFlipped]; NSRect buttonRect = buttonCellFrameFromRect(cellFrame); NSRect textRect = textCellFrameFromRect(cellFrame); BOOL result = NO; // FIXME: May be that should be set by NSActionCell if (_control_view != controlView) _control_view = controlView; // Used by GSComboWindow to appear in the right position _lastValidFrame = cellFrame; point = [controlView convertPoint: [theEvent locationInWindow] fromView: nil]; if (NSMouseInRect(point, textRect, isFlipped)) { return NO; } else if (NSMouseInRect(point, buttonRect, isFlipped)) { NSEvent *e = theEvent; BOOL isMouseUp = NO; NSUInteger eventMask = NSLeftMouseDownMask | NSLeftMouseUpMask | NSMouseMovedMask | NSLeftMouseDraggedMask | NSOtherMouseDraggedMask | NSRightMouseDraggedMask; NSPoint location; while (isMouseUp == NO) // Loop until mouse goes up { location = [controlView convertPoint: [e locationInWindow] fromView: nil]; // Ask the cell to track the mouse only when the mouse is within the cell if (NSMouseInRect(location, buttonRect, isFlipped)) { [_buttonCell setHighlighted: YES]; [controlView setNeedsDisplayInRect: cellFrame]; result = [_buttonCell trackMouse: e inRect: buttonRect ofView: controlView untilMouseUp: [NSButtonCell prefersTrackingUntilMouseUp]]; isMouseUp = result; [_buttonCell setHighlighted: NO]; [controlView setNeedsDisplayInRect: cellFrame]; } if (isMouseUp == NO) { e = [NSApp nextEventMatchingMask: eventMask untilDate: [NSDate distantFuture] inMode: NSEventTrackingRunLoopMode dequeue: YES]; if ([e type] == NSLeftMouseUp) isMouseUp = YES; } } if (flag) { return YES; } else { return NO; } } return NO; // Pathological case, normally never happens } - (void) resetCursorRect: (NSRect)cellFrame inView: (NSView *)controlView { [super resetCursorRect: textCellFrameFromRect(cellFrame) inView: controlView]; } - (void) setEnabled: (BOOL)flag { [_buttonCell setEnabled: flag]; [super setEnabled: flag]; } // NSCoding /** * Encodes the combo box cell using encoder. take note that when it * uses a data source, the data source is conditionally encoded. */ - (void) encodeWithCoder: (NSCoder *)coder { [super encodeWithCoder: coder]; if ([coder allowsKeyedCoding]) { [coder encodeBool: [self hasVerticalScroller] forKey: @"NSHasVerticalScroller"]; [coder encodeInteger: [self numberOfVisibleItems] forKey: @"NSVisibleItemCount"]; [coder encodeBool: [self completes] forKey: @"NSCompletes"]; [coder encodeDouble: _intercellSpacing.width forKey: @"NSIntercellSpacingWidth"]; [coder encodeDouble: _intercellSpacing.height forKey: @"NSIntercellSpacingHeight"]; [coder encodeDouble: [self itemHeight] forKey: @"NSRowHeight"]; [coder encodeBool: [self usesDataSource] forKey: @"NSUsesDataSource"]; [coder encodeObject: [self dataSource] forKey: @"NSDataSource"]; [coder encodeObject: _popUpList forKey: @"NSPopUpListData"]; } else { [coder encodeValueOfObjCType: @encode(id) at: &_popUpList]; [coder encodeValueOfObjCType: @encode(BOOL) at: &_usesDataSource]; [coder encodeValueOfObjCType: @encode(BOOL) at: &_hasVerticalScroller]; [coder encodeValueOfObjCType: @encode(BOOL) at: &_completes]; [coder encodeValueOfObjCType: @encode(BOOL) at: &_usesDataSource]; encode_NSInteger(coder, &_visibleItems); [coder encodeValueOfObjCType: @encode(NSSize) at: &_intercellSpacing]; [coder encodeValueOfObjCType: @encode(float) at: &_itemHeight]; encode_NSInteger(coder, &_selectedItem); if (_usesDataSource == YES) [coder encodeConditionalObject: _dataSource]; } } /** * Initializes the combo box cell with data linked to decoder. Take * note that when the decoded instance uses a data source, * initWithCoder: decodes the data source. * Finally, returns thr initialized object. */ - (id) initWithCoder: (NSCoder *)aDecoder { self = [super initWithCoder: aDecoder]; if (nil == self) return nil; if ([aDecoder allowsKeyedCoding]) { //id delegate = [aDecoder decodeObjectForKey: @"NSDelegate"]; //id table = [aDecoder decodeObjectForKey: @"NSTableView"]; if ([aDecoder containsValueForKey: @"NSHasVerticalScroller"]) { [self setHasVerticalScroller: [aDecoder decodeBoolForKey: @"NSHasVerticalScroller"]]; } if ([aDecoder containsValueForKey: @"NSVisibleItemCount"]) { [self setNumberOfVisibleItems: [aDecoder decodeIntegerForKey: @"NSVisibleItemCount"]]; } if ([aDecoder containsValueForKey: @"NSCompletes"]) { [self setCompletes: [aDecoder decodeBoolForKey: @"NSCompletes"]]; } if ([aDecoder containsValueForKey: @"NSIntercellSpacingWidth"]) { _intercellSpacing.width = [aDecoder decodeDoubleForKey: @"NSIntercellSpacingWidth"]; } if ([aDecoder containsValueForKey: @"NSIntercellSpacingHeight"]) { _intercellSpacing.height = [aDecoder decodeDoubleForKey: @"NSIntercellSpacingHeight"]; } if ([aDecoder containsValueForKey: @"NSRowHeight"]) { [self setItemHeight: [aDecoder decodeDoubleForKey: @"NSRowHeight"]]; } if ([aDecoder containsValueForKey: @"NSUsesDataSource"]) { [self setUsesDataSource: [aDecoder decodeBoolForKey: @"NSUsesDataSource"]]; } if ([aDecoder containsValueForKey: @"NSDataSource"]) { [self setDataSource: [aDecoder decodeObjectForKey: @"NSDataSource"]]; } if ([aDecoder containsValueForKey: @"NSPopUpListData"]) { ASSIGN(_popUpList, [aDecoder decodeObjectForKey: @"NSPopUpListData"]); } } else { BOOL dummy; if ([aDecoder versionForClassName: @"NSComboBoxCell"] < 2) { // In previous version we decode _buttonCell, we just discard the decoded value here id previouslyEncodedButton; [aDecoder decodeValueOfObjCType: @encode(id) at: &previouslyEncodedButton]; } [aDecoder decodeValueOfObjCType: @encode(id) at: &_popUpList]; RETAIN(_popUpList); [aDecoder decodeValueOfObjCType: @encode(BOOL) at: &_usesDataSource]; [aDecoder decodeValueOfObjCType: @encode(BOOL) at: &_hasVerticalScroller]; [aDecoder decodeValueOfObjCType: @encode(BOOL) at: &_completes]; [aDecoder decodeValueOfObjCType: @encode(BOOL) at: &dummy]; decode_NSInteger(aDecoder, &_visibleItems); [aDecoder decodeValueOfObjCType: @encode(NSSize) at: &_intercellSpacing]; [aDecoder decodeValueOfObjCType: @encode(float) at: &_itemHeight]; decode_NSInteger(aDecoder, &_selectedItem); if (_usesDataSource == YES) [self setDataSource: [aDecoder decodeObject]]; } [self _loadButtonCell]; return self; } - (void) selectWithFrame: (NSRect)aRect inView: (NSView *)controlView editor: (NSText *)textObj delegate: (id)anObject start: (NSInteger)selStart length: (NSInteger)selLength { [super selectWithFrame: textCellFrameFromRect(aRect) inView: controlView editor: textObj delegate: anObject start: selStart length: selLength]; [nc addObserver: self selector: @selector(textDidChange:) name: NSTextDidChangeNotification object: textObj]; [nc addObserver: self selector: @selector(textViewDidChangeSelection:) name: NSTextViewDidChangeSelectionNotification object: textObj]; // This method is called when the cell obtains the focus; // don't know why the next method editWithFrame: is not called } - (void) editWithFrame: (NSRect)frame inView: (NSView *)controlView editor: (NSText *)textObj delegate: (id)delegate event: (NSEvent *)theEvent { [super editWithFrame: textCellFrameFromRect(frame) inView: controlView editor: textObj delegate: delegate event: theEvent]; /* [nc addObserver: self selector: @selector(textDidChange:) name: NSTextDidChangeNotification object: textObj]; [nc addObserver: self selector: @selector(textViewDidChangeSelection:) name: NSTextViewDidChangeSelectionNotification object: textObj]; */ } - (void) endEditing: (NSText *)editor { /* Close the pop up if it is still open. This may happen, e.g., when the user presses the Tab key to shift focus to a different cell or view. */ if (_popup) [_popup onWindowEdited: nil]; [super endEditing: editor]; [nc removeObserver: self name: NSTextDidChangeNotification object: editor]; [nc removeObserver: self name: NSTextViewDidChangeSelectionNotification object: editor]; } - (void) textViewDidChangeSelection: (NSNotification *)notification { _prevSelectedRange = [[[notification userInfo] objectForKey: @"NSOldSelectedCharacterRange"] rangeValue]; } - (void) textDidChange: (NSNotification *)notification { NSText *textObject = [notification object]; if ([self completes]) { NSString *myString = [[textObject string] copy]; NSString *more; NSUInteger myStringLength = [myString length]; NSUInteger location, length; NSRange selectedRange = [textObject selectedRange]; if (myStringLength != 0 && selectedRange.location == myStringLength && _prevSelectedRange.location < selectedRange.location) { more = [self completedString: myString]; if ((more != nil) && [more isEqualToString: myString] == NO) { [textObject setString: more]; location = myStringLength; length = [more length] - location; [textObject setSelectedRange: NSMakeRange(location, length)]; [textObject scrollRangeToVisible: NSMakeRange(location, length)]; } } RELEASE(myString); } } @end @implementation NSComboBoxCell (GNUstepPrivate) - (NSString *) _stringValueAtIndex: (NSInteger)index { if (_usesDataSource == NO) { return [[self itemObjectValueAtIndex: index] description]; } else { if (_dataSource == nil) { NSLog(@"%@: No data source currently specified", self); return nil; } else if ([_dataSource respondsToSelector: @selector(comboBox:objectValueForItemAtIndex:)]) { return [[_dataSource comboBox: (NSComboBox *)[self controlView] objectValueForItemAtIndex: index] description]; } else if ([_dataSource respondsToSelector: @selector(comboBoxCell:objectValueForItemAtIndex:)]) { return [[_dataSource comboBoxCell: self objectValueForItemAtIndex: index] description]; } } return nil; } - (void) _performClickWithFrame: (NSRect)cellFrame inView: (NSView *)controlView { NSWindow *cvWindow = [controlView window]; NSRect buttonRect = buttonCellFrameFromRect(cellFrame); _control_view = controlView; [controlView lockFocus]; [_buttonCell highlight: YES withFrame: buttonRect inView: controlView]; [controlView unlockFocus]; [cvWindow flushWindow]; [self _didClickWithinButton: self]; [controlView lockFocus]; [_buttonCell highlight: NO withFrame: buttonRect inView: controlView]; [controlView unlockFocus]; [cvWindow flushWindow]; } - (void) _didClickWithinButton: (id)sender { NSView *controlView = [self controlView]; if ((_cell.is_disabled) || (controlView == nil)) return; [nc postNotificationName: NSComboBoxWillPopUpNotification object: controlView userInfo: nil]; _popup = [self _popUp]; [_popup popUpForComboBoxCell: self]; _popup = nil; [nc postNotificationName: NSComboBoxWillDismissNotification object: controlView userInfo: nil]; } - (BOOL) _isWantedEvent: (NSEvent *)event { NSPoint loc; NSWindow *window = [event window]; NSView *controlView = [self controlView]; if (window == [[self controlView] window]) { loc = [event locationInWindow]; loc = [controlView convertPoint: loc fromView: nil]; return NSMouseInRect(loc, [self _textCellFrame], [controlView isFlipped]); } else { return NO; } } - (GSComboWindow *) _popUp { return [GSComboWindow defaultPopUp]; } - (NSRect) _textCellFrame { return textCellFrameFromRect(_lastValidFrame); } - (void) _setSelectedItem: (NSInteger)index { _selectedItem = index; } - (void) _loadButtonCell { _buttonCell = [[NSButtonCell alloc] initImageCell: [NSImage imageNamed: @"NSComboArrow"]]; [_buttonCell setImagePosition: NSImageOnly]; [_buttonCell setButtonType: NSMomentaryPushButton]; [_buttonCell setHighlightsBy: NSPushInCellMask]; [_buttonCell setBordered: YES]; [_buttonCell setTarget: self]; [_buttonCell setAction: @selector(_didClickWithinButton:)]; [_buttonCell setEnabled: [self isEnabled]]; } - (void) _selectCompleted { NSString *more; NSUInteger index = NSNotFound; more = [self completedString: [self stringValue]]; if (_usesDataSource) { if (_dataSource == nil) { NSLog(@"%@: No data source currently specified", self); } else { if ([_dataSource respondsToSelector: @selector(comboBoxCell:indexOfItemWithStringValue:)]) { index = [_dataSource comboBoxCell: self indexOfItemWithStringValue: more]; } } } else { index = [[self objectValues] indexOfObject: more]; } if (index != NSNotFound) { [self _setSelectedItem: index]; } // Otherwise keep old selection } @end