Has anyone found **legal** overrides to customize drawing of NSTabView? - drawrect

BGHUDAppKit BGHUDTabView _drawThemeTab private API override now broken
For years, I have been using code originally based off of BGHUDAppKit, and found replacements for all of the private API that BGHUDAppKit overrides.
Except for one that I could not find a way to replace...
-[NSTabView _drawThemeTab:withState:inRect:]
(Note: I also use venerable PSMTabBarControl in many circumstances, so if all else fails I'll convert all my tab views to PSMTabBarControl)
Apple has now added the dark NSAppearance in 10.14 Mojave (so in ~10 years I can use it once we stop supporting High Sierra).
Whichever selfish dev at Apple writes NSTabView does not believe in making his view customizable, unlike all of the other NSControls which are customizable.
Here is part of the hackish overrides for custom drawing of NSTabView:
// until we can eliminate private API _drawThemeTab:, return nil for new NSAppearance
- (id) appearance { return nil; }
- (id) effectiveAppearance { return nil; }
-(void)_drawThemeTab:(id) tabItem withState:(NSUInteger) state inRect:(NSRect) aRect {
NSInteger idx = [self indexOfTabViewItem: tabItem];
int gradientAngle = 90;
NSBezierPath *path = nil;
aRect = NSInsetRect(aRect, 0.5f, 0.5f);
if([self tabViewType] == NSLeftTabsBezelBorder) {
gradientAngle = 0;
} else if([self tabViewType] == NSRightTabsBezelBorder) {
gradientAngle = 180;
}
NSColor *specialFillColor = [tabItem color];
NSColor *outlineColor = nil;
NSString *name = [specialFillColor description];
// MEC - added new prefix 12/15/17 to fix white border around last segment in High Sierra
if ( [name hasPrefix:#"NSNamedColorSpace System"] || [name hasPrefix:#"Catalog color: System controlColor"])
specialFillColor = nil;
else if ( [name isEqualToString: #"NSCalibratedWhiteColorSpace 0 1"] )
[specialFillColor set];
else
{
outlineColor = specialFillColor;
specialFillColor = nil;
}
... etc ...

It's probably preferrable to completely disable NSTabView's drawing (setting its tabViewType to NSNoTabsNoBorder), and create a custom segmented bar view to draw the selection separately (as a sibling view). This allows you to completely control the appearance, layout, and sizing of that custom implementation rather than relying on any details of NSTabView.
Looking at the view hierarchy of an NSTabViewController, you can see that it has this same approach by using an NSSegmentedControl as a separate subview managing selection from the NSTabView.

Related

Obj-C and SpriteKit - Changing a sprite value that is created randomly

I'm making a game using SpriteKit and Objective-C.
I have four different texture drops (Blue, Green, Orange and Red) that falls down on screen randomly.
In my ANBDropNode class I have this method:
+(instancetype)dropOfType:(ANBDropType)type {
ANBDropsNode *drop;
if (type == ANBDropTypeBlue) {
drop = [self spriteNodeWithImageNamed:#"bluedrop"];
} else if (type == ANBDropTypeGreen) {
drop = [self spriteNodeWithImageNamed:#"greendrop"];
} else if (type == ANBDropTypeOrange) {
drop = [self spriteNodeWithImageNamed:#"orangedrop"];
} else if (type == ANBDropTypeRed){
drop = [self spriteNodeWithImageNamed:#"reddrop"];
}
[drop setupPhysicsBody];
return drop;
}
And in my GamePlayScene these two:
-(void)addDrops {
NSUInteger randomDrop = [ANBUtil randomWithMin:0 max:4];
self.drop = [ANBDropsNode dropOfType:randomDrop];
float y = self.frame.size.height + self.drop.size.height;
float x = [ANBUtil randomWithMin:10 + self.drop.size.width
max:self.frame.size.width - self.drop.size.width - 10];
self.drop.position = CGPointMake(x, y);
[self addChild:self.drop];
}
-(void)update:(NSTimeInterval)currentTime {
if (self.lastUpdateTimeInterval) {
self.timeSinceDropAdded += currentTime - self.lastUpdateTimeInterval;
}
if (self.timeSinceDropAdded > 1) {
[self addDrops];
self.timeSinceDropAdded = 0;
}
self.lastUpdateTimeInterval = currentTime;
}
The question is (and it may sound a little dumber, I know): before the drop hits the ground it has already changed it value. If ANBDropNode *drop is a bluedrop, before it hits the ground the method randomly create another drop and change it value for greendrop, for example. But I don't want this behavior. I want the drop to continue with its value until it reaches the ground so I can detect its color in my didBeginContact method.
Sorry in advance for any english mistakes, as I'm not an native english speaker.
From your question I understand that the reason you are keeping a reference to the drop (self.drop) is to check what is its colour when it hits the ground.
So you could just delete that, and create a new SKSpriteNode object each time, instead of just changing the reference of the current property.
If you have any other reason for keeping a reference to that drop, just still keep a reference.
Note that doing any of the above, will not affect the code below.
I think that you were in the right direction (when asking about didBeginContact) but took the wrong approach/mindset, since there is no need to keep a reference when using didBeginContact, because you can get the nodes in contact from this method.
Anyway, here is the code + explanations
// MyScene.m
// First, conform to SKPhysicsContactDelegate, so you can get didBeginContact 'calls'
#interface MyScene () <SKPhysicsContactDelegate>
#end
#implementation MyScene
// Now add the following constant, that you'll use as the physics body category bit masks
static const uint32_t groundCategory = 0x01 << 0;
static const uint32_t dropsCategory = 0x01 << 1;
// Somewhere in your initialisation, set yourself to be the physicsWorld
// contact delegate, so you'll receive the didBeginContact 'calls',
// And also call setupGround method, that we will create in here as well
-(id)initSceneWithSize:(CGSize)size {
...
...
self.physicsWorld.contactDelegate = self;
[self setupGround];
...
}
// Here we will create the ground node, so we can detect when a drop
// Hits the ground.
// The reason that, in the below code, I am setting just the ground,
// and not the whole borders of the screen, is because the drops
// are added above the screen 'borders', and if we would make
// a 'frame' node, and not a 'ground' node, we will also receive
// contact delegate calls, when the nodes first enter the scene
// and hits the top 'border' of the frame
-(void)setupGround {
SKNode *groundNode = [SKNode node];
groundNode.strokeColor = [SKColor clearColor];
groundNode.fillColor = [SKColor clearColor];
// Not sure if the above two are mandatory, but better be safe than sorry...
// Here we set the physics body to start at the bottom left edge of the screen
// and be the width of the screen, and the size of 1 points
// Then, we also set its category bit mask
CGRect groundRect = CGRectMake(0, 0, self.size.width, 1);
groundNode.physicsBody = [SKPhysicsBody bodyWithEdgeLoopFromRect:groundRect];
groundNode.physicsBody.categoryBitMask = groundCategory;
[self addChild:groundNode];
}
// Next we will modify your current method of creating drops, to also have
// their name property to holds the corresponding colour name
+(instancetype)dropOfType:(ANBDropType)type {
ANBDropsNode *drop;
if (type == ANBDropTypeBlue) {
drop = [self spriteNodeWithImageNamed:#"bluedrop"];
drop.name = #"Blue";
} else if (type == ANBDropTypeGreen) {
drop = [self spriteNodeWithImageNamed:#"greendrop"];
drop.name = #"Green";
} else if (type == ANBDropTypeOrange) {
drop = [self spriteNodeWithImageNamed:#"orangedrop"];
drop.name = #"Orange";
} else if (type == ANBDropTypeRed){
drop = [self spriteNodeWithImageNamed:#"reddrop"];
drop.name = #"Red";
}
[drop setupPhysicsBody];
return drop;
}
// In your setupPhysicsBody method of the drop, add the following to define
// the drop's bit mask, contact test, and collision.
// Make sure you are adding them AFTER setting the physics body, and not before.
// Since you revealed no code regarding this method, I will assume 'self' is
// referring to the drop, since you call this method on the drop.
-(void) setupPhysicsBody {
...
...
...
self.physicsBody.categoryBitMask = dropsCategory;
self.physicsBody.contactTestBitMask = groundCategory;
self.physicsBody.collisionBitMask = 0;
// The above code sets the drop category bit mask, sets its contactTestBitMask
// to be of the groundCategory, so whenever an object with drop category, will
// 'touch' and object with groundCategory, our didBeginContact delegate will
// get called.
// Also, we've set the collision bit mask to be 0, since we only want to
// be notified when a contact begins, but we don't actually want them both to
// 'collide', and therefore, have the drops 'lying' on the ground.
...
...
...
}
// Now we can set the didBeginContact: delegate method.
// Note that, as the name of the method suggests, this method gets called when a
// Contact is began, meaning, the drop is still visible on screen.
// If you would like to do whatever you want to do, when the drop leaves the screen,
// just call the didEndContact: delegate method
-(void)didBeginContact:(SKPhysicsContact *)contact {
// SKPhysicsContact have two properties, bodyA and bodyB, which represents
// the two nodes that contacted each other.
// Since there is no certain way to know which body will always be our drop,
// We will check the bodies category bit masks, to determine which is which
ANBDropsNode *drop = (contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask) ? (ANBDropsNode *)contact.bodyB.node : (ANBDropsNode *)contact.bodyA.node;
// In the above we are checking the category bit masks,
// Note that we set groundCategory to be 1, and dropsCategory to be 2,
// So we are checking which is higher.
// If bodyA bit mask is lower than bodyB bit mask, that means the bodyA is
// the ground, and bodyB is the drop, so we set the drop to be bodyB's node
// Else, we set it to be bodyA's node.
// Now you can easily get the drop colour from the name property we've set in
// the beginning. you can do some sort of if-else statement, that check
// 'if isEqualToString'. Here I just NSLog the colour
NSLog(#"%#", drop.name);
}
Good luck mate.
You can simply associate a property with the ANBDropsNode class which can be set when the drop is instantiated.
In the ANBDropsNode.h file,
#interface ANBDropsNode
#property (strong, nonatomic) NSString *dropColor; //This property will hold the value associated with the color.
Then in the dropOfType class method:
+(instancetype)dropOfType:(ANBDropType)type {
NSString *strDropColor;
if (type == ANBDropTypeBlue) {
strDropColor = #"bluedrop";
} else if (type == ANBDropTypeGreen) {
strDropColor = #"greendrop";
} else if (type == ANBDropTypeOrange) {
strDropColor = #"orangedrop";
} else if (type == ANBDropTypeRed){
strDropColor = #"reddrop";
}
ANBDropsNode *drop = [self spriteNodeWithImageNamed:strDropColor];
drop.dropColor = strDropColor;
[drop setupPhysicsBody];
return drop;
}
Now, in your collision detection delegate method, you can find out the color of the node by simply referring the dropColor property.

Showing a button when all 'enemy' ccsprites have been removed from scene

I am using SpriteBuilder to make a game. The objective is to destroy some CCSprites. I have 3 sprites on screen and are destroyed by another sprite, so the code must have something to do with when there are no more 'enemy' sprites remaining a next button must show. I have looked on the internet and are inexperienced with Cocos2D coding. Here is the code I have used to get rid of the 'enemy'
-(void)ccPhysicsCollisionPostSolve:(CCPhysicsCollisionPair *)pair danald:(CCNode *)nodeA wildcard:(CCNode *)nodeB {
float energy = [pair totalKineticEnergy];
if (energy > 5000.f) {
[self danaldRemoved:nodeA];
}
}
If the object is hit with a certain speed it will call the method below
- (void)danaldRemoved:(CCNode *)Danald {
CCParticleSystem *explosion = (CCParticleSystem *)[CCBReader load:#"Explosion"];
explosion.autoRemoveOnFinish = TRUE;
explosion.position = Danald.position;
[Danald.parent addChild:explosion];
[Danald removeFromParent];
}
Thanks in an advanced, sorry if this question has been asked before but I cannot find it
Well I would suggest this method:
Create a variable where you store the number of sprites left. For example:
int spritesLeft;
And then initialize it to 0:
-(void) didLoadFromCCB{
//REST OF CODE
spritesLeft=3; //3 because you said there are only 3.
}
Now when you call danaldRemoved: method, just subtract 1 to spritesLeft, and check if spritesLeft is equal to 0. If it's true, just call your method to make a button appear:
- (void)danaldRemoved:(CCNode *)Danald {
spritesLeft--; //substract 1
CCParticleSystem *explosion = (CCParticleSystem *)[CCBReader load:#"Explosion"];
explosion.autoRemoveOnFinish = TRUE;
explosion.position = Danald.position;
[Danald.parent addChild:explosion];
[Danald removeFromParent];
//check if game is over.
if (spritesLeft == 0){
[self printButton];
}
}
Now create the method printButton, but before go to SpriteBuilder, create the button and place it where you want. Now uncheck 'Visible' value, and then go to code connections, and select 'Doc root var' (under custom class) and write a name for the button, for example: nextButton. At the selector value write: changeLevel and target: document root
Now declare it at the top of your .m file as you did with any other objects:
CCButton *nextButton;
Method for button (just set visibility ON)
-(void) printButton{
nextButton.visible = YES;
}
And now your method to change level:
-(void) changeLevel{
CCScene *nextLevel = [CCBReader loadAsScene:#"YOUR LEVEL"];
[[CCDirector sharedDirector] replaceScene:nextLevel];
}
Hope this helps!
EDIT: HOW TO DETECT WHEN A SPRITE GOES OFF THE SCREEN
As I said, create any kind of physic object in spritebuilder. For example, I use CCNodeColor. Then make it a rectangle and place it at left of the screen. Now go to physics, enable physics, polygon type and static. Now in connections, select doc root var and call it _leftNode. Now repeat with top,right and bottom and call them _topNode, etc.
Now go to code, declare your new nodes: CCNode *_leftNode; and so...
Now let's make a collision type:
_bottomNode.physicsBody.collisionType = #"_bound";
_leftNode.physicsBody.collisionType = #"_bound";
_rightNode.physicsBody.collisionType = #"_bound";
_topNode.physicsBody.collisionType = #"_bound";
And do the same with your sprite, but I think you have done that before. Let's make an example:
spritename.physicsBody.collisionType = #"_sprite";
So now implement the method:
-(void)ccPhysicsCollisionPostSolve:(CCPhysicsCollisionPair *)pair _sprite:(CCNode *)nodeA _bound:(CCNode *)nodeB {
[_physicsNode removeChild:nodeA cleanup:YES];
}
And that's all.

Eliminate Pin Overlap in the MKMapView

I am working with MKMapView Based application. I need a clarification whether it is possible to eliminate the pin OverLap in the MKMapView? Because at some places there are large number of pins displaying. It is difficult to me to identify the pins.
If you have an Apple Developer Account, I would strongly recommend getting the Session 111 video from the 2011 WWDC Conference Sessions, entitled "Visualizing Information Geographically with MapKit". One of the segments specifically covers how to cluster content from large data sets to allow you to group or ungroup pins based on density at various zoom levels.
Their example is elegantly simple, but at the heart of the problem you want to replace a group of overlapping pins with a single pin and as you zoom in the single pin will split back into the individual pins.
How and when you decide to group things can be varied considerably. Apple's solution simply subdivides the map into a grid and any box that has more than 1 pin results in a group. You could also take an algorithmic approach such as using a kMeansCluster algorithm which is incredibly simple and you could feed all of your annotations through the algorithm and get an array of groups out the other side logically organized.
From there it's a matter of keeping track of all the individual pins and how they are grouped as you zoom in and out. You will only display a single annotation for each group or any individual pins that are left over. It's also possible to animate the transitions as the map zooms in and out so you can visually reinforce what is happening.
My own technique is too closely related to Apple's approach for me to post here so I'm hoping you can access the above video which covers almost all of these points.
For this you have to implement clustering concept to your map.By using Apple demo code it's easy to implement clustering concept in our code. Reference link
Simply we can use following code for the Clustering
Steps to implement clustering
Step1 : The important thing is for clustering we use two mapviews(allAnnotationsMapView, ), One is for reference(allAnnotationsMapView).
#property (nonatomic, strong) MKMapView *allAnnotationsMapView;
#property (nonatomic, strong) IBOutlet MKMapView *mapView;
In viewDidLoad
_allAnnotationsMapView = [[MKMapView alloc] initWithFrame:CGRectZero];
Step2 : Add all annotations to the _allAnnotationsMapView, In below _photos are the annotations array.
[_allAnnotationsMapView addAnnotations:_photos];
[self updateVisibleAnnotations];
Step3 : Add below methods for clustering, in this PhotoAnnotation is the custom annotation.
MapViewDelegate methods
- (void)mapView:(MKMapView *)aMapView regionDidChangeAnimated:(BOOL)animated {
[self updateVisibleAnnotations];
}
- (void)mapView:(MKMapView *)aMapView didAddAnnotationViews:(NSArray *)views {
for (MKAnnotationView *annotationView in views) {
if (![annotationView.annotation isKindOfClass:[PhotoAnnotation class]]) {
continue;
}
PhotoAnnotation *annotation = (PhotoAnnotation *)annotationView.annotation;
if (annotation.clusterAnnotation != nil) {
// animate the annotation from it's old container's coordinate, to its actual coordinate
CLLocationCoordinate2D actualCoordinate = annotation.coordinate;
CLLocationCoordinate2D containerCoordinate = annotation.clusterAnnotation.coordinate;
// since it's displayed on the map, it is no longer contained by another annotation,
// (We couldn't reset this in -updateVisibleAnnotations because we needed the reference to it here
// to get the containerCoordinate)
annotation.clusterAnnotation = nil;
annotation.coordinate = containerCoordinate;
[UIView animateWithDuration:0.3 animations:^{
annotation.coordinate = actualCoordinate;
}];
}
}
}
clustering Handling methods
- (id<MKAnnotation>)annotationInGrid:(MKMapRect)gridMapRect usingAnnotations:(NSSet *)annotations {
// first, see if one of the annotations we were already showing is in this mapRect
NSSet *visibleAnnotationsInBucket = [self.mapView annotationsInMapRect:gridMapRect];
NSSet *annotationsForGridSet = [annotations objectsPassingTest:^BOOL(id obj, BOOL *stop) {
BOOL returnValue = ([visibleAnnotationsInBucket containsObject:obj]);
if (returnValue)
{
*stop = YES;
}
return returnValue;
}];
if (annotationsForGridSet.count != 0) {
return [annotationsForGridSet anyObject];
}
// otherwise, sort the annotations based on their distance from the center of the grid square,
// then choose the one closest to the center to show
MKMapPoint centerMapPoint = MKMapPointMake(MKMapRectGetMidX(gridMapRect), MKMapRectGetMidY(gridMapRect));
NSArray *sortedAnnotations = [[annotations allObjects] sortedArrayUsingComparator:^(id obj1, id obj2) {
MKMapPoint mapPoint1 = MKMapPointForCoordinate(((id<MKAnnotation>)obj1).coordinate);
MKMapPoint mapPoint2 = MKMapPointForCoordinate(((id<MKAnnotation>)obj2).coordinate);
CLLocationDistance distance1 = MKMetersBetweenMapPoints(mapPoint1, centerMapPoint);
CLLocationDistance distance2 = MKMetersBetweenMapPoints(mapPoint2, centerMapPoint);
if (distance1 < distance2) {
return NSOrderedAscending;
} else if (distance1 > distance2) {
return NSOrderedDescending;
}
return NSOrderedSame;
}];
PhotoAnnotation *photoAnn = sortedAnnotations[0];
NSLog(#"lat long %f %f", photoAnn.coordinate.latitude, photoAnn.coordinate.longitude);
return sortedAnnotations[0];
}
- (void)updateVisibleAnnotations {
// This value to controls the number of off screen annotations are displayed.
// A bigger number means more annotations, less chance of seeing annotation views pop in but decreased performance.
// A smaller number means fewer annotations, more chance of seeing annotation views pop in but better performance.
static float marginFactor = 2.0;
// Adjust this roughly based on the dimensions of your annotations views.
// Bigger numbers more aggressively coalesce annotations (fewer annotations displayed but better performance).
// Numbers too small result in overlapping annotations views and too many annotations on screen.
static float bucketSize = 60.0;
// find all the annotations in the visible area + a wide margin to avoid popping annotation views in and out while panning the map.
MKMapRect visibleMapRect = [self.mapView visibleMapRect];
MKMapRect adjustedVisibleMapRect = MKMapRectInset(visibleMapRect, -marginFactor * visibleMapRect.size.width, -marginFactor * visibleMapRect.size.height);
// determine how wide each bucket will be, as a MKMapRect square
CLLocationCoordinate2D leftCoordinate = [self.mapView convertPoint:CGPointZero toCoordinateFromView:self.view];
CLLocationCoordinate2D rightCoordinate = [self.mapView convertPoint:CGPointMake(bucketSize, 0) toCoordinateFromView:self.view];
double gridSize = MKMapPointForCoordinate(rightCoordinate).x - MKMapPointForCoordinate(leftCoordinate).x;
MKMapRect gridMapRect = MKMapRectMake(0, 0, gridSize, gridSize);
// condense annotations, with a padding of two squares, around the visibleMapRect
double startX = floor(MKMapRectGetMinX(adjustedVisibleMapRect) / gridSize) * gridSize;
double startY = floor(MKMapRectGetMinY(adjustedVisibleMapRect) / gridSize) * gridSize;
double endX = floor(MKMapRectGetMaxX(adjustedVisibleMapRect) / gridSize) * gridSize;
double endY = floor(MKMapRectGetMaxY(adjustedVisibleMapRect) / gridSize) * gridSize;
// for each square in our grid, pick one annotation to show
gridMapRect.origin.y = startY;
while (MKMapRectGetMinY(gridMapRect) <= endY) {
gridMapRect.origin.x = startX;
while (MKMapRectGetMinX(gridMapRect) <= endX) {
NSSet *allAnnotationsInBucket = [self.allAnnotationsMapView annotationsInMapRect:gridMapRect];
NSSet *visibleAnnotationsInBucket = [self.mapView annotationsInMapRect:gridMapRect];
// we only care about PhotoAnnotations
NSMutableSet *filteredAnnotationsInBucket = [[allAnnotationsInBucket objectsPassingTest:^BOOL(id obj, BOOL *stop) {
return ([obj isKindOfClass:[PhotoAnnotation class]]);
}] mutableCopy];
if (filteredAnnotationsInBucket.count > 0) {
PhotoAnnotation *annotationForGrid = (PhotoAnnotation *)[self annotationInGrid:gridMapRect usingAnnotations:filteredAnnotationsInBucket];
[filteredAnnotationsInBucket removeObject:annotationForGrid];
// give the annotationForGrid a reference to all the annotations it will represent
annotationForGrid.containedAnnotations = [filteredAnnotationsInBucket allObjects];
[self.mapView addAnnotation:annotationForGrid];
for (PhotoAnnotation *annotation in filteredAnnotationsInBucket) {
// give all the other annotations a reference to the one which is representing them
annotation.clusterAnnotation = annotationForGrid;
annotation.containedAnnotations = nil;
// remove annotations which we've decided to cluster
if ([visibleAnnotationsInBucket containsObject:annotation]) {
CLLocationCoordinate2D actualCoordinate = annotation.coordinate;
[UIView animateWithDuration:0.3 animations:^{
annotation.coordinate = annotation.clusterAnnotation.coordinate;
} completion:^(BOOL finished) {
annotation.coordinate = actualCoordinate;
[self.mapView removeAnnotation:annotation];
}];
}
}
}
gridMapRect.origin.x += gridSize;
}
gridMapRect.origin.y += gridSize;
}
}
By following above steps we can achieve clustering on mapview, it is not necessary to use any third party code or framework. Please check the Apple sample code here. Please let me know if you have any doubts on this.
It's quite easy to implement your own annotation clustering framework. Here's an example of a basic one that you can refer here.
If your pins are overlapping then it must be that your zoom level is high for that place.
You can think of removing some annotations in that zoom level until you dont have annotation overlaps and while zooming in you can add the annotations so that there are enough space between the annotations.

Best way to implement multiple levels within Objective-C roguelike?

I'm working on a roguelike using Objective-C/Cocoa to learn more. I've gotten most of the basic functionality out of the way, but I still have one problem I've been trying to figure out.
Here's a breakdown of the process:
First, the map is loaded:
NSString* mapPath = [[NSBundle mainBundle] pathForResource:mapFileName ofType:mapFileType];
NSURL* mapURL = [NSURL fileURLWithPath: mapPath];
currentMap_ = [[Map alloc] initWithContentsOfURL: mapURL];
worldArray = [[NSMutableArray alloc] init];
itemArray = [[NSMutableArray alloc] init];
[self populateMap];
return;
Then, in the populateMap function, it goes through each cell of the loaded map, using NSPoints and a loop, and creates objects based on the data from the map in WorldArray. For items, normal floor is put in where the item is, and an item is then made in itemArray. Both arrays are 30x30, as determined by the height of the map.
Here is the populateMap code:
- (void)populateMap
{
NSPoint location;
for ( location.y = 0; location.y < [currentMap_ height]; location.y++ )
{
for ( location.x = 0; location.x < [currentMap_ width]; location.x++ )
{
char mapData = [currentMap_ dataAtLocation: location];
for ( GameObject *thisObject in worldDictionary )
{
//NSLog(#"char: <%c>", [thisObject single]);
if ( mapData == [thisObject single])
{
NSString* world = [thisObject className];
//NSLog(#"(%#) object created",thisObject);
[self spawnObject:world atLocation:location];
}
}
for ( Item *thisObject in itemDictionary )
{
//NSLog(#"char: <%c>", [thisObject single]);
if ( mapData == [thisObject single] )
{
NSString* item = [thisObject className];
NSString* floor = [NormalFloor className];
//NSLog(#"(%#) object created",thisObject);
[self spawnItem:item atLocation:location];
[self spawnObject:floor atLocation:location];
}
}
if ( mapData == '1'
&& [player_ stepsTaken] <= 0)
{
//NSLog(#"player spawned at (%f, %f)",location.x,location.y);
player_ = [[Player alloc] initAtLocation: location];
}
if ( mapData == '1' )
{
//NSLog(#"floor created at (%f, %f)",location.x,location.y);
[worldArray addObject:[[NormalFloor alloc] initAtLocation: location]];
}
}
}
[self setNeedsDisplay:YES];
}
This is what is called when things are spawned:
- (void)spawnObject: (NSString*) object atLocation: (NSPoint) location
{
//NSLog(#"(%#) object created",thisObject);
[worldArray addObject:[[NSClassFromString(object) alloc] initAtLocation: location]];
}
- (void)spawnItem: (NSString*) item atLocation: (NSPoint) location
{
//NSLog(#"(%#) object created",thisObject);
[itemArray addObject:[[NSClassFromString(item) alloc] initAtLocation: location]];
}
worldArray and itemArray are what the game works on from that moment onwards, including the drawing. The player is inside of worldArray as well. I'm considering splitting the player into another array of characterArray, to make it easier when I add things like monsters in the not so distant future.
Now, when I load a new level, I had first considered methods like saving them to data and loading them later, or some sort of savestate function. Then I came to the realization that I would need to be able to get to everything at the same time, because things can still happen outside of the player's current scope, including being chased by monsters for multiple floors, and random teleports. So basically, I need to figure out a good way to store worldArray and itemArray in a way that I will be able to have levels of them, starting from 0 and going onward. I do need a savestate function, but there's no point touching that until I have this done, as you shouldn't actually be allowed to save your game in roguelikes.
So to reiterate, I need to have one set of these arrays per level, and I need to store them in a way that is easy for me to use. A system of numbers going from 0-upward are fine, but if I could use something more descriptive like a map name, that would be much better in the long run.
I've figured out my problem, I'm using an NSMutableDictionary for each and storing them with the keys that correspond to each level. Works like a charm. Bigger problems elsewhere now.
I figured it out, I'm using NSMutableDictionaries, one for each array (objects, items, eventually characters). They're stored using the name of the level. Works like a charm.

Wrap NSButton title

Any way to have a NSButton title to wrap when it's width is longer than the button width, instead of getting clipped?
I'm trying to have a radio button with a text that can be long and have multiple lines. One way I thought about having it work is to have an NSButton of type NSRadioButton but can't get multiple lines of text to work.
Maybe my best alternative is to have an NSButton followed by an NSTextView with the mouseDown delegate function on it triggering the NSButton state?
I don't believe you can. You'd have to subclass NSButtonCell to add support for this.
That said, it's typically a bad idea to have multiple lines of text on a button. A button label should concisely represent the action performed:
The label on a push button should be a verb or verb phrase that describes the action it performs—Save, Close, Print, Delete, Change Password, and so on. If a push button acts on a single setting, label the button as specifically as possible; “Choose Picture…,” for example, is more helpful than “Choose…” Because buttons initiate an immediate action, it shouldn’t be necessary to use “now” (Scan Now, for example) in the label.
What are you trying to do?
I`m incredibly late, but I still feel obliged to share what I`ve found.
Just add a newline character before and after the button title before you assign it to the actual button — and voilà! It now wraps automatically.
The downside of this approach is that, for reasons unknown to me, apps compiled on a certain version of OS X shift button titles one line down when run on newer versions.
Well here's my excuse for needing multiline buttons: I'm writing an emulator for an IBM 701, complete with front panel, and, bless their hearts, the designers of that front panel used multi-line labels. Here's my code. You only have to subclass NSButtonCell (not NSButton), and only one method needs to be overridden.
// In Xcode 4.6 (don't know about earlier versions): Place NSButton, then double-click it
// and change class NSButtonCell to ButtonMultiLineCell.
#interface ButtonMultiLineCell : NSButtonCell
#end
#implementation ButtonMultiLineCell
- (NSRect)drawTitle:(NSAttributedString *)title withFrame:(NSRect)frame inView:(NSView *)controlView
{
NSAttributedString *as = [[NSAttributedString alloc] initWithString:[title.string stringByReplacingOccurrencesOfString:#" " withString:#"\n"]];
NSFont *sysFont = [NSFont systemFontOfSize:10];
NSMutableParagraphStyle *paragraphStyle = [[[NSParagraphStyle defaultParagraphStyle] mutableCopy] autorelease];
[paragraphStyle setAlignment:NSCenterTextAlignment];
NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys:
sysFont, NSFontAttributeName,
paragraphStyle, NSParagraphStyleAttributeName,
nil];
NSSize textSize = [as.string sizeWithAttributes:attributes];
NSRect textBounds = NSMakeRect(0, 0, textSize.width, textSize.height);
// using frame argument seems to produce text in wrong place
NSRect f = NSMakeRect(0, (controlView.frame.size.height - textSize.height) / 2, controlView.frame.size.width, textSize.height);
[as.string drawInRect:f withAttributes:attributes];
return textBounds; // not sure what rectangle to return or what is done with it
}
#end
Even later, but I also feel obliged to share. You can set the attributedTitle property of NSButton to achieve manual wrapping.
In my case, I wanted the button title to wrap if it was greater than 6 characters (Swift 3):
if button.title.characters.count > 6 {
var wrappedTitle = button.title
wrappedTitle.insert("\n", at: wrappedTitle.index(wrappedTitle.startIndex, offsetBy: 6))
let style = NSMutableParagraphStyle()
style.alignment = .center
let attributes = [NSFontAttributeName: NSFont.systemFont(ofSize: 19), NSParagraphStyleAttributeName: style] as [String : Any]
button.attributedTitle = NSAttributedString(string: wrappedTitle, attributes: attributes)
}
I'm with Sören; If you need a longer description, think about using a tool tip or placing descriptive text in a wrapped text field using the small system font below the radio choices if the descriptive text is only a few lines. Otherwise, you could provide more information in a help document.
Figuring out a way to say what you need to say in a concise way is your best bet, though.
As of today, I'm seeing this can be done simply with a property on the cell of NSButton:
myButton.cell?.wraps = true
I had the same problem and tried, with a sinking heart, the solutions in this post. (While I appreciate advice that one generally should keep button titles short, I'm writing a game, and I want multi-line answers to behave like buttons).
Sometimes, you don't get there from here. My ideal was an NSButton with a multi-line label, but since I can't get that without considerable hassle, I have created a PseudoButton: an NSControl subclass that behaves like a button. It has a hand cursor to indicate 'you can click here' and it gives feedback: when you click the mouse, it changes to selectedControlColor, when you release the mouse, it returns to normal. And unlike solutions that try to stack buttons and labels, there is no problem with having labels and images on top of the view: the whole of the view is the clickable area.
import Cocoa
#IBDesignable
class PseudoButton: NSControl {
#IBInspectable var backgroundColor: NSColor = NSColor.white{
didSet{
self.needsDisplay = true
}
}
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
let path = NSBezierPath(rect: dirtyRect)
backgroundColor.setFill()
path.fill()
NSColor.black.setStroke()
path.lineWidth = 2
path.stroke()
}
override func mouseDown(with event: NSEvent) {
self.backgroundColor = NSColor.selectedControlColor
}
override func mouseUp(with event: NSEvent) {
self.backgroundColor = NSColor.clear
guard let action = action else {return}
tryToPerform(action, with: self)
//#IBAction func pseudobuttonClicked(_ sender: PseudoButton) in the ViewController class
}
override func resetCursorRects() {
addCursorRect(bounds, cursor: .pointingHand)
}
}
You use this like any other control in the storyboard: drag a Pseudobutton in, decorate it at will, and connect it to an appropriate IBAction in your viewController class.
I like this better than meddling with NSCell. (On past experience, NSCell-based hacks are more likely to break).
A little bit late here, here's my code to insert new line in title:
private func calculateMultipleLineTitle(_ title: String) -> String {
guard !title.isEmpty else { return title }
guard let cell = cell as? NSButtonCell else { return title }
let titleRect = cell.titleRect(forBounds: bounds)
let attr = attributedTitle.attributes(at: 0, effectiveRange: nil)
let indent = (attr[.paragraphStyle] as? NSMutableParagraphStyle)?.firstLineHeadIndent ?? 0
let titleTokenArray = title.components(separatedBy: " ") // word wrap break mode
guard !titleTokenArray.isEmpty else { return title }
var multipleLineTitle = titleTokenArray[0]
var multipleLineAttrTitle = NSMutableAttributedString(string: multipleLineTitle, attributes: attr)
var index = 1
while index < titleTokenArray.count {
multipleLineAttrTitle = NSMutableAttributedString(
string: multipleLineTitle + " " + titleTokenArray[index],
attributes: attr
)
if titleRect.minX+indent+multipleLineAttrTitle.size().width > bounds.width {
multipleLineTitle += " \n" + titleTokenArray[index]
} else {
multipleLineTitle += " " + titleTokenArray[index]
}
index += 1
}
return multipleLineTitle
}
Just pass the original title as parameter, it will return multiple line title.
I added an "\n" at the end of the title and I am setting the title using the NSAttributedString. this fixed the problem for me.
I am on MacOS Big Sur 11.7.2, Xcode 13.12.1
private NSAttributedString GetAttributedString(string text)
{
var paragraph = new NSMutableParagraphStyle();
paragraph.Alignment = NSTextAlignment.Center;
paragraph.LineBreakMode = NSLineBreakMode.ByWordWrapping;
var attrString = new NSAttributedString
(
text + "\n",
font: NSFont.FromFontName("Arial", 50.0f),
foregroundColor: NSColor.White,
backgroundColor: NSColor.FromCalibratedRgba(0, 0, 0, 0.0f),
paragraphStyle: paragraph
);
return attrString;
}
textButton.AttributedTitle = GetAttributedString("some text");