Custom NSSliderCell knob doesn't follow mouse when tracking - cocoa

I've implemented a custom NSSliderCell that uses a very different knob in size than the default one (this is for an interactive exhibit - I cannot use any default Mac OS X controls).
While the slider appears and behaves correctly (the knob goes from end to end, etc), when you look carefully you see a weird behaviour: Moving the mouse say, 20 pixels, will result in the knob to move by 30 pixels. This means that the knob might reach the end of the slider (and the slider will have the maximum value) before the mouse will reach the end.
This looks very weird and goes against all expectations. I wonder what do I have to change to ensure that the knob follows the mouse and doesn't move faster.

OK, as always, the solution was the simplest one.
Here is the simplest code you need to have a very custom slider:
#import "CSSSliderCell.h"
#define KNOB_WIDTH 20
#define KNOB_HEIGHT 126
#define SLIDER_WIDTH 13
#implementation CSSSliderCell
- (void)drawKnob:(NSRect)rect
{
// knobImage is an NSImage
[knobImage drawInRect:rect fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0];
}
- (void)drawBarInside:(NSRect)cellFrame flipped:(BOOL)flipped
{
NSRect slideRect = cellFrame;
NSColor *backColor = [NSColor redColor];
if ([(NSSlider*) [self controlView] isVertical] == YES)
{
slideRect.size.width = SLIDER_WIDTH;
slideRect.origin.x += (cellFrame.size.width - SLIDER_WIDTH) * 0.5;
} else {
slideRect.size.height = SLIDER_WIDTH;
slideRect.origin.y += (cellFrame.size.height - SLIDER_WIDTH) * 0.5;
}
NSBezierPath *bezierPath = [NSBezierPath bezierPathWithRoundedRect:slideRect xRadius:SLIDER_WIDTH * 0.5 yRadius:SLIDER_WIDTH * 0.5];
[backColor setFill];
[bezierPath fill];
}
- (NSRect)knobRectFlipped:(BOOL)flipped{
CGFloat value = ([self doubleValue] - [self minValue])/ ([self maxValue] - [self minValue]);
NSRect defaultRect = [super knobRectFlipped:flipped];
NSRect myRect = NSMakeRect(0, 0, 0, 0);
if ([(NSSlider*) [self controlView] isVertical] == YES)
{
myRect.size.width = KNOB_WIDTH;
myRect.size.height = KNOB_HEIGHT;
if (!flipped) {
myRect.origin.y = value * ([[self controlView] frame].size.height - KNOB_HEIGHT);
} else {
myRect.origin.y = (1.0 - value) * ([[self controlView] frame].size.height - KNOB_HEIGHT);
}
myRect.origin.x = defaultRect.origin.x;
} else {
myRect.size.width = KNOB_HEIGHT;
myRect.size.height = KNOB_WIDTH;
myRect.origin.x = value * ([[self controlView] frame].size.width - KNOB_HEIGHT);
myRect.origin.y = defaultRect.origin.y;
}
return myRect;
}
- (BOOL)_usesCustomTrackImage
{
return YES;
}
#end
This might have problems, but so far both on horizontal and vertical orientations it works well.

orestis, your code helps me much. Thank you!
However, the knob's position (myRect.origin) is not correct. I modified the code (and also removed the hardcoded macros).
- (NSRect)knobRectFlipped:(BOOL)flipped {
float KNOB_WIDTH = [knobImage size].width;
float KNOB_HEIGHT = [knobImage size].height;
CGFloat value = ([self doubleValue] - [self minValue])/ ([self maxValue] - [self minValue]);
NSRect defaultRect = [super knobRectFlipped:flipped];
NSRect myRect = NSMakeRect(0, 0, 0, 0);
if ([(NSSlider*) [self controlView] isVertical] == YES)
{
//...
} else {
myRect.size.width = KNOB_WIDTH;
myRect.size.height = KNOB_HEIGHT;
myRect.origin.x = value * ([[self controlView] frame].size.width - KNOB_WIDTH);
myRect.origin.y = defaultRect.origin.y + defaultRect.size.height/2.0 - myRect.size.height/2.0; // Fixed the position
}
return myRect;
};

Related

NSView in cocoa refuses to redraw, whatever I try

I'm writing an application on mac to measure colors on screen. For that I have a ColorView that fills a bezierpath with a specific color. When measuring it goes well for most of the patches, but at a given moment the redraw staggers and the view does not get updated anymore. I have been looking for a while now, tried many proposed solutions. None of them are waterproof.
My actual code:
[colorView setColor:[colorsForMeasuring objectAtIndex:index]];
[colorView setNeedsDisplay:YES];
[colorView setNeedsLayout:YES];
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_async(mainQueue,^{
[colorView setNeedsDisplay:YES];
[colorView setNeedsLayout:YES];
[colorView updateLayer];
[colorView displayIfNeeded];
[colorView display];
});
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
[NSApp runModalSession:aSession];
The drawcode of my colorView is as follows:
- (void)display
{
CALayer *layer = self.layer;
[layer setNeedsDisplay];
[layer displayIfNeeded];
}
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)context
{
[self internalDrawWithRect:self.bounds];
}
- (void)internalDrawWithRect:(CGRect)rect
{
NSRect bounds = [self bounds];
[color set];
[NSBezierPath fillRect:bounds];
}
- (void)drawRect:(CGRect)rect {
[self internalDrawWithRect:rect];
}
Any help would be very welcome! My original code was much simpler, but I kept on adding things that might have helped.
As I said, the original code was rather straightfoward and simple:
The measuring loop:
for ( uint32 index = 0; index < numberOfColors && !stopMeasuring; index++ )
{
#ifndef NDEBUG
NSLog( #"Color on screen:%#", [colorsForMeasuring objectAtIndex:index] );
#endif
[colorView setColor:[colorsForMeasuring objectAtIndex:index]];
[colorView setNeedsDisplay:YES];
[colorView displayIfNeeded];
// Measure the displayed color in XYZ values
CGFloat myRed, myGreen, myBlue, myAlpha;
[[colorsForMeasuring objectAtIndex:index] getRed:&myRed green:&myGreen blue:&myBlue alpha:&myAlpha];
float theR = (float) myRed;
float theG = (float) myGreen;
float theB = (float) myBlue;
xyzData = [measDeviceController measureOnceXYZforRed:255.0f*theR Green:255.0f*theG Blue:255.0f*theB];
if ( [xyzData count] > 0 )
{
measuringResults[index*3] = [[xyzData objectAtIndex:0] floatValue];
measuringResults[(index*3) + 1] = [[xyzData objectAtIndex:1] floatValue];
measuringResults[(index*3) + 2] = [[xyzData objectAtIndex:2] floatValue];
#ifndef NDEBUG
printf("Measured value X=%f, Y=%f, Z=%f\n", measuringResults[index*3], measuringResults[(index*3) + 1], measuringResults[(index*3) + 2]);
#endif
} }
The drawing of the MCTD_ColorView which just is inherited from NSView:
- (void)setColor:(NSColor *)aColor;
{
[aColor retain];
[color release];
color = aColor;
}
- (void)drawRect:(NSRect)rect
{
NSRect bounds = [self bounds];
[color set];
[NSBezierPath fillRect:bounds];
}
- (void)dealloc
{
[color release];
[super dealloc];
}
When running my measurement loop and after putting breakpoints in "setColor" and "drawRect", debugging always stops in setColor but never in drawRect. Without debugging, the view remains white while I should see all different colors appearing.
As can be seen in my first post, I have tried many things to get it drawing, but they all failed.

how to show UI objects code in xcode?

in one part of my project I need to create some UI objects programmatically, can I just customize my UI Objects like labels,... visually in storyboard then simply copy/paste generated code relevant to that object?
I searched in xcode menu but I couldn't find this but once I saw it in a tutorial in youtube.
Thanks in Advance
Yes you can customize the UI Classes or any other class, Like I have customize UILabel Class as UILabelExtended
UILabelExtended.h
#import <Foundation/Foundation.h>
/* **********************************************************************************************
This class inherit the class UILabel and extend the features of UILabel.
********************************************************************************************** */
#interface UILabelExtended : UILabel {
__unsafe_unretained id customDelegate;
id objectInfo;
SEL selector;
}
#property (nonatomic,assign) SEL selector;;
#property (nonatomic,assign) id customDelegate;
#property (nonatomic,retain) id objectInfo;
#end
#interface UILabel(UILabelCategory)
- (void)setHeightOfLabel;
- (void)setWidthOfLabel;
- (void)setHeightOfLabelWithMaxHeight:(float)maxHeight;
- (void)setWidthOfLabelWithMaxWidth:(float)maxWidth ;
#end
UILabelExtended.m
#import "UILabelExtended.h"
#implementation UILabelExtended
#synthesize selector,customDelegate, objectInfo;
- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
if(self.selector)
if([self.customDelegate respondsToSelector:self.selector]) {
[self.customDelegate performSelector:self.selector withObject:self];
return;
}
}
- (void)dealloc {
self.customDelegate = nil;
self.selector = NULL;
self.objectInfo = nil;
}
#end
#implementation UILabel(UILabelCategory)
- (void)setHeightOfLabel {
UILabel* label = self;
//get the height of label content
CGFloat height = [label.text sizeWithFont:label.font constrainedToSize:CGSizeMake(label.bounds.size.width, 99999) lineBreakMode:NSLineBreakByWordWrapping].height;
//set the frame according to calculated height
CGRect frame = label.frame;
if([label.text length] > 0) {
frame.size.height = height;
}
else {
frame.size.height = 0;
}
label.frame = frame;
}
- (void)setWidthOfLabel {
UILabel* label = self;
//get the height of label content
CGFloat width = [label.text sizeWithFont:label.font constrainedToSize:CGSizeMake(99999, label.bounds.size.height) lineBreakMode:NSLineBreakByWordWrapping].width;
//set the frame according to calculated height
CGRect frame = label.frame;
if([label.text length] > 0) {
frame.size.width = width+5;
}
else {
frame.size.width = 0;
}
label.frame = frame;
}
- (void)setHeightOfLabelWithMaxHeight:(float)maxHeight {
UILabel* label = self;
//get the height of label content
CGFloat height = [label.text sizeWithFont:label.font constrainedToSize:CGSizeMake(label.bounds.size.width, maxHeight) lineBreakMode:NSLineBreakByWordWrapping].height;
//set the frame according to calculated height
CGRect frame = label.frame;
if([label.text length] > 0) {
if (height > maxHeight) {
frame.size.height = maxHeight;
}
else {
frame.size.height = height;
}
}
else {
frame.size.height = 0;
}
label.frame = frame;
}
- (void)setWidthOfLabelWithMaxWidth:(float)maxWidth {
UILabel* label = self;
//get the height of label content
CGFloat width = [label.text sizeWithFont:label.font constrainedToSize:CGSizeMake(99999, label.bounds.size.height) lineBreakMode:NSLineBreakByWordWrapping].width;
//set the frame according to calculated height
CGRect frame = label.frame;
if([label.text length] > 0) {
if (width > maxWidth) {
frame.size.width = maxWidth;
}
else {
frame.size.width = width;
}
}
else {
frame.size.width = 0;
}
label.frame = frame;
}
#end

Use a transparent image on top of the standard background color of a View

I' ve a NSOutlineView with a custom background color set with setBackroundColor: method. After that I wan' t to draw a transparent image on top of that background color. I tried with layer but it don' t work. Anyone knows how to do that? thanks
Something like this should work for your case,
Just pasting the relavent code from my application
What i wanted to say is,
You need to make a custom NSOutliveView and override its drawRect,
In case if it wouldn't work in your case, then take a Custom NSView and draw transparent NSOutlineView
- (void)drawRect:(NSRect)rect
{
switch (_backgroundStyle) {
case ImageColorBackgroundStyle:
{
[self drawColor];
[self drawCustomImage:rect];
break;
}
case ColorBackgroundStyle:
[self drawColor];
break;
case PatternBackgroundStyle:
[self drawPattern];
break;
case ImageBackgroundStyle:
[self drawImage];
break;
case GradientBackgroundStyle:
[self drawGradient];
break;
case NoBackgroundStyle:
if([self hasBorder])
[self drawBorder:rect];
return;
[[NSColor clearColor] set];
NSRectFill(rect);
break;
default:
[super drawRect:rect];
break;
}
if([self hasBorder])
[self drawBorder:rect];
}
-(void)drawCustomImage:(NSRect)rect{
NSRect boundsRect = [self bounds];
NSRect imageRect;
NSSize imageSize = [pBGroundImage size];
int x=0;int y=0;
switch (bgImagePosition) {
case img_bg_lowerright:
if(bViewClipped) return;
x = boundsRect.size.width - imageSize.width;
x -= (BORDER_WIDTH*2);
break;
default:
if(!bViewClipped)
return [self drawImage];
break;
}
if(!bViewClipped)
imageRect = NSMakeRect(x,y,imageSize.width,imageSize.height);
else {
imageRect = NSMakeRect(x,y,rect.size.width,rect.size.height);
}
if(imageRect.origin.x < 0 ) imageRect.origin.x = 0;
if(!bViewClipped){
[pBGroundImage drawInRect:imageRect fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0];
}
else {
CGFloat x_Res = 0.0f;
CGFloat y_Res = 0.0f;
x_Res = orignalSize.width/imageSize.width;
y_Res = orignalSize.height/imageSize.height;
CGFloat scaledHeight = orignalSize.height - rect.size.height;
CGFloat orignal_y = scaledHeight/y_Res;
CGFloat orignal_width = imageSize.height - orignal_y;
NSRect srcRect;
/* This is wrt Orignal Image */
srcRect.origin.x = 0;
srcRect.origin.y = orignal_y;
srcRect.size.width = imageSize.width;
srcRect.size.height = orignal_width;
[pBGroundImage drawInRect:rect
fromRect:srcRect
operation:NSCompositeSourceOver fraction:1.0];
}
}
- (void)drawImage
{
// Load the image.
//NSString *imageName = [self GetBackGroundImage];
//NSImage *anImage = [[NSImage alloc ]initByReferencingFile:imageName];
// Find the point at which to draw it.
NSPoint backgroundCenter;
backgroundCenter.x = [self bounds].size.width / 2;
backgroundCenter.y = [self bounds].size.height / 2;
NSPoint drawPoint = backgroundCenter;
drawPoint.x -= [pBGroundImage size].width / 2;
drawPoint.y -= [pBGroundImage size].height / 2;
[pBGroundImage drawInRect:[self bounds] fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0];
//[anImage release];
}

How do I implement scrollWheel elastic scrolling on OSX

I am trying to create a custom view, which is similar to NSScrollView, but is based on CoreAnimation CAScrollLayer/CATiledLayer. Basically, my app requires a lot of near realtime CGPath drawing, and animates these paths using shape layers (it's similar to the way GarageBand animates while recording). The first prototype I created used NSScrollView, but I could not get more than 20 frames per second with it (The reason was that NSRulerView updates by drawing every time the scrollEvent happens, and the entire call flow from -[NSClipView scrollToPoint] to -[NSScrollView reflectScrolledClipView:] is extremely expensive and inefficient).
I've created a custom view that uses CAScrollLayer as scrolling mechanism and CATiledLayer as the traditional documentView (for infinite scrolling option), and now I can get close to 60fps. However, I'm having hard time implementing scrollWheel elastic scrolling, and I'm not sure how to do it. Here's the code I have so far, and would really appreciate if someone can tell me how to implement elasticScrolling.
-(void) scrollWheel:(NSEvent *)theEvent{
NSCAssert(mDocumentScrollLayer, #"The Scroll Layer Cannot be nil");
NSCAssert(mDocumentLayer, #"The tiled layer cannot be nil");
NSCAssert(self.layer, #"The base layer of view cannot be nil");
NSCAssert(mRulerLayer, #"The ScrollLayer for ruler cannot be nil");
NSPoint locationInWindow = [theEvent locationInWindow];
NSPoint locationInBaseLayer = [self convertPoint:locationInWindow
fromView:nil];
NSPoint locationInRuler = [mRulerLayer convertPoint:locationInBaseLayer
fromLayer:self.layer];
if ([mRulerLayer containsPoint:locationInRuler]) {
return;
}
CGRect docRect = [mDocumentScrollLayer convertRect:[mDocumentLayer bounds]
fromLayer:mDocumentLayer];
CGRect scrollRect = [mDocumentScrollLayer visibleRect];
CGPoint newOrigin = scrollRect.origin;
CGFloat deltaX = [theEvent scrollingDeltaX];
CGFloat deltaY = [theEvent scrollingDeltaY];
if ([self isFlipped]) {
deltaY *= -1;
}
scrollRect.origin.x -= deltaX;
scrollRect.origin.y += deltaY;
if ((NSMinX(scrollRect) < NSMinX(docRect)) ||
(NSMaxX(scrollRect) > NSMaxX(docRect)) ||
(NSMinY(scrollRect) < NSMinY(docRect)) ||
(NSMaxY(scrollRect) > NSMaxX(docRect))) {
mIsScrollingPastEdge = YES;
CGFloat heightPhase = 0.0;
CGFloat widthPhase = 0.0;
CGSize size = [self frame].size;
if (NSMinX(scrollRect) < NSMinX(docRect)) {
widthPhase = ABS(NSMinX(scrollRect) - NSMinX(docRect));
}
if (NSMaxX(scrollRect) > NSMaxX(docRect)) {
widthPhase = ABS(NSMaxX(scrollRect) - NSMaxX(docRect));
}
if (NSMinY(scrollRect) < NSMinY(docRect)) {
heightPhase = ABS(NSMinY(scrollRect) - NSMinY(docRect));
}
if (NSMaxY(scrollRect) > NSMaxY(docRect)) {
heightPhase = ABS(NSMaxY(scrollRect) - NSMaxY(docRect));
}
if (widthPhase > size.width/2.0) {
widthPhase = size.width/2.0;
}
if (heightPhase > size.width/2.0) {
heightPhase = size.width/2.0;
}
deltaX = deltaX*(1-(2*widthPhase/size.width));
deltaY = deltaY*(1-(2*heightPhase/size.height));
}
newOrigin.x -= deltaX;
newOrigin.y += deltaY;
if ( mIsScrollingPastEdge &&
(([theEvent phase] == NSEventPhaseEnded) ||
([theEvent momentumPhase] == NSEventPhaseEnded)
)
){
CGPoint confinedScrollPoint = [mDocumentScrollLayer bounds].origin;
mIsScrollingPastEdge = NO;
CGRect visibleRect = [mDocumentScrollLayer visibleRect];
if (NSMinX(scrollRect) < NSMinX(docRect)){
confinedScrollPoint.x = docRect.origin.x;
}
if(NSMinY(scrollRect) < NSMinY(docRect)) {
confinedScrollPoint.y = docRect.origin.y;
}
if (NSMaxX(scrollRect) > NSMaxX(docRect)) {
confinedScrollPoint.x = NSMaxX(docRect) - visibleRect.size.width;
}
if (NSMaxY(scrollRect) > NSMaxY(docRect)){
confinedScrollPoint.y = NSMaxY(docRect) - visibleRect.size.height;
}
[mDocumentScrollLayer scrollToPoint:confinedScrollPoint];
CGPoint rulerPoint = [mRulerLayer bounds].origin;
rulerPoint.x = [mDocumentLayer bounds].origin.x;
[mRulerLayer scrollToPoint:rulerPoint];
return;
}
CGPoint rulerPoint = [mDocumentScrollLayer convertPoint:newOrigin
toLayer:mRulerLayer];
rulerPoint.y = [mRulerLayer bounds].origin.y;
if (!mIsScrollingPastEdge) {
[CATransaction setDisableActions:YES];
[mDocumentScrollLayer scrollToPoint:newOrigin];
[CATransaction commit];
}else{
[mDocumentScrollLayer scrollToPoint:newOrigin];
[mRulerLayer scrollToPoint:rulerPoint];
}
}
Take a look at TUIScrollView at https://github.com/twitter/twui.
The Core concept of adding ElasticScrolling is to use a SpringSolver.
This has a ElasticScrollView that you can reuse:
https://github.com/eonist/Element

Make MAAttachedWindow resizable without bounce?

To make the MAAttachedWindow resizable, I use code from here:
- (void)mouseDown:(NSEvent *)event {
NSPoint pointInView = [event locationInWindow];
BOOL resize = YES;
if (NSPointInRect(pointInView, [self resizeRect]))
{
resize = YES;
}
NSWindow *window = self;
NSPoint originalMouseLocation = [window convertBaseToScreen:[event locationInWindow]];
NSRect originalFrame = [window frame];
while (YES)
{
//
// Lock focus and take all the dragged and mouse up events until we
// receive a mouse up.
//
NSEvent *newEvent = [window
nextEventMatchingMask:(NSLeftMouseDraggedMask | NSLeftMouseUpMask)];
if ([newEvent type] == NSLeftMouseUp)
{
break;
}
//
// Work out how much the mouse has moved
//
NSPoint newMouseLocation = [window convertBaseToScreen:[newEvent locationInWindow]];
NSPoint delta = NSMakePoint(
newMouseLocation.x - originalMouseLocation.x,
newMouseLocation.y - originalMouseLocation.y);
NSRect newFrame = originalFrame;
if (!resize)
{
//
// Alter the frame for a drag
//
newFrame.origin.x += delta.x;
newFrame.origin.y += delta.y;
}
else
{
//
// Alter the frame for a resize
//
// newFrame.size.width += delta.x;
newFrame.size.width += delta.x*2; // customize
newFrame.size.height -= delta.y;
newFrame.origin.y += delta.y;
//
// Constrain to the window's min and max size
//
NSRect newContentRect = [window contentRectForFrameRect:newFrame];
NSSize maxSize = [window maxSize];
NSSize minSize = [window minSize];
if (newContentRect.size.width > maxSize.width)
{
newFrame.size.width -= newContentRect.size.width - maxSize.width;
}
else if (newContentRect.size.width < minSize.width)
{
newFrame.size.width += minSize.width - newContentRect.size.width;
}
if (newContentRect.size.height > maxSize.height)
{
newFrame.size.height -= newContentRect.size.height - maxSize.height;
newFrame.origin.y += newContentRect.size.height - maxSize.height;
}
else if (newContentRect.size.height < minSize.height)
{
newFrame.size.height += minSize.height - newContentRect.size.height;
newFrame.origin.y -= minSize.height - newContentRect.size.height;
}
}
[window setFrame:newFrame display:NO animate:NO];
}
}
When I drag the window, I can resize it. But there is a little bounce every now and then.
You can download the demo project.
Run the project and click the icon on the status menu to open the window. Try to stretch back and forward so you can reproduce it easily.
Hope someone can help me fixed this.
I checked out you code and it seemed like the bug was that occasionally NSPoint newMouseLocation = [window convertBaseToScreen:[newEvent locationInWindow]]; would return the original mouse location instead of the new one.
I'm not sure why that would fail but regardless that's not the optimal way to do it. Instead of calculating the new frame from the original mouse location, you should continually update that location and just move the window by the amount that the mouse has moved in the current tracking iteration
Here's a cleaned up, shortened version that seems to eliminate the bug.
- (void) mouseDown:(NSEvent *)event
{
NSWindow *window = self;
NSPoint originalMouseLocation = [window convertBaseToScreen:[event locationInWindow]];
while (YES)
{
NSEvent *newEvent = [window
nextEventMatchingMask:(NSLeftMouseDraggedMask | NSLeftMouseUpMask)];
if ([newEvent type] == NSLeftMouseUp)
{
break;
}
NSPoint newMouseLocation = [window convertBaseToScreen:[newEvent locationInWindow]];
NSPoint delta = NSMakePoint(
roundf(newMouseLocation.x - originalMouseLocation.x),
roundf(newMouseLocation.y - originalMouseLocation.y));
NSRect newFrame = [window frame];
newFrame.size.width += delta.x*2; // customize
newFrame.size.height -= delta.y;
newFrame.origin.y += delta.y;
[window setFrame:newFrame display:YES animate:NO];
originalMouseLocation = newMouseLocation; // <-- added this
}
}

Resources