![]() |
API 0.9.5
|
00001 /* 00002 * CPPopUpButton.j 00003 * AppKit 00004 * 00005 * Created by Francisco Tolmasky. 00006 * Copyright 2008, 280 North, Inc. 00007 * 00008 * This library is free software; you can redistribute it and/or 00009 * modify it under the terms of the GNU Lesser General Public 00010 * License as published by the Free Software Foundation; either 00011 * version 2.1 of the License, or (at your option) any later version. 00012 * 00013 * This library is distributed in the hope that it will be useful, 00014 * but WITHOUT ANY WARRANTY; without even the implied warranty of 00015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 00016 * Lesser General Public License for more details. 00017 * 00018 * You should have received a copy of the GNU Lesser General Public 00019 * License along with this library; if not, write to the Free Software 00020 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 00021 */ 00022 00023 00024 00025 var VISIBLE_MARGIN = 7.0; 00026 00027 CPPopUpButtonStatePullsDown = CPThemeState("pulls-down"); 00028 00035 @implementation CPPopUpButton : CPButton 00036 { 00037 CPUInteger _selectedIndex; 00038 CPRectEdge _preferredEdge; 00039 } 00040 00041 + (CPString)defaultThemeClass 00042 { 00043 return "popup-button"; 00044 } 00045 00046 + (CPSet)keyPathsForValuesAffectingSelectedIndex 00047 { 00048 return [CPSet setWithObject:@"objectValue"]; 00049 } 00050 00051 + (CPSet)keyPathsForValuesAffectingSelectedTag 00052 { 00053 return [CPSet setWithObject:@"objectValue"]; 00054 } 00055 00056 + (CPSet)keyPathsForValuesAffectingSelectedItem 00057 { 00058 return [CPSet setWithObject:@"objectValue"]; 00059 } 00060 00067 - (id)initWithFrame:(CGRect)aFrame pullsDown:(BOOL)shouldPullDown 00068 { 00069 self = [super initWithFrame:aFrame]; 00070 00071 if (self) 00072 { 00073 [self selectItemAtIndex:CPNotFound]; 00074 00075 _preferredEdge = CPMaxYEdge; 00076 00077 [self setValue:CPImageLeft forThemeAttribute:@"image-position"]; 00078 [self setValue:CPLeftTextAlignment forThemeAttribute:@"alignment"]; 00079 [self setValue:CPLineBreakByTruncatingTail forThemeAttribute:@"line-break-mode"]; 00080 00081 [self setMenu:[[CPMenu alloc] initWithTitle:@""]]; 00082 00083 [self setPullsDown:shouldPullDown]; 00084 00085 var options = CPKeyValueObservingOptionNew | CPKeyValueObservingOptionOld; // | CPKeyValueObservingOptionInitial; 00086 [self addObserver:self forKeyPath:@"menu.items" options:options context:nil]; 00087 [self addObserver:self forKeyPath:@"_firstItem.changeCount" options:options context:nil]; 00088 [self addObserver:self forKeyPath:@"selectedItem.changeCount" options:options context:nil]; 00089 } 00090 00091 return self; 00092 } 00093 00094 - (id)initWithFrame:(CGRect)aFrame 00095 { 00096 return [self initWithFrame:aFrame pullsDown:NO]; 00097 } 00098 00099 // Setting the Type of Menu 00100 00109 - (void)setPullsDown:(BOOL)shouldPullDown 00110 { 00111 if (shouldPullDown) 00112 var changed = [self setThemeState:CPPopUpButtonStatePullsDown]; 00113 else 00114 var changed = [self unsetThemeState:CPPopUpButtonStatePullsDown]; 00115 00116 if (!changed) 00117 return; 00118 00119 var items = [[self menu] itemArray]; 00120 00121 if ([items count] <= 0) 00122 return; 00123 00124 [items[0] setHidden:[self pullsDown]]; 00125 00126 [self synchronizeTitleAndSelectedItem]; 00127 } 00128 00132 - (BOOL)pullsDown 00133 { 00134 return [self hasThemeState:CPPopUpButtonStatePullsDown]; 00135 } 00136 00137 // Inserting and Deleting Items 00138 00142 - (void)addItem:(CPMenuItem)anItem 00143 { 00144 [[self menu] addItem:anItem]; 00145 } 00146 00151 - (void)addItemWithTitle:(CPString)aTitle 00152 { 00153 [[self menu] addItemWithTitle:aTitle action:NULL keyEquivalent:nil]; 00154 } 00155 00160 - (void)addItemsWithTitles:(CPArray)titles 00161 { 00162 var index = 0, 00163 count = [titles count]; 00164 00165 for (; index < count; ++index) 00166 [self addItemWithTitle:titles[index]]; 00167 } 00168 00174 - (void)insertItemWithTitle:(CPString)aTitle atIndex:(int)anIndex 00175 { 00176 var items = [self itemArray], 00177 count = [items count]; 00178 00179 while (count--) 00180 if ([items[count] title] == aTitle) 00181 [self removeItemAtIndex:count]; 00182 00183 [[self menu] insertItemWithTitle:aTitle action:NULL keyEquivalent:nil atIndex:anIndex]; 00184 } 00185 00189 - (void)removeAllItems 00190 { 00191 [[self menu] removeAllItems]; 00192 [self synchronizeTitleAndSelectedItem]; 00193 } 00194 00199 - (void)removeItemWithTitle:(CPString)aTitle 00200 { 00201 [self removeItemAtIndex:[self indexOfItemWithTitle:aTitle]]; 00202 [self synchronizeTitleAndSelectedItem]; 00203 } 00204 00209 - (void)removeItemAtIndex:(int)anIndex 00210 { 00211 [[self menu] removeItemAtIndex:anIndex]; 00212 [self synchronizeTitleAndSelectedItem]; 00213 } 00214 00215 // Getting the User's Selection 00219 - (CPMenuItem)selectedItem 00220 { 00221 var indexOfSelectedItem = [self indexOfSelectedItem]; 00222 00223 if (indexOfSelectedItem < 0 || indexOfSelectedItem > [self numberOfItems] - 1) 00224 return nil; 00225 00226 return [[self menu] itemAtIndex:indexOfSelectedItem]; 00227 } 00228 00232 - (CPString)titleOfSelectedItem 00233 { 00234 return [[self selectedItem] title]; 00235 } 00236 00240 - (int)indexOfSelectedItem 00241 { 00242 return _selectedIndex; 00243 } 00244 00248 - (int)selectedTag 00249 { 00250 return [[self selectedItem] tag]; 00251 } 00252 00256 - (void)_setSelectedTag:(int)aTag 00257 { 00258 [self selectItemWithTag:aTag]; 00259 } 00260 00261 // Setting the Current Selection 00266 - (void)selectItem:(CPMenuItem)aMenuItem 00267 { 00268 [self selectItemAtIndex:[self indexOfItem:aMenuItem]]; 00269 } 00270 00275 - (void)selectItemAtIndex:(CPUInteger)anIndex 00276 { 00277 [self setObjectValue:anIndex]; 00278 } 00279 00280 - (void)setSelectedIndex:(CPUInteger)anIndex 00281 { 00282 [self setObjectValue:anIndex]; 00283 } 00284 00285 - (CPUInteger)selectedIndex 00286 { 00287 return [self objectValue]; 00288 } 00289 00294 - (void)setObjectValue:(int)anIndex 00295 { 00296 var indexOfSelectedItem = [self objectValue]; 00297 00298 anIndex = parseInt(+anIndex, 10); 00299 00300 if (indexOfSelectedItem === anIndex) 00301 return; 00302 00303 if (indexOfSelectedItem >= 0 && ![self pullsDown]) 00304 [[self selectedItem] setState:CPOffState]; 00305 00306 _selectedIndex = anIndex; 00307 00308 if (indexOfSelectedItem >= 0 && ![self pullsDown]) 00309 [[self selectedItem] setState:CPOnState]; 00310 00311 [self synchronizeTitleAndSelectedItem]; 00312 } 00313 00314 - (id)objectValue 00315 { 00316 return _selectedIndex; 00317 } 00318 00323 - (void)selectItemWithTag:(int)aTag 00324 { 00325 [self selectItemAtIndex:[self indexOfItemWithTag:aTag]]; 00326 } 00327 00332 - (void)selectItemWithTitle:(CPString)aTitle 00333 { 00334 [self selectItemAtIndex:[self indexOfItemWithTitle:aTitle]]; 00335 } 00336 00337 // Getting Menu Items 00338 00342 - (int)numberOfItems 00343 { 00344 return [[self menu] numberOfItems]; 00345 } 00346 00350 - (CPArray)itemArray 00351 { 00352 return [[self menu] itemArray]; 00353 } 00354 00359 - (CPMenuItem)itemAtIndex:(unsigned)anIndex 00360 { 00361 return [[self menu] itemAtIndex:anIndex]; 00362 } 00363 00368 - (CPString)itemTitleAtIndex:(unsigned)anIndex 00369 { 00370 return [[[self menu] itemAtIndex:anIndex] title]; 00371 } 00372 00376 - (CPArray)itemTitles 00377 { 00378 var titles = [], 00379 items = [self itemArray], 00380 index = 0, 00381 count = [items count]; 00382 00383 for (; index < count; ++index) 00384 titles.push([items[index] title]); 00385 00386 return titles; 00387 } 00388 00393 - (CPMenuItem)itemWithTitle:(CPString)aTitle 00394 { 00395 var menu = [self menu], 00396 itemIndex = [menu indexOfItemWithTitle:aTitle]; 00397 00398 if (itemIndex === CPNotFound) 00399 return nil; 00400 00401 return [menu itemAtIndex:itemIndex]; 00402 } 00403 00407 - (CPMenuItem)lastItem 00408 { 00409 return [[[self menu] itemArray] lastObject]; 00410 } 00411 00412 // Getting the Indices of Menu Items 00417 - (int)indexOfItem:(CPMenuItem)aMenuItem 00418 { 00419 return [[self menu] indexOfItem:aMenuItem]; 00420 } 00421 00426 - (int)indexOfItemWithTag:(int)aTag 00427 { 00428 return [[self menu] indexOfItemWithTag:aTag]; 00429 } 00430 00435 - (int)indexOfItemWithTitle:(CPString)aTitle 00436 { 00437 return [[self menu] indexOfItemWithTitle:aTitle]; 00438 } 00439 00446 - (int)indexOfItemWithRepresentedObject:(id)anObject 00447 { 00448 return [[self menu] indexOfItemWithRepresentedObject:anObject]; 00449 } 00450 00458 - (int)indexOfItemWithTarget:(id)aTarget action:(SEL)anAction 00459 { 00460 return [[self menu] indexOfItemWithTarget:aTarget action:anAction]; 00461 } 00462 00463 // Setting the Cell Edge to Pop out in Restricted Situations 00469 - (CPRectEdge)preferredEdge 00470 { 00471 return _preferredEdge; 00472 } 00473 00479 - (void)setPreferredEdge:(CPRectEdge)aRectEdge 00480 { 00481 _preferredEdge = aRectEdge; 00482 } 00483 00484 // Setting the Title 00489 - (void)setTitle:(CPString)aTitle 00490 { 00491 if ([self title] === aTitle) 00492 return; 00493 00494 if ([self pullsDown]) 00495 { 00496 var items = [[self menu] itemArray]; 00497 00498 if ([items count] <= 0) 00499 [self addItemWithTitle:aTitle]; 00500 00501 else 00502 { 00503 [items[0] setTitle:aTitle]; 00504 [self synchronizeTitleAndSelectedItem]; 00505 } 00506 } 00507 else 00508 { 00509 var index = [self indexOfItemWithTitle:aTitle]; 00510 00511 if (index < 0) 00512 { 00513 [self addItemWithTitle:aTitle]; 00514 00515 index = [self numberOfItems] - 1; 00516 } 00517 00518 [self selectItemAtIndex:index]; 00519 } 00520 } 00521 00522 // Setting the Image 00528 - (void)setImage:(CPImage)anImage 00529 { 00530 // The Image is set by the currently selected item. 00531 } 00532 00533 // Setting the State 00538 - (void)synchronizeTitleAndSelectedItem 00539 { 00540 var item = nil; 00541 00542 if ([self pullsDown]) 00543 { 00544 var items = [[self menu] itemArray]; 00545 00546 if ([items count] > 0) 00547 item = items[0]; 00548 } 00549 else 00550 item = [self selectedItem]; 00551 00552 [super setImage:[item image]]; 00553 [super setTitle:[item title]]; 00554 } 00555 00556 - (void)observeValueForKeyPath:(CPString)aKeyPath ofObject:(id)anObject change:(CPDictionary)changes context:(id)aContext 00557 { 00558 var pullsDown = [self pullsDown]; 00559 00560 if (!pullsDown && aKeyPath === @"selectedItem.changeCount" || 00561 pullsDown && (aKeyPath === @"_firstItem" || aKeyPath === @"_firstItem.changeCount")) 00562 [self synchronizeTitleAndSelectedItem]; 00563 00564 // FIXME: This is due to a bug in KVO, we should never get it for "menu". 00565 if (aKeyPath === @"menu") 00566 { 00567 aKeyPath = @"menu.items"; 00568 00569 [changes setObject:CPKeyValueChangeSetting forKey:CPKeyValueChangeKindKey]; 00570 [changes setObject:[[self menu] itemArray] forKey:CPKeyValueChangeNewKey]; 00571 } 00572 00573 if (aKeyPath === @"menu.items") 00574 { 00575 var changeKind = [changes objectForKey:CPKeyValueChangeKindKey], 00576 indexOfSelectedItem = [self indexOfSelectedItem]; 00577 00578 if (changeKind === CPKeyValueChangeRemoval) 00579 { 00580 var index = CPNotFound, 00581 indexes = [changes objectForKey:CPKeyValueChangeIndexesKey]; 00582 00583 if ([indexes containsIndex:0] && [self pullsDown]) 00584 [self _firstItemDidChange]; 00585 00586 // See whether the index has changed, despite the actual item not changing. 00587 while ((index = [indexes indexGreaterThanIndex:index]) !== CPNotFound && 00588 index <= indexOfSelectedItem) 00589 --indexOfSelectedItem; 00590 00591 [self selectItemAtIndex:indexOfSelectedItem]; 00592 } 00593 00594 else if (changeKind === CPKeyValueChangeReplacement) 00595 { 00596 var indexes = [changes objectForKey:CPKeyValueChangeIndexesKey]; 00597 00598 if (pullsDown && [indexes containsIndex:0] || 00599 !pullsDown && [indexes containsIndex:indexOfSelectedItem]) 00600 [self synchronizeTitleAndSelectedItem]; 00601 } 00602 00603 else 00604 { 00605 // No matter what, we want to prepare the new items. 00606 var newItems = [changes objectForKey:CPKeyValueChangeNewKey]; 00607 00608 [newItems enumerateObjectsUsingBlock:function(aMenuItem) 00609 { 00610 var action = [aMenuItem action]; 00611 00612 if (!action) 00613 [aMenuItem setAction:action = @selector(_popUpItemAction:)]; 00614 00615 if (action === @selector(_popUpItemAction:)) 00616 [aMenuItem setTarget:self]; 00617 }]; 00618 00619 if (changeKind === CPKeyValueChangeSetting) 00620 { 00621 [self _firstItemDidChange]; 00622 00623 [self selectItemAtIndex:CPNotFound]; 00624 [self selectItemAtIndex:MIN([newItems count] - 1, indexOfSelectedItem)]; 00625 } 00626 00627 else //if (changeKind === CPKeyValueChangeInsertion) 00628 { 00629 var indexes = [changes objectForKey:CPKeyValueChangeIndexesKey]; 00630 00631 if ([self pullsDown] && [indexes containsIndex:0]) 00632 { 00633 [self _firstItemDidChange]; 00634 00635 if ([self numberOfItems] > 1) 00636 { 00637 var index = CPNotFound, 00638 originalIndex = 0; 00639 00640 while ((index = [indexes indexGreaterThanIndex:index]) !== CPNotFound && 00641 index <= originalIndex) 00642 ++originalIndex; 00643 00644 [[self itemAtIndex:originalIndex] setHidden:NO]; 00645 } 00646 } 00647 00648 if (indexOfSelectedItem < 0) 00649 [self selectItemAtIndex:0]; 00650 00651 else 00652 { 00653 var index = CPNotFound; 00654 00655 // See whether the index has changed, despite the actual item not changing. 00656 while ((index = [indexes indexGreaterThanIndex:index]) !== CPNotFound && 00657 index <= indexOfSelectedItem) 00658 ++indexOfSelectedItem; 00659 00660 [self selectItemAtIndex:indexOfSelectedItem]; 00661 } 00662 } 00663 } 00664 } 00665 00666 // [super observeValueForKeyPath:aKeyPath ofObject:anObject change:changes context:aContext]; 00667 } 00668 00669 - (void)mouseDown:(CPEvent)anEvent 00670 { 00671 if (![self isEnabled] || ![self numberOfItems]) 00672 return; 00673 00674 [self highlight:YES]; 00675 00676 var menu = [self menu], 00677 bounds = [self bounds], 00678 minimumWidth = CGRectGetWidth(bounds); 00679 00680 // FIXME: setFont: should set the font on the menu. 00681 [menu setFont:[self font]]; 00682 00683 if ([self pullsDown]) 00684 { 00685 var positionedItem = nil, 00686 location = CGPointMake(0.0, CGRectGetMaxY(bounds)); 00687 } 00688 else 00689 { 00690 var contentRect = [self contentRectForBounds:bounds], 00691 positionedItem = [self selectedItem], 00692 standardLeftMargin = [_CPMenuWindow _standardLeftMargin] + [_CPMenuItemStandardView _standardLeftMargin], 00693 location = CGPointMake(CGRectGetMinX(contentRect) - standardLeftMargin, 0.0); 00694 00695 minimumWidth += standardLeftMargin; 00696 00697 // To ensure the selected item is highlighted correctly, unset the highlighted item 00698 [menu _highlightItemAtIndex:CPNotFound]; 00699 } 00700 00701 [menu setMinimumWidth:minimumWidth]; 00702 00703 [menu 00704 _popUpMenuPositioningItem:positionedItem 00705 atLocation:location 00706 topY:CGRectGetMinY(bounds) 00707 bottomY:CGRectGetMaxY(bounds) 00708 inView:self 00709 callback:function(aMenu) 00710 { 00711 [self highlight:NO]; 00712 00713 var highlightedItem = [aMenu highlightedItem]; 00714 00715 if ([highlightedItem _isSelectable]) 00716 [self selectItem:highlightedItem]; 00717 }]; 00718 /* 00719 else 00720 { 00721 // This is confusing, I KNOW, so let me explain it to you. 00722 // We want the *content* of the selected menu item to overlap the *content* of our pop up. 00723 // 1. So calculate where our content is, then calculate where the menu item is. 00724 // 2. Move LEFT by whatever indentation we have (offsetWidths, aka, window margin, item margin, etc). 00725 // 3. MOVE UP by the difference in sizes of the content and menu item, this will only work if the content is vertically centered. 00726 var contentRect = [self convertRect:[self contentRectForBounds:bounds] toView:nil], 00727 menuOrigin = [theWindow convertBaseToGlobal:contentRect.origin], 00728 menuItemRect = [menuWindow rectForItemAtIndex:_selectedIndex]; 00729 00730 menuOrigin.x -= CGRectGetMinX(menuItemRect) + [menuWindow overlapOffsetWidth] + [[[menu itemAtIndex:_selectedIndex] _menuItemView] overlapOffsetWidth]; 00731 menuOrigin.y -= CGRectGetMinY(menuItemRect) + (CGRectGetHeight(menuItemRect) - CGRectGetHeight(contentRect)) / 2.0; 00732 } 00733 */ 00734 } 00735 00736 - (void)rightMouseDown:(CPEvent)anEvent 00737 { 00738 // Disable standard CPView behavior which incorrectly displays the menu as a 'context menu'. 00739 } 00740 00741 - (void)_popUpItemAction:(id)aSender 00742 { 00743 [self sendAction:[self action] to:[self target]]; 00744 } 00745 00746 - (void)_firstItemDidChange 00747 { 00748 [self willChangeValueForKey:@"_firstItem"]; 00749 [self didChangeValueForKey:@"_firstItem"]; 00750 00751 [[self _firstItem] setHidden:YES]; 00752 } 00753 00754 - (CPMenuItem)_firstItem 00755 { 00756 if ([self numberOfItems] <= 0) 00757 return nil; 00758 00759 return [[self menu] itemAtIndex:0]; 00760 } 00761 00762 - (void)takeValueFromKeyPath:(CPString)aKeyPath ofObjects:(CPArray)objects 00763 { 00764 var count = objects.length, 00765 value = [objects[0] valueForKeyPath:aKeyPath]; 00766 00767 [self selectItemWithTag:value]; 00768 [self setEnabled:YES]; 00769 00770 while (count-- > 1) 00771 if (value !== [objects[count] valueForKeyPath:aKeyPath]) 00772 [[self selectedItem] setState:CPOffState]; 00773 } 00774 00775 @end 00776 00777 var DEPRECATED_CPPopUpButtonMenuKey = @"CPPopUpButtonMenuKey", 00778 DEPRECATED_CPPopUpButtonSelectedIndexKey = @"CPPopUpButtonSelectedIndexKey"; 00779 00780 @implementation CPPopUpButton (CPCoding) 00788 - (id)initWithCoder:(CPCoder)aCoder 00789 { 00790 self = [super initWithCoder:aCoder]; 00791 00792 if (self) 00793 { 00794 // FIXME: (or not?) _title is nulled in - [CPButton initWithCoder:], 00795 // so we need to do this again. 00796 [self synchronizeTitleAndSelectedItem]; 00797 00798 // FIXME: Remove deprecation leniency for 1.0 00799 if ([aCoder containsValueForKey:DEPRECATED_CPPopUpButtonMenuKey]) 00800 { 00801 CPLog.warn(self + " was encoded with an older version of Cappuccino. Please nib2cib the original nib again or open and re-save in Atlas."); 00802 00803 [self setMenu:[aCoder decodeObjectForKey:DEPRECATED_CPPopUpButtonMenuKey]]; 00804 [self setObjectValue:[aCoder decodeObjectForKey:DEPRECATED_CPPopUpButtonSelectedIndexKey]]; 00805 } 00806 00807 var options = CPKeyValueObservingOptionNew | CPKeyValueObservingOptionOld;/* | CPKeyValueObservingOptionInitial */ 00808 00809 [self addObserver:self forKeyPath:@"menu.items" options:options context:nil]; 00810 [self addObserver:self forKeyPath:@"_firstItem.changeCount" options:options context:nil]; 00811 [self addObserver:self forKeyPath:@"selectedItem.changeCount" options:options context:nil]; 00812 } 00813 00814 return self; 00815 } 00816 00817 @end