API 0.9.5
AppKit/CPPopUpButton.j
Go to the documentation of this file.
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
 All Classes Files Functions Variables Defines