HowTo: Disappearing NSTable Buttons

Posted: August 10th, 2010 | Author: Remi | Filed under: Cocoa, OS X, Programming | No Comments »
Disappearing NSTable Buttons

Completed Project

Recently I was working on a project where I wanted a certain effect. This was that I wanted the user to be able to delete items out of a table by clicking on the item and not having to add some button somewhere else to do it. So the answer was to put a button on the table to delete it. That was simple enough, just make a table with button cells, but it didn’t look very nice, it showed all the button when you could only delete one at a time. So thus, I decided to make the buttons only appear on the selected table items. Its simple, functional, and you can do it too if you just follow this short guide.

To tackle this problem, there are two “hacks” that we need to do.  First, we have to somehow make the NSButtonCell hide itself.  Next, we have to keep track of which row is selected, and tie that information back to our buttons. To accomplish the first task, will tie the button being hidden to it’s enabled value, since this way is easier then creating a new binding, and it doesn’t matter is the button is disabled if it’s hidden anyway.  Subclass NSButtonCell and override this method with this code.

- (void) drawWithFrame: (NSRect)cellFrame inView: (NSView*)controlView
{
	if([self isEnabled])
		[super drawWithFrame:cellFrame inView:controlView];
}

In interface builder, drag a NSButtonCell to one of the columns of your NSTableView. (Apple hides it as a ‘Check Box Cell’, see below, you have to change the type to ‘Momentary Push In’).

Interface Builder Check Box Cell (NSButonCell)

Check Box Cell (NSButtonCell)

Setting the Class to HidingButtonCell

Setting the Class to HidingButtonCell

Change the class to your custom subclass. Next we need to somehow get the data in a form we can bind to the button’s enabled key.  There are potentially a couple ways to do this, but I decided to subclass NSTableView, and override this method.

- (void)selectRowIndexes:(NSIndexSet *)indexes byExtendingSelection:(BOOL)extend
{
	[super selectRowIndexes:indexes byExtendingSelection:extend];
	[controller setSelectionIndex:[indexes firstIndex]];
 
	int i;
	for(i = 0; i < [[controller arrangedObjects] count]; i++)
	{
		[[[controller arrangedObjects] objectAtIndex:i] setObject:
			[NSNumber numberWithInt:[indexes firstIndex] == i] forKey:@"enabled"];
	}
 
}

Of, course this way of doing this assumes you’re binding to an NSArray of NSMutableDictionaries.  Next, back in Interface Builder we can bind the column to the new data that’s going to be generated with that method. And thats it enjoy you’re disappearing buttons.  If you want the button to delete the row, like mine, just connect the NSButtonCell’s action to the NSArrayController’s remove: method. If I made any mistakes, or made anything unclear, please leave a comment.  For a closer look, Download the Xcode Project


Remi’s Chat App

Posted: July 18th, 2010 | Author: Remi | Filed under: Cocoa, Programming, iPhone | No Comments »

As promised, here is some example code for the iPhone using ThoMoNetworking. I call it Remi’s Chat App. The mobile companion to Remi’s Chat Program.


Sorry it took me so long to put it up.  Some things to note: It interfaces with Remi’s Chat Program in everything except that iOS doesn’t support the rich text.  You may notice if you download the project that I created a file called NSPatches, this was to allow for the iOS to properly unarchive some classes that it doesn’t have, most notably NSFont.  Other then that, from what I can tell ThoMoNetworking works flawlessly on iOS.  The only thing I couldn’t test was from iOS to iOS, since I lacked two iOS devices, but from iOS to OS X worked great, so I would assume it would work fine.  So here is the bulk of the code for you to peruse, and you can download the project underneath, enjoy!

P.S. Let me know if anybody runs into any issues with the code, I may be able to resolve them easily, since some crashes might just be due to iOS not having certain clases that OS X has, which a simple appending to the NSPatches file should fix.

//
//  Remi_s_Chat_AppViewController.m
//  Remi's Chat App
//
//  Created by Remi Bernotavicius on 7/18/10.
//  Copyright __MyCompanyName__ 2010. All rights reserved.
//
 
#import "Remi_s_Chat_AppViewController.h"
 
@implementation Remi_s_Chat_AppViewController
 
/*
// The designated initializer. Override to perform setup that is required before the view is loaded.
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
    if ((self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil])) {
        // Custom initialization
    }
    return self;
}
*/
 
/*
// Implement loadView to create a view hierarchy programmatically, without using a nib.
- (void)loadView {
}
*/
 
// Implement viewDidLoad to do additional setup after loading the view, typically from a nib.
- (void)viewDidLoad {
    [super viewDidLoad];
 
	myServer = [[ThoMoServerStub alloc] initWithProtocolIdentifier:@"remischat"];
	[myServer setDelegate:self];
	[myServer start];
 
	myClient = [[ThoMoClientStub alloc] initWithProtocolIdentifier:@"remischat"];
	[myClient setDelegate:self];
	[myClient start];
 
	[[UIApplication sharedApplication] setDelegate:self];
}
 
- (void)applicationWillTerminate:(UIApplication *)application
{
	NSMutableAttributedString* temp = [[[NSMutableAttributedString alloc] initWithString:[NSFullUserName() stringByAppendingString:@" disconnected"]] autorelease];
	[temp appendAttributedString:[[[NSAttributedString alloc] initWithString:@"\n"] autorelease]];
	[myServer sendToAllClients:temp];
}
 
- (void)server:(ThoMoServerStub *)theServer acceptedConnectionFromClient:(NSString *)aClientIdString;
{
	NSMutableAttributedString* temp = [[[NSMutableAttributedString alloc] initWithString:[NSFullUserName() stringByAppendingString:@" connected"]] autorelease];
	[temp appendAttributedString:[[[NSAttributedString alloc] initWithString:@"\n"] autorelease]];
	[myServer send:temp toClient:aClientIdString];
}
 
/*
// Override to allow orientations other than the default portrait orientation.
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
    // Return YES for supported orientations
    return (interfaceOrientation == UIInterfaceOrientationPortrait);
}
*/
 
- (void)didReceiveMemoryWarning {
	// Releases the view if it doesn't have a superview.
    [super didReceiveMemoryWarning];
 
	// Release any cached data, images, etc that aren't in use.
}
 
- (void)viewDidUnload {
	// Release any retained subviews of the main view.
	// e.g. self.myOutlet = nil;
}
 
- (void)textFieldDidBeginEditing:(UITextField *)textField
{
	CGRect newFrame = self.view.frame;
	if ([[UIApplication sharedApplication] statusBarOrientation]== UIInterfaceOrientationPortrait
		|| [[UIApplication sharedApplication] statusBarOrientation] == UIInterfaceOrientationPortraitUpsideDown)
		newFrame.size.height -= 216;
	else
		newFrame.size.height -= 162;
 
    [UIView beginAnimations:nil context:NULL];
    [UIView setAnimationBeginsFromCurrentState:YES];
    [UIView setAnimationDuration:0.3];
 
    [self.view setFrame:newFrame];
 
    [UIView commitAnimations];
}
 
-(IBAction)sendMessage:(id)sender
{
	NSMutableAttributedString* temp = [[[NSMutableAttributedString alloc] initWithString:[NSFullUserName() stringByAppendingString:@": "]] autorelease];
	[temp appendAttributedString:[[[NSMutableAttributedString alloc] initWithString:toSend.text] autorelease]];
	[temp appendAttributedString:[[[NSMutableAttributedString alloc] initWithString:@"\n"] autorelease]];
 
	[myServer sendToAllClients:temp];
	toSend.text = @"";
}
 
- (void)textFieldDidEndEditing:(UITextField *)textField
{
	CGRect newFrame = self.view.frame;
	if ([[UIApplication sharedApplication] statusBarOrientation]== UIInterfaceOrientationPortrait
		|| [[UIApplication sharedApplication] statusBarOrientation] == UIInterfaceOrientationPortraitUpsideDown)
		newFrame.size.height += 216;
	else
		newFrame.size.height += 162;
 
    [UIView beginAnimations:nil context:NULL];
    [UIView setAnimationBeginsFromCurrentState:YES];
    [UIView setAnimationDuration:0.3];
 
    [self.view setFrame:newFrame];
 
    [UIView commitAnimations];
}
 
-(void)server:(ThoMoServerStub *)theServer didReceiveData:(id)theData fromClient:(NSString *)aClientIdString {}
 
-(void)client:(ThoMoClientStub *)theClient didReceiveData:(id)theData fromServer:(NSString *)aServerIdString;
{
	NSMutableAttributedString* temp = [[[NSMutableAttributedString alloc] initWithAttributedString:theData] autorelease];
	history.text = [history.text stringByAppendingString:[temp string]];
 
	[history scrollRangeToVisible:NSMakeRange([history.text length]-2, 1)];
}
 
- (void)dealloc {
    [super dealloc];
}
 
@end

Download the Xcode Project


ThoMoNetworking Framework

Posted: June 8th, 2010 | Author: Remi | Filed under: Cocoa, Programming | 2 Comments »

I happened to stumble upon an amazing framework called ThoMoNetworking. It takes the complicated task of networking in Cocoa and makes it extremely easy. Basically it lets you define your own protocol and connects to other instances of it on the network automatically using bonjour. Then to send data it allows you to send objects. The potential from this framework is huge, especially since it works on both iPhone OS and Mac OS.  It is incredibly simple, check out their website for how to use it, it is outlined in its full simplistic beauty.

With such a powerful framework at my disposal, I decided to test it out.  I wanted to see just how easy it would be to create my own ad-hoc chatting program.  I’ve included the source as well so you can test out the framework for yourselves.

I give you, Remi’s Chat Program

Remi's Chat Program

Since ThoMoNetworking lets you send any object, I was able to send NSAttributedString objects, so the text’s color and font are preserved.  I was also able to include recalling previously sent messages with the arrow keys (like in the terminal) and growl support as a superfluous features.  This seemingly complicated program is actually very little code.

//
//  Remi_s_Chat_ProgramAppDelegate.m
//  Remi's Chat Program
//
//  Created by Remi Bernotavicius on 6/1/10.
//  Copyright 2010 __MyCompanyName__. All rights reserved.
//
 
#import "Remi_s_Chat_ProgramAppDelegate.h"
 
@implementation Remi_s_Chat_ProgramAppDelegate
 
- (NSDictionary *) registrationDictionaryForGrowl {
	NSArray *array = [NSArray arrayWithObjects:@"new message", nil];
    NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
                          [NSNumber numberWithInt:1],
                          @"TicketVersion",
                          array,
                          @"AllNotifications",
                          array,
                          @"DefaultNotifications",
                          nil];
    return dict;
}
 
-(void) growlAlert:(NSString *)message title:(NSString *)title{
    [GrowlApplicationBridge notifyWithTitle:title
								description:message
						   notificationName:@"new message"
								   iconData:[[[NSApplication sharedApplication] applicationIconImage] TIFFRepresentation]
								   priority:0
								   isSticky:NO
							   clickContext:@"bringFrontContext"];
}
 
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
	[GrowlApplicationBridge setGrowlDelegate:self];
 
	myServer = [[ThoMoServerStub alloc] initWithProtocolIdentifier:@"remischat"];
	[myServer setDelegate:self];
	[myServer start];
 
	myClient = [[ThoMoClientStub alloc] initWithProtocolIdentifier:@"remischat"];
	[myClient setDelegate:self];
	[myClient start];
 
	[[NSApplication sharedApplication] setDelegate:self];
	previousChats = [[NSMutableArray alloc] init];
	current = 0;
}
 
- (void)server:(ThoMoServerStub *)theServer acceptedConnectionFromClient:(NSString *)aClientIdString;
{
	NSMutableAttributedString* temp = [[[NSMutableAttributedString alloc] initWithString:[NSFullUserName() stringByAppendingString:@" connected"]] autorelease];
	[temp addAttributes:[NSDictionary dictionaryWithObjectsAndKeys:[NSFont boldSystemFontOfSize:12], NSFontAttributeName, nil] range:NSMakeRange(0, [temp length])];
	[temp appendAttributedString:[[[NSAttributedString alloc] initWithString:@"\n"] autorelease]];
	[myServer send:temp toClient:aClientIdString];
}
 
-(void)dealloc
{
	[previousChats release];
	[super dealloc];
}
 
-(void)retreat
{
	if(current < [previousChats count]) 	{ 		current++; 		[[toSend textStorage] setAttributedString:[previousChats objectAtIndex:current]]; 		if(current == [previousChats count]-1) 			[previousChats removeLastObject]; 	} } -(void)advance { 	 	if(current > 0 && [previousChats count] > 0)
	{
		if(current > [previousChats count]-1)
			[previousChats addObject:[[toSend attributedString] copy]];
		current--;
		[[toSend textStorage] setAttributedString:[previousChats objectAtIndex:current]];
	}
}
 
- (void)windowDidBecomeKey:(NSNotification *)notification
{
	[mainWindow makeFirstResponder:[toSend superview]];
}
 
-(IBAction)sendMessage:(id)sender
{
	NSMutableAttributedString* temp = [[[NSMutableAttributedString alloc] initWithString:[NSFullUserName() stringByAppendingString:@": "]] autorelease];
	[temp appendAttributedString:[toSend attributedString]];
 
	[previousChats addObject:[toSend attributedString]];
 
	if ([previousChats count] <= 10)
		current = [previousChats count];
	else
		[previousChats removeObjectAtIndex:0];
 
	NSString* t = [temp string];
	if(![[t substringWithRange:NSMakeRange([t length]-1, 1)] isEqual:@"\n"])
		[temp appendAttributedString:[[[NSAttributedString alloc] initWithString:@"\n"] autorelease]];
	[myServer sendToAllClients:temp];
	[[toSend textStorage] setAttributedString:[[[NSAttributedString alloc] initWithString:@""] autorelease]];
}
 
- (void) growlNotificationWasClicked:(id)clickContext{
	[[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
 
}
 
-(void)server:(ThoMoServerStub *)theServer didReceiveData:(id)theData fromClient:(NSString *)aClientIdString {}
 
-(void)client:(ThoMoClientStub *)theClient didReceiveData:(id)theData fromServer:(NSString *)aServerIdString;
{
	NSMutableAttributedString* temp = [[[NSMutableAttributedString alloc] initWithAttributedString:[history attributedString]] autorelease];
	[temp appendAttributedString:theData];
	[[history textStorage] setAttributedString:temp];
 
	[history scrollRangeToVisible:NSMakeRange([[history textStorage] length], 0)];
 
	if(![mainWindow isKeyWindow])
	{
		[[NSApplication sharedApplication] requestUserAttention:NSCriticalRequest];
		[self growlAlert:[(NSAttributedString*)theData string] title:@"New Message"];
	}
}
 
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender
{
	NSMutableAttributedString* temp = [[[NSMutableAttributedString alloc] initWithString:[NSFullUserName() stringByAppendingString:@" disconnected"]] autorelease];
	[temp appendAttributedString:[[[NSAttributedString alloc] initWithString:@"\n"] autorelease]];
	[temp addAttributes:[NSDictionary dictionaryWithObjectsAndKeys:[NSFont boldSystemFontOfSize:12], NSFontAttributeName, nil] range:NSMakeRange(0, [temp length])];
	[myServer sendToAllClients:temp];
 
	return NSTerminateNow;
}
 
@end

Download the Xcode Project

Let me know if there are any issues with the source, or if you would like to see more, like an iPhone OS example, just post a comment.


New From Kunugiken WikiTyper

Posted: May 15th, 2010 | Author: Remi | Filed under: Cocoa, OS X, Programming | No Comments »

I was playing the fabulous typing game TypeRacer when I thought that it would be fun to type some sort of other text. I decided to try making my own typing program. So I threw together this new typing game for OS X that lets you type random paragraphs of text from wikipedia. Its absolutely free, try downloading it here Download Page.

WikiTyper in Action

If you have any comments on the game please post them below. Some things I know are not working and I might fix in the future if people ask about them.  The words per minute is actual words per minute not the correct wpm, (if anyone knows the formula?)  Also occasionally it will display characters that you cannot type, but I couldn’t find a way to remedy that efficiently.  Finally, for those of you Linux and Windows users, a Java port is being made by someone else.  I will post a link if/when it is finished.


Cocoa: Making Properties KVO Compliant

Posted: November 14th, 2009 | Author: Remi | Filed under: Cocoa, Programming | 1 Comment »

In Cocoa KVO or Key-Value-Observing compliant means that the value of a certain object sends notifications out when its value has been changed to an observer. An example would be values stored in a NSMutableDictionary or NSMutableArray. Since they are KVO compliant, you can bind something to a value in one of these objects, and will be updated automatically, no glue code required. Although not all objects have their values KVO compliant and this can cause problems for example when trying to bind to them in Interface Builder. A good example can be seen with NSDocument’s documentName. Notice you can bind an object’s value to documentName, but if the documentName changes, this value does not update. The way to fix this, is to subclass NSDocument and send out notifications whenever documentName is changed. A class-dump of NSDocument reveals the -(BOOL)_setDocumentName:(NSString*)name method. All you have to do is add this method to your NSDocument subclass

-(void)_setDisplayName:(NSString*)name
{
	//Hack Display Name to make KVO Compliant
	[self willChangeValueForKey:@"displayName"];
	[(YourClassNameHere*)super _setDisplayName:name];
	[self didChangeValueForKey:@"displayName"];
}

And that should be it. Note: I changed the method’s return type to void because otherwise it will create an error in the console about KVO compliant methods not being able to return anything other then void.

Also note that this is for read-only binding, to do a binding where you can change the value you have to implement and expose a new binding programmatically.

Apple’s Documentation on KVO


Snow Leopard and Close to Instant Gmail Contact Sync

Posted: August 31st, 2009 | Author: Remi | Filed under: Cocoa, OS X, iPhone | 1 Comment »

I’m sure many of you are aware that Apple has just released their newest OS X update called Snow Leopard, I was lucky enough to get my hands on a copy already. One very interesting feature of this update is the support for syncing your contacts in Address Book.app with Gmail’s contacts. Through Gmail, you can already sync your contacts to your iPhone thanks to Google Sync‘s MS Exchange servers. With the these two services combined you can have your contacts on your phone synced to your computer and vice-versa fairly instantly without MobileMe. The problem I was having was that this service didn’t do quite instant enough for my taste, here’s how you enable the sync, and how you can tweak it to make it even closer to instant.

First enable Gmail Contacts sync in Address Book by clicking Address Book > Preferences… > Accounts. Then click “On My Mac”, and then check the box next to synchronize with Google. Enabling Google Sync in Address Book in Snow Leopard After enabling it you will be prompted for your Gmail username and password. Then you should see a sync icon in your status bar. This lets you manually force a sync with google by clicking “sync now.” When it syncs you will probably have to resolve some ‘sync issues’ (basically deciding which contact is correct, the Gmail or Address Book version.)
Snow Leopard Sync Menu
If this icon annoys you, just hold down the apple key and drag it out of the bar to remove it, to put it back you can enable it in iSync.app’s preferences.

Now to tweak it. Your computer syncs your contacts in a job in that runs through a process called launchd (For those of you that don’t know, in Tiger and newer, launchd takes the job of cron on normal unix systems.) By default this process runs only once every hour, we are going to try and tweak this. To easily edit your loaded jobs, download Lingon from here http://sourceforge.net/projects/lingon/files/. Open up Lingon, on the left under ‘My Agents’ you should see ‘com.google.GoogleContactSyncAgent.’ Click this, and you here you are able to change it from running every hour to as little time as every second (although I don’t recommend this AT ALL.) Just adjust this setting to a shorter time period to speed it up. The real trick here though is that you can set something called a watch file/directory. This lets the process run when that file/directory has been modified. To make the sync run whenever you change one of your contacts you should just fill in under “Run when this file is modified” this file /Users/remi/Library/Application Support/AddressBook/AddressBook-v22.abcddb This file is your address book database, so hence it runs when you update the database. Lingon Editing Snow Leopard Google Sync Agent Remember that this makes your contacts instantly updated from your computer to google, but no the other way around. The frequency that the contacts come into your computer from google is determined by the “run it every” setting. I do not know why apple did not have the file set to watch the address book database to begin with, so I’m not sure if there are any adverse effects, but there has been none that I have experienced so far. Once you are finished editing the job, hit save, then log out and back in or restart for it to take effect. To try it out just edit one of your contacts in Address Book.app and see the update automatically appear on Gmail, and if you iPhone is synced to Gmail, then right to your iPhone.

Please post a comment if you have problems with this method, good luck everyone!