PageRenderTime 24ms CodeModel.GetById 17ms app.highlight 2ms RepoModel.GetById 1ms app.codeStats 1ms

/AppKit/CPMenu/_CPMenuWindow.j

http://github.com/cacaodev/cappuccino
Unknown | 584 lines | 442 code | 142 blank | 0 comment | 0 complexity | ca1c84e1bac7918d4106356c79905297 MD5 | raw file
  1/*
  2 * _CPMenuWindow.j
  3 * AppKit
  4 *
  5 * Created by Francisco Tolmasky.
  6 * Copyright 2009, 280 North, Inc.
  7 *
  8 * This library is free software; you can redistribute it and/or
  9 * modify it under the terms of the GNU Lesser General Public
 10 * License as published by the Free Software Foundation; either
 11 * version 2.1 of the License, or (at your option) any later version.
 12 *
 13 * This library is distributed in the hope that it will be useful,
 14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 16 * Lesser General Public License for more details.
 17 *
 18 * You should have received a copy of the GNU Lesser General Public
 19 * License along with this library; if not, write to the Free Software
 20 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 21 */
 22
 23@import "CPClipView.j"
 24@import "CPImageView.j"
 25@import "CPWindow.j"
 26@import "_CPMenuManager.j"
 27
 28@class _CPMenuView
 29
 30var _CPMenuWindowPool                       = [],
 31    _CPMenuWindowPoolCapacity               = 5,
 32
 33    _CPMenuWindowBackgroundColors           = [];
 34
 35_CPMenuWindowMenuBarBackgroundStyle         = 0;
 36_CPMenuWindowPopUpBackgroundStyle           = 1;
 37_CPMenuWindowAttachedMenuBackgroundStyle    = 2;
 38
 39/*
 40    @ignore
 41*/
 42@implementation _CPMenuWindow : CPWindow
 43{
 44    _CPMenuView         _menuView;
 45    CPClipView          _menuClipView;
 46
 47    CPImageView         _moreAboveView;
 48    CPImageView         _moreBelowView;
 49
 50    CGRect              _unconstrainedFrame;
 51    CGRect              _constraintRect;
 52}
 53
 54+ (id)menuWindowWithMenu:(CPMenu)aMenu font:(CPFont)aFont
 55{
 56    var menuWindow = nil;
 57
 58    if (_CPMenuWindowPool.length)
 59    {
 60        menuWindow = _CPMenuWindowPool.pop();
 61
 62        // Do this so that coordinates will be accurate.
 63        [menuWindow setFrameOrigin:CGPointMakeZero()];
 64    }
 65    else
 66        menuWindow = [[_CPMenuWindow alloc] init];
 67
 68    [menuWindow setFont:aFont];
 69    [menuWindow setMenu:aMenu];
 70    [menuWindow setMinWidth:[aMenu minimumWidth]];
 71
 72    return menuWindow;
 73}
 74
 75+ (void)poolMenuWindow:(_CPMenuWindow)aMenuWindow
 76{
 77    // FIXME :the poolMenuWindow is called too many times somewhere....
 78    if (!aMenuWindow || _CPMenuWindowPool.length >= _CPMenuWindowPoolCapacity || [_CPMenuWindowPool containsObject:aMenuWindow])
 79        return;
 80
 81    _CPMenuWindowPool.push(aMenuWindow);
 82}
 83
 84- (id)initWithContentRect:(CGRect)aRect styleMask:(unsigned)aStyleMask
 85{
 86    _constraintRect = CGRectMakeZero();
 87    _unconstrainedFrame = CGRectMakeZero();
 88
 89    self = [super initWithContentRect:aRect styleMask:CPBorderlessWindowMask];
 90
 91    if (self)
 92    {
 93        _constrainsToUsableScreen = NO;
 94
 95        [self setLevel:CPPopUpMenuWindowLevel];
 96        [self setHasShadow:YES];
 97        [self setShadowStyle:CPMenuWindowShadowStyle];
 98        [self setAcceptsMouseMovedEvents:YES];
 99
100        var contentView = [self contentView];
101
102        _menuView = [[_CPMenuView alloc] initWithFrame:CGRectMakeZero()];
103
104        _menuClipView = [[CPClipView alloc] initWithFrame:CGRectMake([_menuView valueForThemeAttribute:@"menu-window-margin-inset"].left, [_menuView valueForThemeAttribute:@"menu-window-margin-inset"].top, 0.0, 0.0)];
105        [_menuClipView setDocumentView:_menuView];
106
107        [contentView addSubview:_menuClipView];
108
109        _moreAboveView = [[CPImageView alloc] initWithFrame:CGRectMakeZero()];
110
111        [_moreAboveView setImage:[_menuView valueForThemeAttribute:@"menu-window-more-above-image"]];
112        [_moreAboveView setFrameSize:[[_menuView valueForThemeAttribute:@"menu-window-more-above-image"] size]];
113
114        [contentView addSubview:_moreAboveView];
115
116        _moreBelowView = [[CPImageView alloc] initWithFrame:CGRectMakeZero()];
117
118        [_moreBelowView setImage:[_menuView valueForThemeAttribute:@"menu-window-more-below-image"]];
119        [_moreBelowView setFrameSize:[[_menuView valueForThemeAttribute:@"menu-window-more-below-image"] size]];
120
121        [contentView addSubview:_moreBelowView];
122    }
123
124    return self;
125}
126
127+ (float)_standardLeftMargin
128{
129    return [[CPTheme defaultTheme] valueForAttributeWithName:@"menu-window-margin-inset" forClass:_CPMenuView].left;
130}
131
132- (void)setFont:(CPFont)aFont
133{
134    [_menuView setFont:aFont];
135}
136
137- (CPFont)font
138{
139    return [_menuView font];
140}
141
142+ (CPColor)backgroundColorForBackgroundStyle:(_CPMenuWindowBackgroundStyle)aBackgroundStyle
143{
144    var color = _CPMenuWindowBackgroundColors[aBackgroundStyle];
145
146    if (!color)
147    {
148        var bundle = [CPBundle bundleForClass:[self class]];
149
150        if (aBackgroundStyle == _CPMenuWindowPopUpBackgroundStyle)
151            color = [[CPTheme defaultTheme] valueForAttributeWithName:@"menu-window-pop-up-background-style-color" forClass:_CPMenuView];
152
153        else if (aBackgroundStyle == _CPMenuWindowMenuBarBackgroundStyle)
154            color = [[CPTheme defaultTheme] valueForAttributeWithName:@"menu-window-menu-bar-background-style-color" forClass:_CPMenuView];
155
156        _CPMenuWindowBackgroundColors[aBackgroundStyle] = color;
157    }
158
159    return color;
160}
161
162- (void)setBackgroundStyle:(_CPMenuWindowBackgroundStyle)aBackgroundStyle
163{
164    [self setBackgroundColor:[[self class] backgroundColorForBackgroundStyle:aBackgroundStyle]];
165}
166
167- (void)setMenu:(CPMenu)aMenu
168{
169    [aMenu _setMenuWindow:self];
170    [_menuView setMenu:aMenu];
171
172    var menuViewSize = [_menuView frame].size,
173        marginInset = [_menuView valueForThemeAttribute:@"menu-window-margin-inset"];
174
175    [self setFrameSize:CGSizeMake(marginInset.left + menuViewSize.width + marginInset.right, marginInset.top + menuViewSize.height + marginInset.bottom)];
176
177    [_menuView scrollPoint:CGPointMake(0.0, 0.0)];
178    [_menuClipView setFrame:CGRectMake(marginInset.left, marginInset.top, menuViewSize.width, menuViewSize.height)];
179}
180
181- (void)setMinWidth:(float)aWidth
182{
183    var size = [self unconstrainedFrame].size;
184
185    [self setFrameSize:CGSizeMake(MAX(size.width, aWidth), size.height)];
186}
187
188- (CPMenu)menu
189{
190    return [_menuView menu];
191}
192
193- (_CPMenuView)_menuView
194{
195    return _menuView;
196}
197
198- (void)orderFront:(id)aSender
199{
200    [[self menu] update];
201    [self setFrame:_unconstrainedFrame];
202
203    [super orderFront:aSender];
204}
205
206- (void)setConstraintRect:(CGRect)aRect
207{
208    _constraintRect = aRect;
209
210    [self setFrame:_unconstrainedFrame];
211}
212
213- (CGRect)unconstrainedFrame
214{
215    return CGRectMakeCopy(_unconstrainedFrame);
216}
217
218// We need this because if not this will call setFrame: with -frame instead of -unconstrainedFrame, turning
219// the constrained frame into the unconstrained frame.
220- (void)setFrameOrigin:(CGPoint)aPoint
221{
222    [super setFrame:CGRectMake(aPoint.x, aPoint.y, CGRectGetWidth(_unconstrainedFrame), CGRectGetHeight(_unconstrainedFrame))];
223}
224
225- (void)setFrameSize:(CGSize)aSize
226{
227    [super setFrame:CGRectMake(CGRectGetMinX(_unconstrainedFrame), CGRectGetMinY(_unconstrainedFrame), aSize.width, aSize.height)];
228}
229
230- (void)setFrame:(CGRect)aFrame display:(BOOL)shouldDisplay animate:(BOOL)shouldAnimate
231{
232    // FIXME: There are integral window issues with platform windows.
233    // FIXME: This gets called far too often.
234    _unconstrainedFrame = CGRectMakeCopy(aFrame);
235
236    var constrainedFrame = CGRectIntersection(_unconstrainedFrame, _constraintRect),
237        marginInset = [_menuView valueForThemeAttribute:@"menu-window-margin-inset"],
238        scrollIndicatorHeight = [_menuView valueForThemeAttribute:@"menu-window-scroll-indicator-height"];
239
240    // We don't want to simply intersect the visible frame and the unconstrained frame.
241    // We should be allowing as much of the width to fit as possible (pushing back and forward).
242    constrainedFrame.origin.x = CGRectGetMinX(_unconstrainedFrame);
243    constrainedFrame.size.width = CGRectGetWidth(_unconstrainedFrame);
244
245    if (CGRectGetWidth(constrainedFrame) > CGRectGetWidth(_constraintRect))
246        constrainedFrame.size.width = CGRectGetWidth(_constraintRect);
247
248    if (CGRectGetMaxX(constrainedFrame) > CGRectGetMaxX(_constraintRect))
249        constrainedFrame.origin.x -= CGRectGetMaxX(constrainedFrame) - CGRectGetMaxX(_constraintRect);
250
251    if (CGRectGetMinX(constrainedFrame) < CGRectGetMinX(_constraintRect))
252        constrainedFrame.origin.x = CGRectGetMinX(_constraintRect);
253
254    [super setFrame:constrainedFrame display:shouldDisplay animate:shouldAnimate];
255
256    // This needs to happen before changing the frame.
257    var menuViewOrigin = CGPointMake(CGRectGetMinX(aFrame) + marginInset.left, CGRectGetMinY(aFrame) + marginInset.top),
258        moreAbove = menuViewOrigin.y < CGRectGetMinY(constrainedFrame) + marginInset.top,
259        moreBelow = menuViewOrigin.y + CGRectGetHeight([_menuView frame]) > CGRectGetMaxY(constrainedFrame) - marginInset.bottom,
260
261        topMargin = marginInset.top,
262        bottomMargin = marginInset.bottom,
263
264        contentView = [self contentView],
265        bounds = [contentView bounds];
266
267    if (moreAbove)
268    {
269        topMargin += scrollIndicatorHeight;
270
271        var frame = [_moreAboveView frame];
272
273        [_moreAboveView setFrameOrigin:CGPointMake((CGRectGetWidth(bounds) - CGRectGetWidth(frame)) / 2.0, (marginInset.top + scrollIndicatorHeight - CGRectGetHeight(frame)) / 2.0)];
274    }
275
276    [_moreAboveView setHidden:!moreAbove];
277
278    if (moreBelow)
279    {
280        bottomMargin += scrollIndicatorHeight;
281
282        [_moreBelowView setFrameOrigin:CGPointMake((CGRectGetWidth(bounds) - CGRectGetWidth([_moreBelowView frame])) / 2.0, CGRectGetHeight(bounds) - scrollIndicatorHeight - marginInset.bottom)];
283    }
284
285    [_moreBelowView setHidden:!moreBelow];
286
287    var clipFrame = CGRectMakeZero();
288
289    clipFrame.origin.x = marginInset.left;
290    clipFrame.origin.y = topMargin;
291    clipFrame.size.width = CGRectGetWidth(constrainedFrame) - marginInset.left - marginInset.right;
292    clipFrame.size.height = CGRectGetHeight(constrainedFrame) - topMargin - bottomMargin;
293
294    [_menuClipView setFrame:clipFrame];
295    [_menuView setFrameSize:CGSizeMake(CGRectGetWidth(clipFrame), CGRectGetHeight([_menuView frame]))];
296
297    [_menuView scrollPoint:CGPointMake(0.0, [self convertBaseToGlobal:clipFrame.origin].y - menuViewOrigin.y)];
298}
299
300- (BOOL)hasMinimumNumberOfVisibleItems
301{
302    var visibleRect = [_menuView visibleRect];
303
304    // Clearly if the entire view isn't visible the minimum won't be visible.
305    if (CGRectIsEmpty(visibleRect))
306        return NO;
307
308    var numberOfUnhiddenItems = [_menuView numberOfUnhiddenItems],
309        minimumNumberOfVisibleItems = MIN(numberOfUnhiddenItems, 3),
310        count = 0,
311        index = [_menuView itemIndexAtPoint:[_menuView convertPoint:[_menuClipView frame].origin fromView:nil]];
312
313    for (; index < numberOfUnhiddenItems && count < minimumNumberOfVisibleItems; ++index)
314    {
315        var itemRect = [_menuView rectForUnhiddenItemAtIndex:index],
316            visibleItemRect = CGRectIntersection(visibleRect, itemRect);
317
318        // As soon as we get to the first unhidden item that is no longer visible, stop.
319        if (CGRectIsEmpty(visibleItemRect))
320            break;
321
322        // If the item is *completely* visible, count it.
323        if (CGRectEqualToRect(visibleItemRect, itemRect))
324            ++count;
325    }
326
327    return count >= minimumNumberOfVisibleItems;
328}
329
330- (BOOL)canScrollUp
331{
332    return ![_moreAboveView isHidden];
333}
334
335- (BOOL)canScrollDown
336{
337    return ![_moreBelowView isHidden];
338}
339
340- (BOOL)canScroll
341{
342    return [self canScrollUp] || [self canScrollDown];
343}
344
345- (void)scrollByDelta:(float)theDelta
346{
347    if (theDelta === 0.0)
348        return;
349
350    if (theDelta > 0.0 && ![self canScrollDown])
351        return;
352
353    if (theDelta < 0.0 && ![self canScrollUp])
354        return;
355
356    _unconstrainedFrame.origin.y -= theDelta;
357    [self setFrame:_unconstrainedFrame];
358}
359
360- (void)scrollUp
361{
362    [self scrollByDelta:-10.0];
363}
364
365- (void)scrollDown
366{
367    [self scrollByDelta:10.0];
368}
369
370@end
371
372@implementation _CPMenuWindow (CPMenuContainer)
373
374- (CGRect)globalFrame
375{
376    return [self frame];
377}
378
379- (BOOL)isMenuBar
380{
381    return NO;
382}
383
384- (_CPManagerScrollingState)scrollingStateForPoint:(CGPoint)aGlobalLocation
385{
386    var frame = [self frame];
387    if (!CGRectContainsPoint(frame,aGlobalLocation) || ![self canScroll])
388        return _CPMenuManagerScrollingStateNone;
389
390    // If we're at or above of the top scroll indicator...
391    if (aGlobalLocation.y < CGRectGetMinY(frame) + [_menuView valueForThemeAttribute:@"menu-window-margin-inset"].top + [_menuView valueForThemeAttribute:@"menu-window-scroll-indicator-height"] &&  ![_moreAboveView isHidden])
392        return _CPMenuManagerScrollingStateUp;
393
394    // If we're at or below the bottom scroll indicator...
395    if (aGlobalLocation.y > CGRectGetMaxY(frame) - [_menuView valueForThemeAttribute:@"menu-window-margin-inset"].bottom - [_menuView valueForThemeAttribute:@"menu-window-scroll-indicator-height"] &&  ![_moreBelowView isHidden])
396        return _CPMenuManagerScrollingStateDown;
397
398    return _CPMenuManagerScrollingStateNone;
399}
400
401- (float)deltaYForItemAtIndex:(int)anIndex
402{
403    return [_menuView valueForThemeAttribute:@"menu-window-margin-inset"].top + CGRectGetMinY([_menuView rectForItemAtIndex:anIndex]);
404}
405
406- (CGPoint)rectForItemAtIndex:(int)anIndex
407{
408    return [_menuView convertRect:[_menuView rectForItemAtIndex:anIndex] toView:nil];
409}
410
411- (int)itemIndexAtPoint:(CGPoint)aPoint
412{
413    // Don't return indexes of items that aren't visible.
414    if (!CGRectContainsPoint([_menuClipView bounds], [_menuClipView convertPoint:aPoint fromView:nil]))
415        return CPNotFound;
416
417    return [_menuView itemIndexAtPoint:[_menuView convertPoint:aPoint fromView:nil]];
418}
419
420@end
421
422/*
423    @ignore
424*/
425
426@implementation _CPMenuView : CPView
427{
428    CPArray _menuItemViews;
429    CPArray _visibleMenuItemInfos;
430
431    CPFont  _font @accessors(property=font);
432}
433
434
435+ (CPString)defaultThemeClass
436{
437    return "menu-view";
438}
439
440+ (CPDictionary)themeAttributes
441{
442    return @{
443            @"menu-window-more-above-image": [CPNull null],
444            @"menu-window-more-below-image": [CPNull null],
445            @"menu-window-pop-up-background-style-color": [CPNull null],
446            @"menu-window-menu-bar-background-style-color": [CPNull null],
447            @"menu-window-margin-inset": CGInsetMake(5.0, 1.0, 1.0, 5.0),
448            @"menu-window-scroll-indicator-height": 16.0,
449            @"menu-bar-window-background-color": [CPNull null],
450            @"menu-bar-window-background-selected-color": [CPNull null],
451            @"menu-bar-window-font": [CPNull null],
452            @"menu-bar-window-height": 30.0,
453            @"menu-bar-window-margin": 10.0,
454            @"menu-bar-window-left-margin": 10.0,
455            @"menu-bar-window-right-margin": 10.0,
456            @"menu-bar-text-color": [CPNull null],
457            @"menu-bar-title-color": [CPNull null],
458            @"menu-bar-text-shadow-color": [CPNull null],
459            @"menu-bar-title-shadow-color": [CPNull null],
460            @"menu-bar-highlight-color": [CPNull null],
461            @"menu-bar-highlight-text-color": [CPNull null],
462            @"menu-bar-highlight-text-shadow-color": [CPNull null],
463            @"menu-bar-height": 28.0,
464            @"menu-bar-icon-image": [CPNull null],
465            @"menu-bar-icon-image-alpha-value": 1.0,
466            @"menu-general-icon-new": [CPNull null],
467            @"menu-general-icon-save": [CPNull null],
468            @"menu-general-icon-open": [CPNull null],
469        };
470}
471
472- (unsigned)numberOfUnhiddenItems
473{
474    return _visibleMenuItemInfos.length;
475}
476
477- (CGRect)rectForUnhiddenItemAtIndex:(int)anIndex
478{
479    return [self rectForItemAtIndex:_visibleMenuItemInfos[anIndex].index];
480}
481
482- (CGRect)rectForItemAtIndex:(int)anIndex
483{
484    return [_menuItemViews[anIndex === CPNotFound ? 0 : anIndex] frame];
485}
486
487- (int)itemIndexAtPoint:(CGPoint)aPoint
488{
489    var x = aPoint.x,
490        bounds = [self bounds];
491
492    if (x < CGRectGetMinX(bounds) || x > CGRectGetMaxX(bounds))
493        return CPNotFound;
494
495    var y = aPoint.y,
496        low = 0,
497        high = _visibleMenuItemInfos.length - 1;
498
499    while (low <= high)
500    {
501        var middle = FLOOR(low + (high - low) / 2),
502            info = _visibleMenuItemInfos[middle],
503            frame = [info.view frame];
504
505        if (y < CGRectGetMinY(frame))
506            high = middle - 1;
507
508        else if (y > CGRectGetMaxY(frame))
509            low = middle + 1;
510
511        else
512            return info.index;
513   }
514
515   return CPNotFound;
516}
517
518- (void)tile
519{
520    [_menuItemViews makeObjectsPerformSelector:@selector(removeFromSuperview)];
521
522    _menuItemViews = [];
523    _visibleMenuItemInfos = [];
524
525    var menu = [self menu];
526
527    if (!menu)
528        return;
529
530    var items = [menu itemArray],
531        index = 0,
532        count = [items count],
533        maxWidth = 0,
534        y = 0,
535        showsStateColumn = [menu showsStateColumn];
536
537    for (; index < count; ++index)
538    {
539        var item = items[index],
540            view = [item _menuItemView];
541
542        _menuItemViews.push(view);
543
544        if ([item isHidden])
545            continue;
546
547        _visibleMenuItemInfos.push({ view:view, index:index });
548
549        [view setFont:_font];
550        [view setShowsStateColumn:showsStateColumn];
551        [view synchronizeWithMenuItem];
552
553        [view setFrameOrigin:CGPointMake(0.0, y)];
554
555        [self addSubview:view];
556
557        var size = [view minSize],
558            width = size.width;
559
560        if (maxWidth < width)
561            maxWidth = width;
562
563        y += size.height;
564    }
565
566    for (index = 0; index < count; ++index)
567    {
568        var view = _menuItemViews[index];
569
570        [view setFrameSize:CGSizeMake(maxWidth, CGRectGetHeight([view frame]))];
571    }
572
573    [self setAutoresizesSubviews:NO];
574    [self setFrameSize:CGSizeMake(maxWidth, y)];
575    [self setAutoresizesSubviews:YES];
576}
577
578- (void)setMenu:(CPMenu)aMenu
579{
580    [super setMenu:aMenu];
581    [self tile];
582}
583
584@end