Custom circular NSSlider - objective-c

I am trying to implement a custom circular slider which uses images to draw the dial and the knob. For this purpose I have subclassed NSSliderCell and overridden the drawBarInside and drawKnob methods.
Now, if I leave the slider type as default (horizontal), my drawing functions are called and images are drawn in stead of the default bar and knob (of course it looks all wrong since images are made for a circular slider, but they are there). But as soon as I change the slider type to circular (either in IB or by setting self.sliderType = NSCircularSlider; in my slider cell) those two methods are never called and my images are not drawn (only the standard dial is created).
Am I forgetting something here, or can circular sliders not be customized at all?
Here is my code:
DialSliderCell is a subclass of NSSliderCell and is initiated and set in a class called DialSlider which is a subclass of NSSlider (this class does nothing else at the time).
#implementation DialSliderCell
- (id)init {
self = [super init];
if (self) {
dialImage = [NSImage imageNamed:#"dial"];
knobImage = [NSImage imageNamed:#"dialKnob"];
self.sliderType = NSCircularSlider;
}
NSLog(#"init");
return self;
}
- (void)drawKnob:(NSRect)knobRect {
knobRect.size = knobImage.size;
[knobImage drawInRect:knobRect fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0];
NSLog(#"drawKnob");
}
- (void)drawBarInside:(NSRect)aRect flipped:(BOOL)flipped {
[dialImage drawInRect:aRect fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0];
NSLog(#"drawBarInside");
}
#end

I have solved this problem by overriding the drawInteriorWithFrame method. The solution is based on the original method. I just removed anything not related to circular sliders, removed the original graphics and added my own. The geometry is almost entirely original.
- (void) drawInteriorWithFrame: (NSRect)cellFrame inView: (NSView*)controlView
{
cellFrame = [self drawingRectForBounds: cellFrame];
_trackRect = cellFrame;
NSPoint knobCenter;
NSPoint point;
float fraction, angle, radius;
knobCenter = NSMakePoint(NSMidX(cellFrame), NSMidY(cellFrame));
[dialImage drawInRect:cellFrame fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0];
int knobDiameter = knobImage.size.width;
int knobRadius = knobDiameter / 2;
fraction = ([self floatValue] - [self minValue]) / ([self maxValue] - [self minValue]);
angle = (fraction * (2.0 * M_PI)) - (M_PI / 2.0);
radius = (cellFrame.size.height / 2) - knobDiameter;
point = NSMakePoint((radius * cos(angle)) + knobCenter.x,
(radius * sin(angle)) + knobCenter.y);
NSRect dotRect;
dotRect.origin.x = point.x - knobRadius;
dotRect.origin.y = point.y - knobRadius;
dotRect.size = knobImage.size;
[knobImage drawInRect:dotRect fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0];
}
The size of the cellFramerectangle is set to the size of the dialImageimage.
Notice that the method above doesn't seem to get called automatically so I also had to override the drawWithFrame method and make it call drawInteriorWithFrame:
- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView
{
[self drawInteriorWithFrame:cellFrame inView:controlView];
}

All I can think of is that you are not overriding the all the designated initialisers
from the docs...
Designated Initializers. When subclassing NSCell you must implement all
of the designated initializers. Those methods are: init,
initWithCoder:, initTextCell:, and initImageCell:.
Probably one of the other 3 is called for a circular slider.

Related

Not getting UIImageView Rounded Border while Added Multiplier for UIImageViwe

In Storyboard I applied Multiplier for height and width to UIImageView then I just want to rounded border so I used below code its not work for all iPhones.
_profileImgView.clipsToBounds = YES;
_profileImgView.layer.backgroundColor = color.CGColor;
_profileImgView.layer.cornerRadius =_profileImgView.frame.size.width/2;
_profileImgView.layer.borderColor = [UIColor colorWithRed:253.0/255.0 green:182.0/255.0 blue:43.0/255.0 alpha:100].CGColor;
_profileImgView.layer.borderWidth = 5.0f;
Since your corner radius depends on your frame size, you need to update it whenever the frame size changes. If you are using storyboards for your design, you will get the frame size that is in the design when viewDidLoad is called. If the frame size differs for different devices, you will get the final size at a later point in time in the views layoutSubviews or possibly the view controller's viewDidLayoutSubviews.
My suggested solution is to sub-class UIImageView and put the specifics for the image view in awakeFromNib and layoutSubviews, then use this class instead of UIImageView where appropriate.
// CircularImageView.h
#import <UIKit/UIKit.h>
#interface CircularImageView : UIImageView
#end
// CircularImageView.m
#implementation CircularImageView
- (void)awakeFromNib {
[super awakeFromNib];
self.clipsToBounds = YES;
self.layer.borderColor = [UIColor colorWithRed:253.0/255.0 green:182.0/255.0 blue:43.0/255.0 alpha:100].CGColor;
self.layer.borderWidth = 5.0f;
}
- (void)layoutSubviews {
[super layoutSubviews];
self.layer.cornerRadius = self.frame.size.width / 2;
}
#end
implement this code in same Viewcontroller class it is work for me
-(void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
_profileImgView.clipsToBounds = YES;
_profileImgView.layer.backgroundColor = color.CGColor;
_profileImgView.layer.cornerRadius =_profileImgView.frame.size.width/2;
_profileImgView.layer.borderColor = [UIColor colorWithRed:253.0/255.0 green:182.0/255.0 blue:43.0/255.0 alpha:100].CGColor;
_profileImgView.layer.borderWidth = 5.0f;
}

Receive global TouchEvents with NSEvent

I just try to receive Touch Events globally in the Window and not only in a view.
In my code, you can see below, i will get the absolute position of the touch in the magic trackpad. As long as the cursor is inside the view (the red NSRect) it works fine, but how can i receive touches outside of this view.
I searched for solutions in many communities and the apple devcenter but found nothing.
I think the problem is this: NSSet *touches = [ev touchesMatchingPhase:NSTouchPhaseTouching inView:nil]; Isn't there a method in NSEvent that gets every touch?
Hope anybody can help me.
Here my Implementation:
#implementation MyView
- (id)initWithFrame:(NSRect)frame {
self = [super initWithFrame:frame];
if (self) {
// Initialization code here.
[self setAcceptsTouchEvents:YES];
myColor = [NSColor colorWithDeviceRed:1.0 green:0 blue:0 alpha:0.5];
}
return self;
}
- (void)drawRect:(NSRect)dirtyRect {
// Drawing code here.
NSRect bounds = [self bounds];
[myColor set];
[NSBezierPath fillRect:bounds];
}
- (void)touchesBeganWithEvent:(NSEvent *)ev {
NSSet *touches = [ev touchesMatchingPhase:NSTouchPhaseTouching inView:nil];
for (NSTouch *touch in touches) {
NSPoint fraction = touch.normalizedPosition;
NSSize whole = touch.deviceSize;
NSPoint wholeInches = {whole.width / 72.0, whole.height / 72.0};
NSPoint pos = wholeInches;
pos.x *= fraction.x;
pos.y *= fraction.y;
NSLog(#"%s: Finger is touching %g inches right and %g inches up "
#"from lower left corner of trackpad.", __func__, pos.x, pos.y);
}
}
Calling touchesMatchingPhase:inView: with nil for the last parameter (the way you are doing) will get all touches. The problem is that touchesBeganWithEvent: will simply not fire for a control that isn't in the touch area.
You can make the view the first responder, which will send all events to it first. See becomeFirstResponder and Responder object

NSScrollView infinite / endless scroll | subview reuse

I'm searching for a way to implement something like reusable cells for UI/NSTableView but for NSScrollView. Basically I want the same like the WWDC 2011 video "Session 104 - Advanced Scroll View Techniques" but for Mac.
I have several problems realizing this. The first: NSScrollView doesn't have -layoutSubviews. I tried to use -adjustScroll instead but fail in setting a different contentOffset:
- (NSRect)adjustScroll:(NSRect)proposedVisibleRect {
if (proposedVisibleRect.origin.x > 600) {
// non of them work properly
// proposedVisibleRect.origin.x = 0;
// [self setBoundsOrigin:NSZeroPoint];
// [self setFrameOrigin:NSZeroPoint];
// [[parentScrollView contentView] scrollPoint:NSZeroPoint];
// [[parentScrollView contentView] setBoundsOrigin:NSZeroPoint];
}
return proposedVisibleRect;
}
The next thing I tried was to set a really huge content view with a width of millions of pixel (which actually works in comparison to iOS!) but now the question is, how to install a reuse-pool?
Is it better to move the subviews while scrolling to a new position or to remove all subviews and insert them again? and how and where should I do that?
As best I can tell, -adjustScroll: is not where you want to tap into the scrolling events because it doesn't get called universally. I think -reflectScrolledClipView: is probably a better hookup point.
I cooked up the following example that should hit the high points of one way to do a view-reusing scroll view. For simplicity, I set the dimensions of the scrollView's documentView to "huge", as you suggest, rather than trying to "fake up" the scrolling behavior to look infinite. Obviously drawing the constituent tile views for real is up to you. (In this example I created a dummy view that just fills itself with red with a blue outline to convince myself that everything was working.) It came out like this:
// For the header file
#interface SOReuseScrollView : NSScrollView
#end
// For the implementation file
#interface SOReuseScrollView () // Private
- (void)p_updateTiles;
#property (nonatomic, readonly, retain) NSMutableArray* p_reusableViews;
#end
// Just a small diagnosting view to convince myself that this works.
#interface SODiagnosticView : NSView
#end
#implementation SOReuseScrollView
#synthesize p_reusableViews = mReusableViews;
- (void)dealloc
{
[mReusableViews release];
[super dealloc];
}
- (NSMutableArray*)p_reusableViews
{
if (nil == mReusableViews)
{
mReusableViews = [[NSMutableArray alloc] init];
}
return mReusableViews;
}
- (void)reflectScrolledClipView:(NSClipView *)cView
{
[super reflectScrolledClipView: cView];
[self p_updateTiles];
}
- (void)p_updateTiles
{
// The size of a tile...
static const NSSize gGranuleSize = {250.0, 250.0};
NSMutableArray* reusableViews = self.p_reusableViews;
NSRect documentVisibleRect = self.documentVisibleRect;
// Determine the needed tiles for coverage
const CGFloat xMin = floor(NSMinX(documentVisibleRect) / gGranuleSize.width) * gGranuleSize.width;
const CGFloat xMax = xMin + (ceil((NSMaxX(documentVisibleRect) - xMin) / gGranuleSize.width) * gGranuleSize.width);
const CGFloat yMin = floor(NSMinY(documentVisibleRect) / gGranuleSize.height) * gGranuleSize.height;
const CGFloat yMax = ceil((NSMaxY(documentVisibleRect) - yMin) / gGranuleSize.height) * gGranuleSize.height;
// Figure out the tile frames we would need to get full coverage
NSMutableSet* neededTileFrames = [NSMutableSet set];
for (CGFloat x = xMin; x < xMax; x += gGranuleSize.width)
{
for (CGFloat y = yMin; y < yMax; y += gGranuleSize.height)
{
NSRect rect = NSMakeRect(x, y, gGranuleSize.width, gGranuleSize.height);
[neededTileFrames addObject: [NSValue valueWithRect: rect]];
}
}
// See if we already have subviews that cover these needed frames.
for (NSView* subview in [[[self.documentView subviews] copy] autorelease])
{
NSValue* frameRectVal = [NSValue valueWithRect: subview.frame];
// If we don't need this one any more...
if (![neededTileFrames containsObject: frameRectVal])
{
// Then recycle it...
[reusableViews addObject: subview];
[subview removeFromSuperview];
}
else
{
// Take this frame rect off the To-do list.
[neededTileFrames removeObject: frameRectVal];
}
}
// Add needed tiles from the to-do list
for (NSValue* neededFrame in neededTileFrames)
{
NSView* view = [[[reusableViews lastObject] retain] autorelease];
[reusableViews removeLastObject];
if (nil == view)
{
// Create one if we didnt find a reusable one.
view = [[[SODiagnosticView alloc] initWithFrame: NSZeroRect] autorelease];
NSLog(#"Created a view.");
}
else
{
NSLog(#"Reused a view.");
}
// Place it and install it.
view.frame = [neededFrame rectValue];
[view setNeedsDisplay: YES];
[self.documentView addSubview: view];
}
}
#end
#implementation SODiagnosticView
- (void)drawRect:(NSRect)dirtyRect
{
// Draw a red tile with a blue border.
[[NSColor blueColor] set];
NSRectFill(self.bounds);
[[NSColor redColor] setFill];
NSRectFill(NSInsetRect(self.bounds, 2,2));
}
#end
This worked pretty well as best I could tell. Again, drawing something meaningful in the reused views is where the real work is here.
Hope that helps.

Move NSView around until it hits a border

In a Cocoa-based App i'm having a canvas for drawing, inherited from NSView, as well as a rectangle, also inherited from NSView. Dragging the rectangle around inside of the canvas is no problem:
-(void)mouseDragged:(NSEvent *)theEvent {
NSPoint myOrigin = self.frame.origin;
[self setFrameOrigin:NSMakePoint(myOrigin.x + [theEvent deltaX],
myOrigin.y - [theEvent deltaY])];
}
Works like a charm. The issue i'm having now: How can i prevent the rectangle from being moved outside the canvas?
So, first of all i would like to fix this just for the left border, adapting the other edges afterwards. My first idea is: "check whether the x-origin of the rectangle is negative". But: once it is negative the rectangle can't be moved anymore around (naturally). I solved this with moving the rectangle to zero x-offset in the else-branch. This works but it's ... ugly.
So i'm little puzzled with this one, any hints? Definitely the solution is really near and easy. That easy, that i cannot figure it out (as always with easy solutions ;).
Regards
Macs
I'd suggest not using the deltaX and deltaY; try using the event's location in the superview. You'll need a reference to the subview.
// In the superview
- (void)mouseDragged:(NSEvent *)event {
NSPoint mousePoint = [self convertPoint:[event locationInWindow]
fromView:nil];
// Could also add the width of the moving rectangle to this check
// to keep any part of it from going outside the superview
mousePoint.x = MAX(0, MIN(mousePoint.x, self.bounds.size.width));
mousePoint.y = MAX(0, MIN(mousePoint.y, self.bounds.size.height));
// position is a custom ivar that indicates the center of the object;
// you could also use frame.origin, but it looks nicer if objects are
// dragged from their centers
myMovingRectangle.position = mousePoint;
[self setNeedsDisplay:YES];
}
You'd do essentially the same bounds checking in mouseUp:.
UPDATE: You should also have a look at the View Programming Guide, which walks you through creating a draggable view: Creating a Custom View.
Sample code that should be helpful, though not strictly relevant to your original question:
In DotView.m:
- (void)drawRect:(NSRect)dirtyRect {
// Ignoring dirtyRect for simplicity
[[NSColor colorWithDeviceRed:0.85 green:0.8 blue:0.8 alpha:1] set];
NSRectFill([self bounds]);
// Dot is the custom shape class that can draw itself; see below
// dots is an NSMutableArray containing the shapes
for (Dot *dot in dots) {
[dot draw];
}
}
- (void)mouseDown:(NSEvent *)event {
NSPoint mousePoint = [self convertPoint:[event locationInWindow]
fromView:nil];
currMovingDot = [self clickedDotForPoint:mousePoint];
// Move the dot to the point to indicate that the user has
// successfully "grabbed" it
if( currMovingDot ) currMovingDot.position = mousePoint;
[self setNeedsDisplay:YES];
}
// -mouseDragged: already defined earlier in post
- (void)mouseUp:(NSEvent *)event {
if( !currMovingDot ) return;
NSPoint mousePoint = [self convertPoint:[event locationInWindow]
fromView:nil];
spot.x = MAX(0, MIN(mousePoint.x, self.bounds.size.width));
spot.y = MAX(0, MIN(mousePoint.y, self.bounds.size.height));
currMovingDot.position = mousePoint;
currMovingDot = nil;
[self setNeedsDisplay:YES];
}
- (Dot *)clickedDotForPoint:(NSPoint)point {
// DOT_NUCLEUS_RADIUS is the size of the
// dot's internal "handle"
for( Dot *dot in dots ){
if( (abs(dot.position.x - point.x) <= DOT_NUCLEUS_RADIUS) &&
(abs(dot.position.y - point.y) <= DOT_NUCLEUS_RADIUS)) {
return dot;
}
}
return nil;
}
Dot.h
#define DOT_NUCLEUS_RADIUS (5)
#interface Dot : NSObject {
NSPoint position;
}
#property (assign) NSPoint position;
- (void)draw;
#end
Dot.m
#import "Dot.h"
#implementation Dot
#synthesize position;
- (void)draw {
//!!!: Demo only: assume that focus is locked on a view.
NSColor *clr = [NSColor colorWithDeviceRed:0.3
green:0.2
blue:0.8
alpha:1];
// Draw a nice border
NSBezierPath *outerCirc;
outerCirc = [NSBezierPath bezierPathWithOvalInRect:
NSMakeRect(position.x - 23, position.y - 23, 46, 46)];
[clr set];
[outerCirc stroke];
[[clr colorWithAlphaComponent:0.7] set];
[outerCirc fill];
[clr set];
// Draw the "handle"
NSRect nucleusRect = NSMakeRect(position.x - DOT_NUCLEUS_RADIUS,
position.y - DOT_NUCLEUS_RADIUS,
DOT_NUCLEUS_RADIUS * 2,
DOT_NUCLEUS_RADIUS * 2);
[[NSBezierPath bezierPathWithOvalInRect:nucleusRect] fill];
}
#end
As you can see, the Dot class is very lightweight, and uses bezier paths to draw. The superview can handle the user interaction.

CGAffineTransformMakeScale Makes UIView Jump to Original Size before scale

I have a UIView that I set up to respond to pinch gestures and change its size, except, once you enlarge it and then try and pinch it again, it jumps to its original size (which just so happens to be 100x200). Here is the code:
#implementation ChartAxisView
- (id)initWithFrame:(CGRect)frame {
if ((self = [super initWithFrame:frame])) {
// do gesture recognizers
UIPinchGestureRecognizer *pinch = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:#selector(onPinch:)];
[self addGestureRecognizer:pinch];
[pinch release];
}
return self;
}
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGColorRef redColor = [UIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:1.0].CGColor;
CGContextSetFillColorWithColor(context, redColor);
CGContextFillRect(context, self.bounds);
}
- (void)onPinch: (UIPinchGestureRecognizer*) gesture {
self.transform = CGAffineTransformMakeScale(gesture.scale, gesture.scale);
}
- (void)dealloc {
[super dealloc];
}
#end
Any thoughts?
So there are two types of Scale (or, transform in general) functions: CGAffineTransformMakeScale and CGAffineTransformScale
The first one, CGAffineTransformMakeScale which you are using, always transforms with respect to the image's original size. And that is why you see the jump to its original size before the scaling happens.
The second one, CGAffineTransformScale, transforms from the image's current position. This is what you need. For this, it requires an additional 'transform' arg. The 'transform' arg in your case represents the enlarged image.
Read this very informative blog post about transformations.
- (void)onPinch: (UIPinchGestureRecognizer*) gesture {
if ([gesture state] == UIGestureRecognizerStateBegan)
{
curTransform = self.transform;
}
self.transform = CGAffineTransformScale(curTransform,gesture.scale, gesture.scale);
}
use CGAffineTransformScale instead of CGAffineTransformMakeScale
you will need a member -> CGAffineTransform curTransform;
;)
you can set transform with following code:
ivClone setTransform:CGAffineTransformMakeScale(scale, scale)];
and you can get current transform with following coee:
newV.transform.a //x scale
newV.transform.d // y scale