Erasing Cocoa Drawing done by NSRectFill? - objective-c

I have an NSBox, inside of which I am drawing small rectangles, with NSRectFill(). My code for this looks like this:
for (int i = 0; i <= 100; i++){
int x = (rand() % 640) + 20;
int y = (rand() % 315) + 196;
array[i] = NSMakeRect(x, y, 4, 4);
NSRectFill(array[i]);
}
This for loop creates 100 randomly placed rectangles within the grid. What I have been trying to do is create a sort of animation, created by this code running over and over, creating an animation of randomly appearing rectangles, with this code:
for (int i = 0; i <= 10; i++) {
[self performSelector:#selector(executeFrame) withObject:nil afterDelay:(.05*i)];
}
The first for loop is the only thing inside the executeFrame function, by the way. So, what I need to do is to erase all the rectangles between frames, so the number of them stays the same and they look like they are moving. I tried doing this by just drawing the background again, by calling [myNsBox display]; before calling executeFrame, but that made it seem as though no rectangles were being drawn. Calling it after did the same thing, so did switching in setNeedsDisplay instead of display. I cannot figure this one out, any help would be appreciated.
By the way, an additional thing is that when I try to run my code for executing the frames, without trying to erase the rectangles in between, all that happens is that 100 more rectangles are drawn. Even if I have requested that 1000 be drawn, or 10,000. Then though, if I leave the window and come back to it (immediately, time is not a factor here), the page updates and the rectangles are there. I attempted to overcome that by with [box setNeedsDisplayInRect:array[i]]; which worked in a strange way, causing it to update every frame, but erasing portions of the rectangles. Any help in this would also be appreciated.

It sounds like you're drawing outside drawRect: . If that's the case, move your drawing code into a view's (the box's or some subview's) drawRect: method. Otherwise your drawing will get stomped on by the Cocoa drawing system like you're seeing. You'll also want to use timers or animations rather than loops to do the repeated drawing.

I recently wrote an example program for someone trying to do something similar with circles. The approach I took was to create an array of circle specifications and to draw them in drawRect. It works pretty well. Maybe it will help. If you want the whole project, you can download it from here
#implementation CircleView
#synthesize maxCircles, circleSize;
- (id)initWithFrame:(NSRect)frame {
self = [super initWithFrame:frame];
if (self) {
maxCircles = 1000;
circles = [[NSMutableArray alloc] initWithCapacity:maxCircles];
}
return self;
}
- (void)dealloc {
[circles release];
[super dealloc];
}
- (void)drawRect:(NSRect)dirtyRect {
NSArray *myCircles;
#synchronized(circles) {
myCircles = [circles copy];
}
NSRect bounds = [self bounds];
NSRect circleBounds;
for (NSDictionary *circleSpecs in myCircles) {
NSColor *color = [circleSpecs objectForKey:colorKey];
float size = [[circleSpecs objectForKey:sizeKey] floatValue];
NSPoint origin = NSPointFromString([circleSpecs objectForKey:originKey]);
circleBounds.size.width = size * bounds.size.width;
circleBounds.size.height = size * bounds.size.height;
circleBounds.origin.x = origin.x * bounds.size.width - (circleBounds.size.width / 2);
circleBounds.origin.y = origin.y * bounds.size.height - (circleBounds.size.height / 2);
NSBezierPath *drawingPath = [NSBezierPath bezierPath];
[color set];
[drawingPath appendBezierPathWithOvalInRect:circleBounds];
[drawingPath fill];
}
[myCircles release];
}
#pragma mark Public Methods
-(void)makeMoreCircles:(BOOL)flag {
if (flag) {
circleTimer = [NSTimer scheduledTimerWithTimeInterval:0.2 target:self selector:#selector(makeACircle:) userInfo:nil repeats:YES];
}
else {
[circleTimer invalidate];
}
}
-(void)makeACircle:(NSTimer*)theTimer {
// Calculate a random color
NSColor *color;
color = [NSColor colorWithCalibratedRed:(arc4random() % 255) / 255.0
green:(arc4random() % 255) / 255.0
blue:(arc4random() % 255) / 255.0
alpha:(arc4random() % 255) / 255.0];
//Calculate a random origin from 0 to 1
NSPoint origin;
origin.x = (double)arc4random() / (double)0xFFFFFFFF;
origin.y = (double)arc4random() / (double)0xFFFFFFFF;
NSDictionary *circleSpecs = [NSDictionary dictionaryWithObjectsAndKeys:color, colorKey,
[NSNumber numberWithFloat:circleSize], sizeKey,
NSStringFromPoint(origin), originKey,
nil];
#synchronized(circles) {
[circles addObject:circleSpecs];
if ([circles count] > maxCircles) {
[circles removeObjectsInRange:NSMakeRange(0, [circles count] - maxCircles)];
}
}
[self setNeedsDisplay:YES];
}
#end

Related

How to slice/cut sprites in iOS (Core Graphics)

I am working on a game and I would like to add a proper slicing feature in it.. so when a sprite sliced, 2 new sprites should be created.. please check here
At the moment, I am just reducing the size and duplicating the sprites.. Something like this.. Thanks in advance..
- (BOOL) sliceSprite: (Sprite *) sprite withPath: (UIBezierPath *) slicePath
{
CGSize size = sprite.size;
size.width /= 2;
size.height /=2;
sprite.size = size;
sprite.sliced = YES;
Sprite *newSprite = [[Sprite alloc] initWithImage: sprite.image];
newSprite.position = sprite.position;
newSprite.size = size;
newSprite.sliced = YES;
newSprite.inView = YES;
newSprite.xVelocity = SLICE_SPEEDUP * sprite.yVelocity;
newSprite.yVelocity = SLICE_SPEEDUP * sprite.xVelocity;
newSprite.angularVelocity = -SLICE_REVUP * sprite.angularVelocity;
[sprites addObject: newSprite];
[newSprite release];
sprite.angularVelocity = SLICE_REVUP * sprite.angularVelocity;
sprite.xVelocity = -SLICE_SPEEDUP * sprite.xVelocity;
sprite.yVelocity = -SLICE_SPEEDUP * sprite.yVelocity;
return YES;
}
- (void) sliceSpritesInSwipePath
{
CGRect swipeRect = [swipePath bounds];
for (NSUInteger i = 0; i < [sprites count]; i++)
{
Sprite *sprite = [sprites objectAtIndex: i];
if ([sprite intersectsWithPathInArray: swipePoints
inRect: swipeRect])
if ([self sliceSprite: sprite withPath: swipePath])
{
[self resetSwipe];
if (![sliceSound isPlaying])
[sliceSound play];
break;
}
}
}
Is the specific line of splitting required? Fruit Ninja just spawns two halves of the fruit, as if it was split down the middle, this would be quite easy to do:
Create two sprites which are half the width of the original sprite
Position them 1/4 and 3/4 of the way along the original sprite's horizontal centre line
Add rotation/acceleration etc.
Modify texture coordinates so that the left sprite has the left half of the texture and the right sprite has the right half of the texture.
Since you're using CoreGraphics here, why not simply use a clipping path when drawing the sprite(s)?
Duplicate the sprite to be sliced, then apply simple polygons masking the two halves as their respective clipping paths. The function you need is called CGContextClip and a short tutorial can be found here.
Edit: The tutorial lists this example:
CGContextBeginPath (context);
CGContextAddArc (context, w/2, h/2, ((w>h) ? h : w)/2, 0, 2*PI, 0);
CGContextClosePath (context);
CGContextClip (context);
This sets the current path to a circle, then applies the current path as the clipping region.

Tile based map movement sluggish with UIImage

I have the following code which draws a bunch of uiimage tiles on screen each time the screen is touched (its a character exploring a tile dungeon). But the movement is very jumpy if I click quickly. Why is it acting sluggishly.
Thanks.
- (void) updateView {
// lets remove all subviews and start fresh
for (UIView *subview in self.view.subviews)
[subview removeFromSuperview];
[self drawVisibleDungeon];
[self displayMonsters];
[self drawPlayer];
[self.view setNeedsDisplay];
[self.view addSubview:messageDisplay];
[self.view addSubview:miniMap];
[miniMap setNeedsDisplay];
}
- (void) processTouchAtX: (int)x AndY: (int)y
{
int tempx = 0;
int tempy = 0;
if (x > 4)
tempx++;
if (x < 4)
tempx--;
if (y > 6)
tempy++;
if (y < 6)
tempy--;
[[[self _accessHook] dungeonReference]processOneTurnWithX:tempx AndY:tempy];
[self updateView];
}
- (void) displayMonsters
{
UIImage *aImg;
int x, y;
NSString *aString;
int monCount = [[[_accessHook dungeonReference]aDungeonLevel]monsterCount];
NSMutableArray *monArray = [[[_accessHook dungeonReference]aDungeonLevel]mArray];
int player_x_loc = [[[self _accessHook] dungeonReference] thePlayer].x_location;
int player_y_loc = [[[self _accessHook] dungeonReference] thePlayer].y_location;
for (int i=0; i<monCount; i++)
{
x = [[monArray objectAtIndex: i] x_loc];
y = [[monArray objectAtIndex: i] y_loc];
if ([self LOSFrom:CGPointMake(x, y)])
if ((x >= player_x_loc-4) && (x < (player_x_loc+5)) && (y >= player_y_loc-6) && (y < (player_y_loc+6)))
{
UIImageView *imgView=[[UIImageView alloc]init];
aString = [[monArray objectAtIndex:i]monsterPicture];
aImg = [UIImage imageNamed:aString];
imgView.frame=CGRectMake((x - (player_x_loc-4))*32, (y - (player_y_loc-6))*32, 32, 32);
imgView.image = aImg;
[self.view addSubview:imgView];
[self.view bringSubviewToFront:imgView];
[imgView release];
}
}
}
Your idea of using cocos2d is fine, and it's a very good framework and probably well suited to other problems you may face. But... why don't we also discuss why you're having so much trouble.
You're using a separate UIImageView for each tile. UIImage is fine, and pretty efficient for most purposes. But a full view for every tile is pretty expensive. Worse, every time you call updateView, you take everything and throw it away. That's very, very expensive.
Just as step one, because it's easy for you to fix, set up all the image views at the beginning and store them in an array. Then in displayMonsters, just change their locations and images rather than deleting them and creating new ones. That by itself will dramatically improve your performance I suspect.
If you still need more performance, you can switch to using CALayer rather than UIImageView. You can very easily create a CALayer and set its contents to aImg.CGImage. Again, you'd add a bunch of CALayer objects with [view.layer addSublayer:layer], just like adding views. Layers are just much cheaper than views because they don't handle things like touch events. They just draw.
But if you anticipate needing fancier 2D graphics, there's nothing wrong with Cocos2D.
It's slow because using UIImage for a bunch of map tiles has a lot of overhead. It would be best to draw everything using OpenGL and vertex buffers.

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.

UIView hitTest:withEvent: and pointInside:withEvent

I'm working with Mixare AR SDK for iOS and I need to solve some bugs that happends, one of them is show the information of a POI when the POI's view is tapped.
Prelude:
Mixare has an overlay UIView within MarkerView views are placed, MarkerView views are moving around the screen to geolocate the POIs and each one has two subviews, an UIImageView and an UILabel.
Issue:
Now, for example, there are 3 visible POIs in the screen, so there are 3 MarkerView as overlay subviews. If you touch anywhere in the overlay, a info view associated to a random POI of which are visible is showed.
Desired:
I want that the associated POI's info is shown only when the user tapped a MarkerView
Let's work. I've see that MarkerView inherits from UIView and implements hitTest:withEvent
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
viewTouched = (MarkerView*)[super hitTest:point withEvent:event];
return self;
}
I've put a breakpoint and hitTest is called once for each visible MarkerView but loadedView always is null so I can't work with it, so I've tried to check if the hit point is inside the MarkerView frame implementing pointInside:withEvent: by this way
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
NSLog(#"ClassName: %#", [[self class] description]);
NSLog(#"Point Inside: %f, %f", point.x, point.y);
NSLog(#"Frame x: %f y: %f widht:%f height:%f", self.frame.origin.x, self.frame.origin.y, self.frame.size.width, self.frame.size.height);
if (CGRectContainsPoint(self.frame, point))
return YES;
else
return NO;
return YES;
}
But this function always returns NO, even when I touch the MarkerView. When I check the log I saw that X and Y point values has negative values sometimes and width and height of the view are very small, 0.00022 or similar instead of 100 x 150 that I set the MarkerView frame on its initialization.
Here you are a extract of my log in which you can see the class name, the point and the MarkerView frame values.
ClassName: MarkerView
2011-12-29 13:20:32.679 paisromanico[2996:707] Point Inside: 105.224899, 49.049023
2011-12-29 13:20:32.683 paisromanico[2996:707] Frame x: 187.568573 y: 245.735138 widht:0.021862 height:0.016427
I'm very lost with this issue so any help will be welcome. Thanks in advance for any help provided and I'm sorry about this brick :(
Edit:
At last I've found that the problem is not in hitTest:withEvent: or pointInside:withEvent, problem is with CGTransform that applies to the MarkerView for scaling based on distande and rotating the view, if I comment any code related to this, the Mixare AR SDK works fine, I mean, info view is shown correctly if you touch a marker and doesn't do anything if any other place in the screen is touched.
So, by the moment, I've not solved the problem but I applied a patch removing the CGTransform related code in AugmentedViewController.m class - (void)updateLocations:(NSTimer *)timer function
- (void)updateLocations:(NSTimer *)timer {
//update locations!
if (!ar_coordinateViews || ar_coordinateViews.count == 0) {
return;
}
int index = 0;
NSMutableArray * radarPointValues= [[NSMutableArray alloc]initWithCapacity:[ar_coordinates count]];
for (PoiItem *item in ar_coordinates) {
MarkerView *viewToDraw = [ar_coordinateViews objectAtIndex:index];
viewToDraw.tag = index;
if ([self viewportContainsCoordinate:item]) {
CGPoint loc = [self pointInView:ar_overlayView forCoordinate:item];
CGFloat scaleFactor = 1.5;
if (self.scaleViewsBasedOnDistance) {
scaleFactor = 1.0 - self.minimumScaleFactor * (item.radialDistance / self.maximumScaleDistance);
}
float width = viewToDraw.bounds.size.width ;//* scaleFactor;
float height = viewToDraw.bounds.size.height; // * scaleFactor;
viewToDraw.frame = CGRectMake(loc.x - width / 2.0, loc.y-height / 2.0, width, height);
/*
CATransform3D transform = CATransform3DIdentity;
//set the scale if it needs it.
if (self.scaleViewsBasedOnDistance) {
//scale the perspective transform if we have one.
transform = CATransform3DScale(transform, scaleFactor, scaleFactor, scaleFactor);
}
if (self.rotateViewsBasedOnPerspective) {
transform.m34 = 1.0 / 300.0;
double itemAzimuth = item.azimuth;
double centerAzimuth = self.centerCoordinate.azimuth;
if (itemAzimuth - centerAzimuth > M_PI) centerAzimuth += 2*M_PI;
if (itemAzimuth - centerAzimuth < -M_PI) itemAzimuth += 2*M_PI;
double angleDifference = itemAzimuth - centerAzimuth;
transform = CATransform3DRotate(transform, self.maximumRotationAngle * angleDifference / (VIEWPORT_HEIGHT_RADIANS / 2.0) , 0, 1, 0);
}
viewToDraw.layer.transform = transform;
*/
//if we don't have a superview, set it up.
if (!(viewToDraw.superview)) {
[ar_overlayView addSubview:viewToDraw];
[ar_overlayView sendSubviewToBack:viewToDraw];
}
} else {
[viewToDraw removeFromSuperview];
viewToDraw.transform = CGAffineTransformIdentity;
}
[radarPointValues addObject:item];
index++;
}
float radius = [[[NSUserDefaults standardUserDefaults] objectForKey:#"radius"] floatValue];
if(radius <= 0 || radius > 100){
radius = 5.0;
}
radarView.pois = radarPointValues;
radarView.radius = radius;
[radarView setNeedsDisplay];
[radarPointValues release];
}
Any CoreGrapics or UI expert could give us his point of view about this issue??
You should either try to hittest as attached:
if ([self pointInside:point withEvent:event]) {
// do something
}
I would suggest you add the hit test on the superview, and do the following in the hit test of the parent of the markerViews
if ([markerView pointInside:point withEvent:event]) {
// extract the tag and show the relevant info
}
Hope this helps

How do I manage the drawing of custom data (not photos) in an NSView that has an area dynamically larger than what is viewable?

UPDATE: Relative to the questions and answers below, it seems I have a misunderstanding of the NSView class in relation to the custom classes I'm trying to draw and the wrapping NSScrollView. In the end, what I'm trying to figure out is how do I manage the dynamic drawing of custom data (not photos) in an NSView that has an area larger than what is viewable?
I am not looking for a handout, but I am a novice to Cocoa and I thought I was doing best-practice based on Apple's docs, but it seems I've gotten the fundamentals wrong. Apple's documentation is incredibly detailed, technical, centered entirely around working with photos, and thus useless to me. The related code examples provided by Apple (e.g. Sketch) get the document size from the printer paper sizes in their typically oblique fashion, and that's not what I need. I've scoured the web for tutorials, examples and the like, but I'm not finding much of anything (and I promise to write one when I get this sorted out).
I'm porting this code from REALbasic where I have this completely working, even with Undo commands, but the paradigms to do so are entirely different. This just isn't "clicking" for me. I appreciate the help given, I'm still missing something here, anything else folks have to offer is appreciated.
Thanks
I have a subclassed NSView where I'm creating a piano-roll MIDI interface. I am trying to resolve a few problems:
Drawing artifacts during and after scrolling
Lines not spanning across the visible area during and after scrolling
While scrolling and sometimes on mouseDown, the horizontal scroller jumps to the right 1 (one) pixel, but I don't have scrollToPoint implemented anywhere yet.
Symptoms that relate to the above:
Implementing adjustScroll makes everything worse.
mouseDown corrects all of the problems except sometimes the 1-pixel jump to the right.
If I uncomment the NSLog command the beginning of drawRect nothing draws.
Apple's documentation mentions pixel-accurate drawing, but (of course) offers up no examples on how this can be achieved. I've been using the floor() function to try to get consistent values, but once I start tacking on scrollToPoint or any other complexity, things go haywire.
Please see the linked image as an example. The screenshot, if you can believe it, actually cleans up what I see on screen. There are double lines almost everywhere at half opacity as well. The same is applied to any objects I draw as well.
Graphics Artifacts and inconsistencies in a subclassed NSView generated after scrolling http://www.oatmealandcoffee.com/external/NSViewArtifacts.png
Here is the code. I hate giving up so much publicly, but I've searched everywhere for clues, and if the Internet is any indication I'm the only person with this problem, and I really just want to get this sorted out and move forward. There is a lot, and there is more to come, but these is the core stuff I really need to get right, and, frankly, I am at a loss on how to correct it.
- (void)drawRect:(NSRect)rect {
//NSLog(#"OCEditorView:drawRect: START");
[self setFrame:[[self EditorDocument] DocumentRect]];
[[NSGraphicsContext currentContext] setShouldAntialias:NO];
// CLEAR BACKGROUND
[[[self EditorDocument] ColorWhiteKey] set];
NSRectFill(rect);
// BACKGROUND KEYS
int firstRowLine = 0; //NSMinY(rect); //<- adding the function results in bad spacing on scrolling
int currentRowLine = 0;
int lastRowLine = NSMaxY(rect);
//NSLog(#"lastRowLine:%d", lastRowLine);
float currentZoomY = [self ZoomY];
for (currentRowLine = firstRowLine; currentRowLine <= lastRowLine; currentRowLine += currentZoomY) {
int currentTone = floor(currentRowLine / [self ZoomY]);
BOOL isBlackKey = [[self MusicLib] IsBlackKey:currentTone];
//NSLog(#"%d, tone:%d, black:%d", [self MusicLib], currentTone, isBlackKey);
if (isBlackKey) {
[[[self EditorDocument] ColorBlackKey] set];
} else {
[[NSColor whiteColor] set];
}
NSBezierPath *rowLine = [NSBezierPath bezierPath];
NSPoint bottomLeftPoint = NSMakePoint(NSMinX(rect), currentRowLine);
NSPoint bottomRightPoint = NSMakePoint(NSMaxX(rect), currentRowLine);
NSPoint topRightPoint = NSMakePoint(NSMaxX(rect), currentRowLine + [self ZoomY]);
NSPoint topLeftPoint = NSMakePoint(NSMinX(rect), currentRowLine + [self ZoomY]);
[rowLine moveToPoint:bottomLeftPoint];
[rowLine lineToPoint:bottomRightPoint];
[rowLine lineToPoint:topRightPoint];
[rowLine lineToPoint:topLeftPoint];
[rowLine closePath];
[rowLine fill];
BOOL isOctave = [[self MusicLib] IsOctave:currentTone];
if (isOctave) {
[[[self EditorDocument] ColorXGrid] set];
NSBezierPath *octaveLine = [NSBezierPath bezierPath];
NSPoint leftPoint = NSMakePoint(NSMinX(rect), currentRowLine);
NSPoint rightPoint = NSMakePoint(NSMaxX(rect), currentRowLine);
[octaveLine moveToPoint:leftPoint];
[octaveLine lineToPoint:rightPoint];
[octaveLine stroke];
}
}
// BACKGROUND MEASURES
//[[self EditorDocument].ColorYGrid setStroke];
int firstColumnLine = 0;
int currentColumnLine = 0;
int lastColumnLine = NSMaxX(rect);
int snapToValueInBeats = [[self EditorDocument] SnapToValue];
int snapToValueInPixels = floor(snapToValueInBeats * [self ZoomX]);
int measureUnitInBeats = floor([[self EditorDocument] TimeSignatureBeatsPerMeasure] * [[self EditorDocument] TimeSignatureBasicBeat]);
int measureUnitInPixels = floor(measureUnitInBeats * [self ZoomX]);
for (currentColumnLine = firstColumnLine; currentColumnLine <= lastColumnLine; currentColumnLine += snapToValueInPixels) {
//int currentBeat = floor(currentColumnLine / [self ZoomX]);
int isAMeasure = currentColumnLine % measureUnitInPixels;
int isAtSnap = currentColumnLine % snapToValueInPixels;
if ((isAMeasure == 0) || (isAtSnap == 0)) {
if (isAtSnap == 0) {
[[NSColor whiteColor] set];
}
if (isAMeasure == 0) {
[[[self EditorDocument] ColorXGrid] set];
}
NSBezierPath *columnLine = [NSBezierPath bezierPath];
NSPoint startPoint = NSMakePoint(currentColumnLine, NSMinY(rect));
NSPoint endPoint = NSMakePoint(currentColumnLine, NSMaxY(rect));
[columnLine moveToPoint:startPoint];
[columnLine lineToPoint:endPoint];
[columnLine setLineWidth:1.0];
[columnLine stroke];
} // isAMeasure or isAtSnap
} // currentColumnLine
// NOTES
for (OCNoteObject *note in [[self EditorDocument] Notes]) {
OCNoteObject *currentNote = note;
NSRect noteBounds = [self GetRectFromNote:currentNote];
//NSLog(#"noteBounds:%d", noteBounds);
// set the color for the note fill
// this will have to come from the parent Track
NSMutableArray *trackColors = [self EditorDocument].TrackColors;
if (note.Selected) {
[[trackColors objectAtIndex:0] set];
} else {
[[trackColors objectAtIndex:1] set];
}
[NSBezierPath fillRect:noteBounds];
// outline
[[NSColor blackColor] set];
[NSBezierPath strokeRect:noteBounds];
} // for each note
/*
if (EditorController.startingUpApplication == YES) {
[self setDefaultSettingForApplicationStartUp];
}
*/
//NSLog(#"OCEditorView:drawRect: END");
}
- (void)mouseDown:(NSEvent *)theEvent {
//NSLog(#"OCEditorObject:mouseDown: START");
// This converts the click into coordinates
MouseDownPoint = [self convertPoint:[theEvent locationInWindow] fromView:nil];
// Calculate the beat and pitch clicked into...
float startBeat = floor(MouseDownPoint.x / [self ZoomX]);
float pitch = floor(MouseDownPoint.y / [self ZoomY]);
float length = [[self EditorDocument] NewNoteLength];
//NSLog(#"X:%f, Y:%f", MouseDownPoint.x, MouseDownPoint.y);
//NSLog(#"beat:%f, pitch:%f", startBeat, pitch);
LastDragPoint = MouseDownPoint; // save the point just in case.
OCNoteObject *note = [self GetClickedNoteFromPoint:MouseDownPoint];
if ([EditorController EditorMode] == AddObjectMode) {
//NSLog(#"AddObjectMode)");
float snapToX = [[self EditorDocument] SnapToValue];
float snappedStartBeat = floor(startBeat / snapToX) * snapToX;
//NSLog(#"%f = %f / %f * %f", snappedStartBeat, startBeat, snapToX, snapToX);
OCNoteObject *newNote = [[self EditorDocument] CreateNote:snappedStartBeat Pitch:pitch Length:length];
//NSLog(#"newNote:%d", newNote);
[newNote Deselect];
} else if ([EditorController EditorMode] == EditObjectMode) {
//NSLog(#"EditObjectMode");
// if nothing was clicked, then clear the selections
// else if the shift key was pressed, add to the selection
if (note == nil) {
[self SelectNone];
} else {
//NSLog(#"mouseDown note.pitch:%f, oldPitch:%f", note.Pitch, note.OldPitch);
BOOL editingSelection = (([theEvent modifierFlags] & NSShiftKeyMask) ? YES : NO);
if (editingSelection) {
if (note.Selected) {
[self RemoveFromSelection:note];
} else {
[self AddToSelection:note];
}
} else {
if (note.Selected) {
// do nothing
} else {
[self SelectNone];
[self AddToSelection:note];
}
}
[self SetOldData];
} // (note == nil)
} else if ([EditorController EditorMode] == DeleteObjectMode) {
if (note != nil) {
[self RemoveFromSelection:note];
[[self EditorDocument] DestroyNote:note];
} // (note != nil)
} // EditorMode
[self setFrame:[[self EditorDocument] DocumentRect]];
[self setNeedsDisplay:YES];
}
- (void)mouseDragged:(NSEvent *)theEvent {
//NSLog(#"mouseDragged");
NSPoint currentDragPoint = [self convertPoint:[theEvent locationInWindow] fromView:nil];
// NSLog(#"currentDragPoint: %d", currentDragPoint)
float snapToValueInBeats = [[self EditorDocument] SnapToValue];
int deltaXinPixels = floor(currentDragPoint.x - MouseDownPoint.x);
int deltaYinPixels = floor(currentDragPoint.y - MouseDownPoint.y);
int deltaXinBeats = floor(deltaXinPixels / [self ZoomX]);
int deltaY = floor(deltaYinPixels / [self ZoomY]);
int deltaX = floor(deltaXinBeats / snapToValueInBeats) * snapToValueInBeats;
for (OCNoteObject *note in [self Selection]) {
[self MoveNote:note DeltaX:deltaX DeltaY:deltaY];
}
LastDragPoint = currentDragPoint;
[self autoscroll:theEvent];
[self setNeedsDisplay:YES]; //artifacts are left if this is off.
}
- (void)mouseUp:(NSEvent *)theEvent {
if ([EditorController EditorMode] == AddObjectMode) {
} else if ([EditorController EditorMode] == EditObjectMode) {
} else if ([EditorController EditorMode] == DeleteObjectMode) {
}
[self setNeedsDisplay:YES];
}
I could very well be missing something obvious, but I think I'm too close to the code to see the solution for what it is. Any help is greatly appreciated! Thanks!
I think you are misunderstanding the way drawRect: and its argument works:
The message drawRect: is sent by cocoa whenever your view or parts of it need to be redrawn. The CGRect argument is the bounding box of all updated areas for the current redraw. That means that you should not derive any positions of objects within your view from this rectangle. It is only passed to the method to allow for optimized drawing: if something is completely outside of this rectangle it does not need to be redrawn.
You should calculate all positions within your view from the views coordinate system: [self bounds]. This does not change each time drawRect: is performed and gives you an origin and size for the contents of the view.
There are a couple of other issues with your code (for instance, don't call setFrame: from within drawRect:) but I think you should first get the coordinates right and then look further into how to calculate pixel-aligned coordinates for your rectangles.
This code of yours looks rather more elaborate than it needs to be. Check out NSCenterScanRect(), and NSRectFillListWithColors(). Also, it's rather wasteful to create and discard paths in -drawRect:.