aboutsummaryrefslogtreecommitdiffstats
path: root/Source/SPComboPopupButton.m
diff options
context:
space:
mode:
Diffstat (limited to 'Source/SPComboPopupButton.m')
-rw-r--r--Source/SPComboPopupButton.m312
1 files changed, 312 insertions, 0 deletions
diff --git a/Source/SPComboPopupButton.m b/Source/SPComboPopupButton.m
new file mode 100644
index 00000000..9ee71084
--- /dev/null
+++ b/Source/SPComboPopupButton.m
@@ -0,0 +1,312 @@
+//
+// $Id$
+//
+// SPComboPopupButton.m
+// sequel-pro
+//
+// Created by Rowan Beentje (rowan.beent.je) on March 22, 2013
+// Copyright (c) 2013 Rowan Beentje. All rights reserved.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+// More info at <http://code.google.com/p/sequel-pro/>
+
+#import "SPComboPopupButton.h"
+
+#define kSPComboPopupButtonLineOffsetMini 13;
+#define kSPComboPopupButtonLineOffsetSmall 15;
+#define kSPComboPopupButtonLineOffsetRegular 17;
+
+@interface SPComboPopupButton (PrivateAPI)
+
+- (void)_initCustomData;
+
+@end
+
+@implementation SPComboPopupButton
+
+@synthesize shouldDrawNonHighlightState;
+@synthesize lineOffset;
+
+#pragma mark -
+#pragma mark Setup
+
+- (id)initWithCoder:(NSCoder *)decoder
+{
+ if ((self = [super initWithCoder:decoder])) {
+ [self _initCustomData];
+ }
+ return self;
+}
+
+- (id)initWithFrame:(NSRect)frameRect pullsDown:(BOOL)flag
+{
+ if ((self = [super initWithFrame:frameRect pullsDown:flag])) {
+ [self _initCustomData];
+ }
+ return self;
+}
+
+/**
+ * Default to the overridden class. Note that this won't apply to instanced
+ * created in a xib, where the cell class should be selected appropriately.
+ */
++ (Class)cellClass
+{
+ return [SPComboPopupButtonCell class];
+}
+
+#pragma mark -
+#pragma mark Drawing
+
+/**
+ * Draw the control, largely leveraging NSPopupButton drawing but with tweaks
+ * to draw a separator line and different highlights if the dropdown area is
+ * selected.
+ */
+- (void)drawRect:(NSRect)dirtyRect
+{
+ NSRect boundsRect = [self bounds];
+ CGFloat boundingLinePosition = boundsRect.origin.x + boundsRect.size.width - lineOffset - 0.5;
+ CGContextRef context = (CGContextRef)[[NSGraphicsContext currentContext] graphicsPort];
+ CGFloat heightIndent = ([self isFlipped] ? 4.f : -4.f);
+
+ // Allow the NSPopupButton to draw the majority of the button, with one exception:
+ // if the menu is open, only draw part of the rectangle highlighted.
+ if (menuIsOpen) {
+
+ // Draw the unhighlighted button state in the left-hand part of the button
+ NSRect partialDirtyRect = NSIntersectionRect(dirtyRect, NSMakeRect(boundsRect.origin.x, boundsRect.origin.y, boundingLinePosition - boundsRect.origin.x, boundsRect.size.height));
+ if (!NSIsEmptyRect(partialDirtyRect)) {
+ CGContextSaveGState(context);
+ CGContextClipToRect(context, NSRectToCGRect(partialDirtyRect));
+ shouldDrawNonHighlightState = YES;
+ [super drawRect:partialDirtyRect];
+ shouldDrawNonHighlightState = NO;
+ CGContextRestoreGState(context);
+ }
+
+ // Draw the right-hand side of the button as normal
+ partialDirtyRect = NSIntersectionRect(dirtyRect, NSMakeRect(boundingLinePosition - 0.5, boundsRect.origin.y, boundsRect.origin.x + boundsRect.size.width + 0.5 - boundingLinePosition, boundsRect.size.height));
+ if (!NSIsEmptyRect(partialDirtyRect)) {
+ CGContextSaveGState(context);
+ CGContextClipToRect(context, NSRectToCGRect(partialDirtyRect));
+ [super drawRect:dirtyRect];
+ CGContextRestoreGState(context);
+ }
+ } else {
+ [super drawRect:dirtyRect];
+ }
+
+ // Draw the divider line for the two parts of the button
+ NSColor *lineBaseColor = [[NSColor lightGrayColor] colorUsingColorSpace:[NSColorSpace deviceRGBColorSpace]];
+ CGFloat lineColorParts[[lineBaseColor numberOfComponents]];
+ CGColorSpaceRef rgbSpace = CGColorSpaceCreateDeviceRGB();
+ [lineBaseColor getComponents:(CGFloat *)&lineColorParts];
+ CGColorRef lineColor = CGColorCreate(rgbSpace, lineColorParts);
+ CGColorRef lineEdgeColor = CGColorCreateCopyWithAlpha(lineColor, 0.1);
+ CGColorRef gradientColors[] = { lineEdgeColor, lineColor, lineColor, lineEdgeColor };
+ CFArrayRef colorArray = CFArrayCreate(NULL, (const void **)gradientColors, 4, &kCFTypeArrayCallBacks);
+ CGFloat gradientPositions[] = { 0.05, 0.25, 0.75, 0.95 };
+ CGGradientRef lineGradient = CGGradientCreateWithColors(rgbSpace, colorArray, gradientPositions);
+
+ CGContextSaveGState(context);
+ CGContextSetStrokeColor(context, lineColorParts);
+ CGContextAddRect(context, CGRectMake(boundingLinePosition - 0.5, boundsRect.origin.y + heightIndent, 1.f, boundsRect.size.height - abs(2 * heightIndent)));
+ CGContextClip(context);
+ CGContextDrawLinearGradient(context, lineGradient, CGPointMake(boundingLinePosition - 0.5, boundsRect.origin.y + heightIndent), CGPointMake(boundingLinePosition - 0.5, boundsRect.origin.y + boundsRect.size.height - abs(heightIndent)), 0);
+ CGContextRestoreGState(context);
+
+ CGGradientRelease(lineGradient);
+ CFRelease(colorArray);
+ CGColorRelease(lineEdgeColor);
+ CGColorRelease(lineColor);
+ CGColorSpaceRelease(rgbSpace);
+}
+
+#pragma mark -
+#pragma mark Click action overrides
+
+- (void)performClick:(id)sender
+{
+ if (actionSelector && actionTarget) {
+ [self sendAction:actionSelector to:actionTarget];
+ }
+}
+
+- (id)target
+{
+ return actionTarget;
+}
+
+- (void)setTarget:(id)anObject
+{
+ actionTarget = anObject;
+}
+
+- (SEL)action
+{
+ return actionSelector;
+}
+
+- (void)setAction:(SEL)aSelector
+{
+ actionSelector = aSelector;
+}
+
+#pragma mark -
+#pragma mark Menu delegate implementation
+
+- (void)menuWillOpen:(NSMenu *)menu
+{
+ menuIsOpen = YES;
+}
+
+- (void)menuDidClose:(NSMenu *)menu
+{
+ menuIsOpen = NO;
+}
+
+@end
+
+#pragma mark -
+
+@implementation SPComboPopupButton (PrivateAPI)
+
+- (void)_initCustomData
+{
+
+ // Set the line position based on the initial control size
+ switch ([[self cell] controlSize]) {
+ case NSMiniControlSize:
+ lineOffset = kSPComboPopupButtonLineOffsetMini;
+ break;
+ case NSSmallControlSize:
+ lineOffset = kSPComboPopupButtonLineOffsetSmall;
+ break;
+ default:
+ lineOffset = kSPComboPopupButtonLineOffsetRegular;
+ break;
+ }
+
+ // Track when the menu is open via delegate methods
+ menuIsOpen = NO;
+ [[[self cell] menu] setDelegate:self];
+
+ // Move any xib-specified action and target for use as the button target
+ actionSelector = [super action];
+ [super setAction:NULL];
+ actionTarget = [super target];
+ [super setTarget:nil];
+}
+
+@end
+
+#pragma mark -
+
+@interface SPComboPopupButtonCell (PrivateAPI)
+
+- (void)_initCustomData;
+
+@end
+
+@implementation SPComboPopupButtonCell
+
+/**
+ * Indent the title slightly to take account of the additional divider
+ */
+- (NSRect)drawTitle:(NSAttributedString *)title withFrame:(NSRect)frame inView:(NSView *)controlView
+{
+ frame.size.width -= 1;
+ return [super drawTitle:title withFrame:frame inView:controlView];
+}
+
+/**
+ * Allow the button to overwrite the draw status as required
+ */
+- (BOOL)isHighlighted
+{
+ if ([(SPComboPopupButton *)[self controlView] shouldDrawNonHighlightState]) {
+ return NO;
+ }
+ return [super isHighlighted];
+}
+
+#pragma mark -
+#pragma mark Custom interaction handling
+
+- (BOOL)trackMouse:(NSEvent *)theEvent inRect:(NSRect)cellFrame ofView:(NSView *)controlView untilMouseUp:(BOOL)untilMouseUp
+{
+ NSPoint thePoint;
+ NSRect activeRect;
+ CGFloat heightIndent = ([controlView isFlipped] ? 2.f : -2.f);
+ BOOL mouseInButton = YES;
+ BOOL trackAsPerMenuButton = NO;
+
+ // If the event isn't a mouse button event, allow the NSPopUpButtonCell to handle it
+ if ([theEvent type] != NSLeftMouseDown) {
+ trackAsPerMenuButton = YES;
+ }
+
+ // If the view doesn't support line position checks, pass on the event
+ else if (![controlView respondsToSelector:@selector(lineOffset)]) {
+ trackAsPerMenuButton = YES;
+ }
+
+ // If the click is to the right of the line, show the menu
+ else if ([controlView convertPoint:theEvent.locationInWindow fromView:nil].x + [(SPComboPopupButton *)controlView lineOffset] >= [controlView frame].size.width) {
+ trackAsPerMenuButton = YES;
+ }
+
+ if (trackAsPerMenuButton) {
+ return [super trackMouse:theEvent inRect:cellFrame ofView:controlView untilMouseUp:untilMouseUp];
+ }
+
+
+ // Custom tracking to be performed - indent the vertical button area slightly
+ activeRect = NSMakeRect(cellFrame.origin.x, cellFrame.origin.y + heightIndent, cellFrame.size.width - [(SPComboPopupButton *)controlView lineOffset] + 1, cellFrame.size.height - fabsf(2 * heightIndent));
+
+ // Continue tracking the mouse while it's down, updating the state as it enters and leaves the cell,
+ // until it is released; if still within the cell, perform a click.
+ while ([theEvent type] != NSLeftMouseUp) {
+ thePoint = [controlView convertPoint:[theEvent locationInWindow] fromView:nil];
+
+ if (NSMouseInRect(thePoint, activeRect, [controlView isFlipped]) != mouseInButton) {
+ mouseInButton = !mouseInButton;
+ [self setHighlighted:mouseInButton];
+ }
+
+ theEvent = [[controlView window] nextEventMatchingMask:(NSLeftMouseUpMask | NSLeftMouseDraggedMask)];
+ }
+
+ // If the mouse is still inside the button area, perform a click action and restore state
+ if (mouseInButton) {
+ if ([controlView respondsToSelector:@selector(performClick:)]) {
+ [(NSControl *)controlView performClick:self];
+ }
+ [self setHighlighted:NO];
+ }
+
+ return YES;
+}
+
+@end