• Increase font size
  • Default font size
  • Decrease font size
Home Article Sections Cappuccino Simple Applications Connected Button

Connected Button

E-mail Print PDF

In the "3-Button Liquid" article we built a simple GUI that had 3 buttons and showed how to "attach" them to the edges of the window.  That let them move around nicely as the window was resized - but they didn't do anything.  This time let's actually make some buttons that work.  We'll make an application with a couple buttons and labels.  Clicking the buttons will change label values and enable/disable buttons.

The Requirements

So we're a budding Captain of the a star ship and, being the geeks that we are, we're going to build our own interface to control it.  Since the computer (the new HAL 0.1.2) actually controls most of the functions we'll need only a couple controls.

  1. A speed control to set the ships speed.  Since we've had a lot of caffeine we really have only two speeds: stop and full speed.
  2. A display to tell us the speed setting
  3. We have a bay to store landing pods (in case we find a cool new planet) so we need a way to open and close the pod bay door.
  4. The pod bay door should never be opened when we're at full speed.  The door should be closed automatically when we start moving.
  5. Of course we need a display to tell us the status of the door.

The image shows us out mock-up of this very complicated interface.  You can see the text descriptions of the engine and door status (statuses? stati?) and the buttons to change engine/door state.

The Code

The complete code is a little long to display here so grab the "ConnectedButton-1.zip" zip file and uncompress it.  It contains the AppController.j for the first part.  We'll go over the various pieces.

First check out the controller declaration

@implementation AppController : CPObject
{
  CPButton speedButton;
  CPTextField speedLabel;
  Bool inWarp;
  CPButton podBayDoor;
  Bool podBayDoorOpen;
  CPTextField doorLabel;
}

Here we've defined the various user interface (UI) pieces (a couple buttons and a couple labels) as well a two booleans to keep track of the state of the engine and door.  Let's skip down a bit and see how we create and connect up one of the buttons.

  speedButton = [[CPButton alloc] initWithFrame:CGRectMake(20, 20, 250, 24)];
  [speedButton setTitle:"Warp speed"];
  [speedButton setFont:[CPFont boldFontWithName:@"Helvetica" size:16.0]];
  [speedButton setTarget:self]; 
  [speedButton setAction:@selector(toggleWarp:)]; 
  [contentView addSubview:speedButton];

The first three lines we've seen before.  They create the button and set its text and font.  We're setting the button text to "Warp speed" since we'll be starting out with the ship stopped.  Lines 4 and 5 are the interesting bits.  Line 4 sets the object/class that will set the events for the buttons (e.g. when the button is pressed) while line 5 specifies the specific message that will be sent (function to be called).  In this case we're calling the "toggleWarp" method of "self".  In other words, when the speed button is pressed the Cappuccino engine will send the toggleWarp message to the AppController object.  So what do we do when we get the toggleWarp message?  Let's look at that routine:

- (void)toggleWarp:(id)sender 
{
  if (inWarp) {
    [speedLabel setStringValue:@"Engine status: impulse power"]; 
    inWarp = false;
    [speedButton setTitle:"Warp speed"];
  }
  else {
    [self closePodBayDoor];
    [speedLabel setStringValue:@"Engine status: warp 11"];
    inWarp = true;
    [speedButton setTitle:"All stop"];
  }
}

In toggleWarp we're simply going to change the state of the engines.  First we check what state we're in by looking at the "inWarp" boolean.  Recall this is member data that we created and initialized in applicationDidFinishLaunching.  If we're already at warp speed (inWarp is true) then we'll set the speed label to indicate we're stopped (running on impulse power), set the warp state to false (remember we have only two speeds), and finally re-set the speed button text to "Warp speed" to give the Captain an indication of what the button will do next time it's pushed.

If we're not at currently at warp speed... well we can't have that - we're going to take off.  But first we better make sure the pod bay door is closed (remember requirement 4 above).  We'll do that by sending the "closePodBayDoor" message to self.  We then just need to take off: set the speed indicator to warp 11, the internal engine status to indicate warp speed (inWarp is true), and to be nice we'll set the speed button text to "All stop" to tell the Captain what pushing it again will do.

We now have a working engine controller.  The button responds to user clicks and modifies two different controls (the speed label and the speed button) when the button is pressed.  You can manipulate any number of UI elements from within event methods this way.

Let's take a quick look at the door controller methods because we have a rather annoying interface...

- (void)closePodBayDoor 
{
  [doorLabel setStringValue:@"Pod bay door status: closed"]; 
  podBayDoorOpen = false;
  [podBayDoor setTitle:"Open the pod bay door"];
}

- (void)openPodBayDoor 
{
  if (inWarp)
    [doorLabel setStringValue:@"I'm sorry Dave, I'm afraid I can't do that."]; 
  else {
    [doorLabel setStringValue:@"Pod bay door status: open"];
    podBayDoorOpen = true;
    [podBayDoor setTitle:"Close the pod bay door"];
  }
}

The closePodBayDoor method is really straightforward.  It just changes the door state (via the member podBayDoorOpen boolean) and the status label and button text.  Pretty simple.  The openPodBayDoor method on the other hand is (marginally) more complicated.  Remember that the door should never be opened when we're moving (requirement 4) so in openPodBayDoor we do a quick check.  If inWarp is true we do not open the door but instead change the door status to "I'm sorry Dave, I'm afraid I can't do that."  I know, I'm mixing movies here, but hey, all the sci-fi computers are snarky.  So what's the problem?  We've satisfied the requirements by not opening the door while we're moving (although we've clobbered the door state display).  Well, this is a pet peeve of mine and rather poor UI design.  If I'm not supposed to open the doors while I'm moving why could I push the button in the first place?!  Let's fix that...

The Code Part II

Let's change the toggleWarp routine a touch (the complete code for this AppController.j is attached in the ConnectedButton-2.zip file):

- (void)toggleWarp:(id)sender 
{
  if (inWarp) {
    [speedLabel setStringValue:@"Status: impulse power"]; 
    inWarp = false;
    [speedButton setTitle:"Warp speed"];
    [podBayDoor setEnabled:true];
  }
  else {
    [self closePodBayDoor];
    [podBayDoor setEnabled:false];
    [speedLabel setStringValue:@"Status: warp 11"];
    inWarp = true;
    [speedButton setTitle:"All stop"];
  }
}

What have we changed?  Well, it's really too easy.  Lines 7 and 11 do the dirty work.  If we're not yet moving (inWarp is false) we'll close the pod bay door and disable the pod bay door button (line 11).  If we're already moving so we're coming out of warp, we just re-enable the pod bay door button (after we've stopped of course).  No need for snarky comments from the computer since you can never push the button to open the doors when you're moving.  Much better!

Note, we made an engineering decision in the toggleWarp routine.  If the doors are open when the got-to-warp button was pressed we chose to close the doors then take off.  We could have chosen to disable the warp speed button as long as the doors are open.  That would probably have been a smarter choice.  After all, the away team could be off the ship if the door is open.  Oh well, I never liked the away team anyway.  We'll leave it as an exercise for the reader to disable to warp button when the pod bay door is open.

The Code Part III

No, this isn't a solution to the homework.  The part II code above is OK, but it's a little rough.  We put everything into the AppController file which, for this really simple case, isn't a problem but for more complicated interfaces will bet to be burdensome.  It's hard enough to figure out which UI elements to turn on and off in response to user interaction without having to wade through all the code.  Let's make a simple change that maybe heads in a better direction.  Let's move the pod bay door control to its own widget.  Grab the code out of the "ConnectedButton-3.zip" file attached.  The zip file contains 2 files this time; AppController.j and PodBayDoorView.j.

Let's first look at the complete AppController.j (it's much shorter now):

@import <Foundation/CPObject.j>
@import "PodBayDoorView.j"

@implementation AppController : CPObject
{
  CPButton       speedButton;
  CPTextField    speedLabel;
  Bool           inWarp;
  PodBayDoorView podBayDoor;
}

- (void)applicationDidFinishLaunching:(CPNotification)aNotification
{
    var theWindow = [[CPWindow alloc] initWithContentRect:CGRectMakeZero() styleMask:CPBorderlessBridgeWindowMask];
    var contentView = [theWindow contentView];
    
    inWarp = false;
    podBayDoorOpen = false;

    speedButton = [[CPButton alloc] initWithFrame:CGRectMake(20, 20, 250, 24)];
    [speedButton setTitle:"Warp speed"];
    [speedButton setFont:[CPFont boldFontWithName:@"Helvetica" size:18.0]];
    [speedButton setTarget:self]; 
    [speedButton setAction:@selector(toggleWarp:)]; 
    [contentView addSubview:speedButton];

    speedLabel = [[CPTextField alloc] initWithFrame:CGRectMake(290, 20, 350, 24)];
    [speedLabel setStringValue:@"Status: impulse power"];
    [speedLabel setFont:[CPFont boldSystemFontOfSize:18.0]];
    [contentView addSubview:speedLabel];
    
    podBayDoor = [[PodBayDoorView alloc] initWithFrame:CGRectMake(20, 60, 620, 24)];
    [contentView addSubview:podBayDoor];

    [theWindow orderFront:self];
}

- (void)toggleWarp:(id)sender 
{
  if (inWarp) {
    [podBayDoor leavingWarp];
    [speedLabel setStringValue:@"Status: impulse power"]; 
    inWarp = false;
    [speedButton setTitle:"Warp speed"];
  }
  else {
    [podBayDoor goingToWarp];
    [speedLabel setStringValue:@"Status: warp 11"];
    inWarp = true;
    [speedButton setTitle:"All stop"];
  }
}

@end

We've dropped all the declarations of the pod bay door control (button and label) but added the declaration of our newly-created class "PodBayDoorView" at line 9 (we'll get to that class in a minute).  There's also not creation of the pod bay door elements in applicationDidFinishLaunching instead we create a PodBayDoorView object. Finally we've modified the toggleWarp routine to pass messages to the PodBayDoorView object (lines 41 and 47).  Most of the pod bay door controlling happens in the PodBayDoorView class.

@import <Foundation/CPObject.j>

@implementation PodBayDoorView : CPView
{
  CPButton    podBayDoor;
  Bool        podBayDoorOpen @accessors;
  CPTextField doorLabel;
}


- (id)initWithFrame:(CGRect)aFrame
{
  self = [super initWithFrame:aFrame];

  if (self) {
    [self setPodBayDoorOpen:false];

    var width  = CGRectGetWidth(aFrame);
    var height = CGRectGetHeight(aFrame);
    var x = CGRectGetMinX(aFrame);
    var y = CGRectGetMinY(aFrame);
    podBayDoor = [[CPButton alloc] initWithFrame:CGRectMake(0, 0, 250, 24)];
    [podBayDoor setTitle:"Open the pod bay door"];
    [podBayDoor setFont:[CPFont boldFontWithName:@"Helvetica" size:18.0]];
    [podBayDoor setTarget:self]; 
    [podBayDoor setAction:@selector(togglePodBayDoor:)]; 
    [self addSubview:podBayDoor];

    doorLabel = [[CPTextField alloc] initWithFrame:CGRectMake(270, 0, 350, 24)];
    [doorLabel setStringValue:@"Status: closed"];
    [doorLabel setFont:[CPFont boldSystemFontOfSize:18.0]];
    [self addSubview:doorLabel];
  }
  
  return self;
}

- (void)togglePodBayDoor:(id)sender 
{
  if (podBayDoorOpen)
    [self closePodBayDoor];
  else
    [self openPodBayDoor];
}

- (void)closePodBayDoor 
{
  [doorLabel setStringValue:@"Status: closed"]; 
  podBayDoorOpen = false;
  [podBayDoor setTitle:"Open the pod bay door"];
}

- (void)openPodBayDoor 
{
  [doorLabel setStringValue:@"Status: open"]; 
  podBayDoorOpen = true;
  [podBayDoor setTitle:"Close the pod bay door"];
}

-(void)goingToWarp
{
  [self closePodBayDoor];
  [podBayDoor setEnabled:false];
}

-(void)leavingWarp
{
  [podBayDoor setEnabled:true];
}

@end

What's nice about this is the logic for controlling the pod bay door is localized in this class.  Specifically we have the new routines

-(void)goingToWarp
{
  [self closePodBayDoor];
  [podBayDoor setEnabled:false];
}

-(void)leavingWarp
{
  [podBayDoor setEnabled:true];
}

Recall that goingToWarp is the message sent by the AppController when the speed button was pressed but the ship was stopped (from the toggleWarp method).  We've effectively separated the code controlling the engine from the code controlling the door, but allow them to indicate state changes via messages.  If you run the code you should find it works exactly the same as in part 2.

As the code stands the messages thrown are only one way (into the door control) so we can't control the engine settings when the door state changes (to, say, disable jumping to warp speed).  We'll leave that as an exercise too...

 

Design by i-cons.ch / etosha-namibia.ch