mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-08-25 13:07:02 +00:00
595 lines
18 KiB
Plaintext
595 lines
18 KiB
Plaintext
/*
|
|
* _CPPopoverWindow.j
|
|
* AppKit
|
|
*
|
|
* Created by Antoine Mercadal
|
|
* Copyright 2011 <primalmotion@archipelproject.org>
|
|
*
|
|
* 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.1 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; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
*/
|
|
|
|
@import <Foundation/CPObject.j>
|
|
|
|
@import "CPButton.j"
|
|
@import "CPMenu.j"
|
|
@import "CPPanel.j"
|
|
|
|
// Use forward declaration because this file is imported by CPPopover
|
|
@class CPPopover
|
|
|
|
@global CPApp
|
|
|
|
CPClosableOnBlurWindowMask = 1 << 4;
|
|
CPPopoverAppearanceMinimal = 0;
|
|
CPPopoverAppearanceHUD = 1;
|
|
|
|
var _CPPopoverWindow_shouldClose_ = 1 << 0,
|
|
_CPPopoverWindow_didClose_ = 1 << 1,
|
|
_CPPopoverWindow_didShow_ = 1 << 2;
|
|
|
|
|
|
/*!
|
|
@ignore
|
|
|
|
This is a simple popover window like the one that pops up
|
|
when you double click on a meeting in iCal.
|
|
*/
|
|
@implementation _CPPopoverWindow : CPPanel
|
|
{
|
|
BOOL _animates @accessors(property=animates);
|
|
id _targetView @accessors(property=targetView);
|
|
int _appearance @accessors(getter=appearance);
|
|
BOOL _isClosing @accessors(property=isClosing);
|
|
|
|
BOOL _closeOnBlur;
|
|
BOOL _browserAnimates;
|
|
BOOL _shouldPerformAnimation;
|
|
CPInteger _implementedDelegateMethods;
|
|
JSObject _orderOutTransitionFunction;
|
|
}
|
|
|
|
|
|
#pragma mark -
|
|
#pragma mark Class methods
|
|
|
|
/*!
|
|
Overrides the default windowView class loader.
|
|
|
|
@param aStyleMask the window mask
|
|
@return the windowView class
|
|
*/
|
|
+ (Class)_windowViewClassForStyleMask:(unsigned)aStyleMask
|
|
{
|
|
return _CPPopoverWindowView;
|
|
}
|
|
|
|
|
|
#pragma mark -
|
|
#pragma mark Initialization
|
|
|
|
/*!
|
|
Create and init a _CPPopoverWindow with given the given frame.
|
|
|
|
@param aFrame the frame of the popover window
|
|
@return ready to use _CPPopoverWindow
|
|
*/
|
|
- (id)initWithContentRect:(CGRect)aFrame
|
|
{
|
|
return [self initWithContentRect:aFrame styleMask:0];
|
|
}
|
|
|
|
/*!
|
|
Designated initializer. Create and init a _CPPopoverWindow with the given frame and style mask.
|
|
|
|
@param aFrame the frame of the popover window
|
|
@param styleMask the window style mask (combine CPClosableWindowMask and CPClosableOnBlurWindowMask)
|
|
@return ready to use _CPPopoverWindow
|
|
*/
|
|
- (id)initWithContentRect:(CGRect)aFrame styleMask:(unsigned)aStyleMask
|
|
{
|
|
if (self = [super initWithContentRect:aFrame styleMask:aStyleMask])
|
|
{
|
|
_animates = YES;
|
|
_isClosing = NO;
|
|
_browserAnimates = [self browserSupportsAnimation];
|
|
_shouldPerformAnimation = YES;
|
|
_orderOutTransitionFunction = function() { [self _orderOutRecursively:YES]; };
|
|
|
|
|
|
[self setStyleMask:aStyleMask];
|
|
[self setBecomesKeyOnlyIfNeeded:YES];
|
|
[self setMovableByWindowBackground:YES];
|
|
[self setHasShadow:NO];
|
|
|
|
[self setCSS3Property:@"TransitionProperty" value:CPBrowserCSSProperty('transform') + @", opacity"];
|
|
|
|
[_windowView setNeedsDisplay:YES];
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Getters / Setters
|
|
|
|
- (void)setAppearance:(int)anAppearance
|
|
{
|
|
if (_appearance === anAppearance)
|
|
return;
|
|
|
|
[_windowView setAppearance:anAppearance];
|
|
}
|
|
|
|
- (void)setStyleMask:(unsigned)aStyleMask
|
|
{
|
|
_closeOnBlur = (aStyleMask & CPClosableOnBlurWindowMask);
|
|
}
|
|
|
|
- (void)setDelegate:(id)aDelegate
|
|
{
|
|
if (_delegate === aDelegate)
|
|
return;
|
|
|
|
_delegate = aDelegate;
|
|
_implementedDelegateMethods = 0;
|
|
|
|
if ([_delegate respondsToSelector:@selector(popoverWindowShouldClose:)])
|
|
_implementedDelegateMethods |= _CPPopoverWindow_shouldClose_;
|
|
|
|
if ([_delegate respondsToSelector:@selector(popoverWindowDidClose:)])
|
|
_implementedDelegateMethods |= _CPPopoverWindow_didClose_;
|
|
|
|
if ([_delegate respondsToSelector:@selector(popoverWindowDidShow:)])
|
|
_implementedDelegateMethods |= _CPPopoverWindow_didShow_;
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Observer
|
|
|
|
/*!
|
|
Update the _CPPopoverWindow frame if a resize event is observed.
|
|
*/
|
|
- (void)observeValueForKeyPath:(CPString)aPath ofObject:(id)anObject change:(CPDictionary)theChange context:(void)aContext
|
|
{
|
|
if (aPath === @"frame")
|
|
{
|
|
if (![_targetView window])
|
|
return;
|
|
|
|
var point = [self computeOriginFromRect:[_targetView bounds] ofView:_targetView preferredEdge:[_windowView preferredEdge]];
|
|
|
|
[self setFrameOrigin:point];
|
|
}
|
|
}
|
|
|
|
|
|
#pragma mark -
|
|
#pragma mark Utilities
|
|
|
|
- (CGPoint)computeOriginFromRect:(CGRect)aRect ofView:(CPView)positioningView preferredEdge:(int)anEdge
|
|
{
|
|
var mainWindow = [positioningView window],
|
|
platformWindow = [mainWindow platformWindow],
|
|
nativeRect = [platformWindow nativeContentRect],
|
|
baseOrigin = [positioningView convertPointToBase:aRect.origin],
|
|
platformOrigin = [mainWindow convertBaseToPlatformWindow:baseOrigin],
|
|
platformRect = _CGRectMake(platformOrigin.x, platformOrigin.y, aRect.size.width, aRect.size.height),
|
|
originLeft = _CGPointCreateCopy(platformOrigin),
|
|
originRight = _CGPointCreateCopy(platformOrigin),
|
|
originTop = _CGPointCreateCopy(platformOrigin),
|
|
originBottom = _CGPointCreateCopy(platformOrigin),
|
|
frameSize = [self frame].size,
|
|
menuBarHeight = [CPMenu menuBarVisible] ? [CPMenu menuBarHeight] : 0;
|
|
|
|
// CPMaxXEdge
|
|
originRight.x += platformRect.size.width;
|
|
originRight.y += (platformRect.size.height / 2.0) - (frameSize.height / 2.0);
|
|
|
|
// CPMinXEdge
|
|
originLeft.x -= frameSize.width;
|
|
originLeft.y += (platformRect.size.height / 2.0) - (frameSize.height / 2.0);
|
|
|
|
// CPMaxYEdge
|
|
originBottom.x += platformRect.size.width / 2.0 - frameSize.width / 2.0;
|
|
originBottom.y += platformRect.size.height;
|
|
|
|
// CPMinYEdge
|
|
originTop.x += platformRect.size.width / 2.0 - frameSize.width / 2.0;
|
|
originTop.y -= frameSize.height;
|
|
|
|
var requestedEdge = (anEdge !== nil) ? anEdge : CPMaxXEdge,
|
|
requestedOrigin;
|
|
|
|
switch (requestedEdge)
|
|
{
|
|
case CPMaxXEdge:
|
|
requestedOrigin = originRight;
|
|
break;
|
|
|
|
case CPMinXEdge:
|
|
requestedOrigin = originLeft;
|
|
break;
|
|
|
|
case CPMinYEdge:
|
|
requestedOrigin = originTop;
|
|
break;
|
|
|
|
case CPMaxYEdge:
|
|
requestedOrigin = originBottom;
|
|
break;
|
|
}
|
|
|
|
var origins = [requestedOrigin, originRight, originLeft, originTop, originBottom],
|
|
edges = [requestedEdge, CPMaxXEdge, CPMinXEdge, CPMinYEdge, CPMaxYEdge];
|
|
|
|
for (var i = 0; i < origins.length; i++)
|
|
{
|
|
var origin = origins[i],
|
|
edge = edges[i];
|
|
|
|
[_windowView setArrowOffsetX:0];
|
|
[_windowView setArrowOffsetY:0];
|
|
[_windowView setPreferredEdge:edge];
|
|
|
|
if (origin.x < 0)
|
|
{
|
|
[_windowView setArrowOffsetX:origin.x];
|
|
origin.x = 0;
|
|
}
|
|
|
|
if (origin.x + frameSize.width > nativeRect.size.width)
|
|
{
|
|
[_windowView setArrowOffsetX:(origin.x + frameSize.width - nativeRect.size.width)];
|
|
origin.x = nativeRect.size.width - frameSize.width;
|
|
}
|
|
|
|
if (origin.y < 0)
|
|
{
|
|
[_windowView setArrowOffsetY:origin.y - menuBarHeight];
|
|
origin.y = menuBarHeight;
|
|
}
|
|
|
|
if (origin.y + frameSize.height > nativeRect.size.height)
|
|
{
|
|
[_windowView setArrowOffsetY:(frameSize.height + origin.y - nativeRect.size.height)];
|
|
origin.y = nativeRect.size.height - frameSize.height;
|
|
}
|
|
|
|
switch (edge)
|
|
{
|
|
case CPMaxXEdge:
|
|
if (origin.x >= _CGRectGetMaxX(platformRect))
|
|
return origin;
|
|
break;
|
|
|
|
case CPMinXEdge:
|
|
if ((origin.x + frameSize.width) <= platformRect.origin.x)
|
|
return origin;
|
|
break;
|
|
|
|
case CPMaxYEdge:
|
|
if (origin.y >= _CGRectGetMaxY(platformRect))
|
|
return origin;
|
|
break;
|
|
|
|
case CPMinYEdge:
|
|
if ((origin.y + frameSize.height) <= platformRect.origin.y)
|
|
return origin;
|
|
break;
|
|
}
|
|
}
|
|
|
|
[_windowView setPreferredEdge:nil];
|
|
return requestedOrigin;
|
|
}
|
|
|
|
/*!
|
|
Compute the frame needed to be placed to the given view
|
|
and position the popover window according to this view (edge will be automatic)
|
|
|
|
@param aView the view where _CPPopoverWindow must be popover
|
|
*/
|
|
- (void)positionRelativeToView:(CPView)aView
|
|
{
|
|
[self positionRelativeToRect:nil ofView:aView preferredEdge:nil];
|
|
}
|
|
|
|
/*!
|
|
Position the _CPPopoverWindow relative to a given rect's edge.
|
|
|
|
@param aRect the rect relative to which the popover window will be positioned
|
|
@param positioningView the view to which the popover window is popover
|
|
@param anEdge the prefered edge
|
|
*/
|
|
- (void)positionRelativeToRect:(CGRect)aRect ofView:(CPView)positioningView preferredEdge:(int)anEdge
|
|
{
|
|
var wasVisible = [self isVisible];
|
|
|
|
if (!aRect || _CGRectIsEmpty(aRect))
|
|
aRect = [positioningView bounds];
|
|
|
|
var point = [self computeOriginFromRect:aRect ofView:positioningView preferredEdge:anEdge];
|
|
|
|
[self setFrameOrigin:point];
|
|
[_windowView showCursor];
|
|
[_windowView setNeedsDisplay:YES];
|
|
[self makeKeyAndOrderFront:nil];
|
|
|
|
if (positioningView !== _targetView)
|
|
{
|
|
[[_targetView window] removeChildWindow:self];
|
|
[_targetView removeObserver:self forKeyPath:@"frame"];
|
|
_targetView = positioningView;
|
|
}
|
|
|
|
/*
|
|
If _targetView's window is not a full platform window,
|
|
add us as a child, because when we close we are detached from
|
|
the parent, and the parent may cache this window and reopen it.
|
|
*/
|
|
var targetWindow = [_targetView window];
|
|
|
|
if (![targetWindow isFullPlatformWindow])
|
|
[[_targetView window] addChildWindow:self ordered:CPWindowAbove];
|
|
|
|
if (!wasVisible)
|
|
[self _trapNextMouseDown];
|
|
}
|
|
|
|
/*! @ignore */
|
|
- (void)setCSS3Property:(CPString)aProperty value:(CPString)value
|
|
{
|
|
var browserProperty = CPBrowserStyleProperty(aProperty);
|
|
|
|
#if PLATFORM(DOM)
|
|
if (browserProperty)
|
|
_DOMElement.style[browserProperty] = value;
|
|
#endif
|
|
}
|
|
|
|
/*! @ignore */
|
|
- (BOOL)browserSupportsAnimation
|
|
{
|
|
return CPBrowserStyleProperty('transition') && CPBrowserStyleProperty('transitionend');
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Actions
|
|
|
|
/*!
|
|
@ignore
|
|
Closes the _CPPopoverWindow
|
|
|
|
@param sender the sender of the action
|
|
*/
|
|
- (IBAction)close:(id)aSender
|
|
{
|
|
[self close];
|
|
}
|
|
|
|
|
|
#pragma mark -
|
|
#pragma mark Overrides
|
|
|
|
/*!
|
|
@ignore
|
|
Show animation if necessary.
|
|
*/
|
|
- (void)close
|
|
{
|
|
[self orderOut:self];
|
|
[self _detachFromChildrenClosing:YES];
|
|
}
|
|
|
|
/*!
|
|
@ignore
|
|
When the window appears, show animation if necessary.
|
|
Also take this opportunity to keep track of window moves.
|
|
|
|
@param sender the sender of the action
|
|
*/
|
|
- (IBAction)orderFront:(is)aSender
|
|
{
|
|
if (![self isKeyWindow])
|
|
{
|
|
[super orderFront:aSender];
|
|
|
|
if (_animates && _browserAnimates && _shouldPerformAnimation)
|
|
{
|
|
var transformOrigin = "50% 100%",
|
|
frame = [self frame],
|
|
preferredEdge = [_windowView preferredEdge],
|
|
posX, posY;
|
|
|
|
switch (preferredEdge)
|
|
{
|
|
case CPMaxYEdge:
|
|
case CPMinYEdge:
|
|
posX = 50 + (([_windowView arrowOffsetX] * 100) / frame.size.width);
|
|
transformOrigin = posX + "% " + (preferredEdge === CPMaxYEdge ? "0%" : "100%");
|
|
break;
|
|
|
|
case CPMinXEdge:
|
|
case CPMaxXEdge:
|
|
posY = 50 + (([_windowView arrowOffsetY] * 100) / frame.size.height);
|
|
transformOrigin = (preferredEdge === CPMaxXEdge ? "0% " : "100% ") + posY + "%";
|
|
break;
|
|
}
|
|
|
|
// This is the initial transform. We start scaled to zero and watch for opacity changes.
|
|
[self setCSS3Property:@"Transform" value:@"scale(0)"];
|
|
[self setCSS3Property:@"TransformOrigin" value:transformOrigin];
|
|
[self setCSS3Property:@"Transition" value:"opacity 0 linear"];
|
|
|
|
window.setTimeout(function()
|
|
{
|
|
// We are watching opacity, so this triggers the next transition
|
|
#if PLATFORM(DOM)
|
|
_DOMElement.style.opacity = 1;
|
|
_DOMElement.style.height = frame.size.height + @"px";
|
|
_DOMElement.style.width = frame.size.width + @"px";
|
|
#endif
|
|
|
|
// Set up the pop-out transition
|
|
[self setCSS3Property:@"Transform" value:@"scale(1.1)"];
|
|
[self setCSS3Property:@"Transition" value:CPBrowserCSSProperty('transform') + @" 200ms ease-in"];
|
|
|
|
var orderFrontTransitionFunction = function()
|
|
{
|
|
#if PLATFORM(DOM)
|
|
_DOMElement.removeEventListener(CPBrowserStyleProperty('transitionend'), orderFrontTransitionFunction, YES);
|
|
#endif
|
|
|
|
// Now set up the pop-in to normal size transition.
|
|
// Because we are watching the -webkit-transform, it will occur now.
|
|
[self setCSS3Property:@"Transform" value:@"scale(1)"];
|
|
[self setCSS3Property:@"Transition" value:CPBrowserCSSProperty('transform') + @" 50ms linear"];
|
|
|
|
var transitionCompleteFunction = function()
|
|
{
|
|
#if PLATFORM(DOM)
|
|
_DOMElement.removeEventListener(CPBrowserStyleProperty('transitionend'), transitionCompleteFunction, YES);
|
|
|
|
// Make sure to clear these properties when the animation is done. Without this,
|
|
// the window becomes blurry in Chrome, presumably because the browser composits
|
|
// a layer with a transform differently even when it's an identity transform.
|
|
[self setCSS3Property:@"Transform" value:nil];
|
|
[self setCSS3Property:@"TransformOrigin" value:nil];
|
|
[self setCSS3Property:@"Transition" value:nil];
|
|
#endif
|
|
if (_implementedDelegateMethods & _CPPopoverWindow_didShow_)
|
|
[_delegate popoverWindowDidShow:self];
|
|
}
|
|
|
|
#if PLATFORM(DOM)
|
|
_DOMElement.addEventListener(CPBrowserStyleProperty('transitionend'), transitionCompleteFunction, YES);
|
|
#endif
|
|
};
|
|
|
|
#if PLATFORM(DOM)
|
|
_DOMElement.addEventListener(CPBrowserStyleProperty('transitionend'), orderFrontTransitionFunction, YES);
|
|
#endif
|
|
}, 0);
|
|
}
|
|
else
|
|
{
|
|
[self setCSS3Property:@"Transition" value:@""];
|
|
#if PLATFORM(DOM)
|
|
_DOMElement.style.opacity = 1;
|
|
#endif
|
|
}
|
|
}
|
|
|
|
_shouldPerformAnimation = NO;
|
|
_isClosing = NO;
|
|
}
|
|
|
|
- (void)_orderFront
|
|
{
|
|
if (![self isVisible])
|
|
[_targetView addObserver:self forKeyPath:@"frame" options:0 context:nil];
|
|
|
|
[super _orderFront];
|
|
}
|
|
|
|
- (void)_parentDidOrderInChild
|
|
{
|
|
[_targetView addObserver:self forKeyPath:@"frame" options:0 context:nil];
|
|
}
|
|
|
|
/*!
|
|
@ignore
|
|
Animate window closing.
|
|
*/
|
|
- (void)orderOut:(id)aSender
|
|
{
|
|
if (![self isVisible])
|
|
return;
|
|
|
|
_isClosing = YES;
|
|
|
|
if (_animates && _browserAnimates)
|
|
{
|
|
// Tell the element to fade out when the opacity changes
|
|
[self setCSS3Property:@"Transition" value:@"opacity 250ms linear"];
|
|
#if PLATFORM(DOM)
|
|
_DOMElement.style.opacity = 0;
|
|
_DOMElement.addEventListener(CPBrowserStyleProperty("transitionend"), _orderOutTransitionFunction, YES);
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
[self _orderOutRecursively:YES];
|
|
}
|
|
}
|
|
|
|
- (void)_orderOutRecursively:(BOOL)recursive
|
|
{
|
|
// Make absolutely sure no dangling event listeners are left
|
|
#if PLATFORM(DOM)
|
|
_DOMElement.removeEventListener(CPBrowserStyleProperty("transitionend"), _orderOutTransitionFunction, YES);
|
|
#endif
|
|
|
|
[_targetView removeObserver:self forKeyPath:@"frame"];
|
|
[super _orderOutRecursively:recursive];
|
|
|
|
_shouldPerformAnimation = YES;
|
|
_isClosing = NO;
|
|
|
|
if (_implementedDelegateMethods & _CPPopoverWindow_didClose_)
|
|
[_delegate popoverWindowDidClose:self];
|
|
}
|
|
|
|
|
|
#pragma mark -
|
|
#pragma mark Private
|
|
|
|
- (void)_mouseWasClicked:(CPEvent)anEvent
|
|
{
|
|
/*
|
|
If the mouse was clicked inside us, trap the next mouse down.
|
|
If the mouse was clicked outside of us, we close and send this
|
|
message to any parent popovers so they have a chance to close
|
|
if necessary.
|
|
*/
|
|
if (![self isVisible])
|
|
return;
|
|
|
|
var mouseWindow = [anEvent window];
|
|
|
|
if (mouseWindow === self)
|
|
[self _trapNextMouseDown];
|
|
else
|
|
{
|
|
// Send _close to the delegate so popoverWillClose is sent to the popover's delegate
|
|
if (_closeOnBlur)
|
|
[_delegate _close];
|
|
|
|
// Give a transient parent popover a chance to close
|
|
var parent = [self parentWindow];
|
|
|
|
if ([parent isKindOfClass:[self class]])
|
|
[parent _mouseWasClicked:anEvent];
|
|
}
|
|
}
|
|
|
|
- (void)_trapNextMouseDown
|
|
{
|
|
// Don't dequeue the event so clicks in controls will work
|
|
[CPApp setTarget:self selector:@selector(_mouseWasClicked:) forNextEventMatchingMask:CPLeftMouseDownMask untilDate:nil inMode:CPDefaultRunLoopMode dequeue:NO];
|
|
}
|
|
|
|
@end
|