Enabler: Turning On and Off Controls and Playing With Speech

My next project is an attempt to understand how to manipulate the enabled state of controls, talk to a pop-up, and to make speech work. Here is the specification:

Speak one of four words when a button is pressed. Select the word to be spoken with a pop-up menu. While the word is being spoken, display a progress bar. Repeat the word between 1 and 5 times, selecting the number of repetitions with a slider and showing the result in a text field. The slider, button, progress bar, and pop-up menu are each enable by a separate checkbox.

Here is the interface in IB:

enablerib

All of the controls except for the checkboxes have been set up with the enabled flag set off. There are four words on the pop-up" Good, Day., To, You. Setting up the first three was just a case of clicking and editing, but getting a fourth one was not obvious. Until I realised that the pop-up items were actually NSMenuItem objects. All I had to do was drag a new one onto the control. Voila!

The Repetitions slider is set up with a minimum of 1 and a maximum of 5. The text field is not editable. The progress indicator does not have an enabled flag, so I set it hidden and hope that will do the trick. My guess is that you unhide it to make it display.

hr


In IB I subclass NSObject from the Classes tab and change the name to EnablerController. I'm going to need to add some actions to that class so that the controls attached to the actions know which methods to call and add some outlets so that the method variables have pointers into the controls I want to manipulate.

One confusing thing about IB (that I think I now understand) is that it doesn't create a bunch of descriptions of the interface elements. When you run the application it creates instances of your objects. The Apple docs tell you this, but they don't explain the implications of that fact. Firstly, if nib files just contained descriptions, then the app would have to instantiate the objects. Cocoa apps don't. So you don't have to do that. Secondly, when working with IB you have to remember that the object instance is really there in the nib. Setting up an outlet on the controller is creating one in the app that points to the control instance on the window. I used to think that outlets were the wrong way around since they were added to the nib file: I thought that since the app "points" to the items in the nib and the controller instance was in the nib, an outlet should be a method name. Wrong.

Next, instantiate the controller from the Class menu. Here is the nib window so far:

enablerdoc

Outlets first. I will need five in all: repetitions slider, repetitions text field, word pop-up, progress indicator, and speak button. Those are all things I need to control. I have to bee able to enable and disable them individually and turn the progress indicator on and off. New outlets are added as attributes of the EnablerController class. Not the instance: if I had several, all the EnablerControllers would have the same set of outlets but they would be hooked up differently.

I give each outlet a type. It defaults to id, but can be changed to the type needed. I don't think this is strictly necessary, but will help the compiler find errors. Here are the outlets set up:

enableroutlets

For the actions I will need five: one for all the checkboxes and one each for the repetitions slider, repetitions text field, word pop-up, and speak button. I'm not using bindings, so I will have to coordinate the repetitions slider and text field myself. i think I can use one just for the checkboxes since I can ask the checkbox for its name. We will see. Here are the actions added:

enableractions

I hope I have the tense of the names right. I also avoided using the same names as the outlets, but I don't know if that is important. Now I generate source code for the new class using the Classes->Create Files for EnablerController in IB and in XCode move them from the Other Sources group to the Classes group. I compile the application as it stands so far just to see what happens.

Not quite right. I can click on the checkboxes and change their state, and the other controls are grey, but the labels for Repetitions and Word are dark. Back to IB.

enablerwindowbad


But I got it right: the Enabled checkbox in the attributes is not set. And if I use Hidden it goes away completely. Maybe I have to group it with the control or do the disabling programmatically. Add that to the to do list.

Next: write some code to let the checkboxes control the state of the other controls.

hr


Researching the problem of the static text that was not disabled when Enabled was not checked on the Attributes page of the inspector led me to this (located here):
enablersubclass
So I have to subclass NSTextField and override -setEnabled:flag method. It calls the parent object (NSTextView) so that it can do whatever it needs to with the enabled flag, then changes the color. I'll also have to hook up two more outlets in my EnablerController, one for each piece of static text so I can enable and disable them.

I've not subclassed anything except NSObject before, so let try to do it in IB. There is a little search box on the nib window that proves to be the way to locate NSTextView. Select it and subclass from the classes menu. That gives me MyTextField, so I rename it EnablerGreyableTextField.

How to make my static text an instance of the new class? If I click on Repetitions and look in the Custom Class tab of the inspector it lets me choose from several classes:

enablersubclass2

Click on EnablerGreyableTextField am I am done. Repeat for the static text Word. Now I am aware that I have to do something special with the custom class because IB does not know how to handle it: IB will create an NSTextField for me, not my custom class, so I think have to do something in -awakeFromNib to initialize my object. And I think I have to instantiate it in the nib twice. So I do that and rename one instance Repetitions Field and one instance WordField. And I create source files for EnablerGreyableTextField.

Back to XCode and compile. Another interface problem. The top left checkbox is unchecked, but has focus. I really want no focus unless the repetitions text field is enabled and then that has focus. So lets try making the text field the initial first responder. Control click from Window to the text field and select the InitialFirstResponder outlet. That creates a pointer in the window manager that will be set to point at the text field. The NSTextField has a nextKeyView outlet that ought to point back to this text field so that tabbing does nowhere, but IB will not let me do that. So maybe I have to do that programmatically later.

Build and go and the Slider check box no longer has initial focus. Now to coding the custom class. The source code for the interface file of my custom class as created by IB looks like this:

enablergreyableh
I add the method I am overriding after the last closing curly bracket and before @end. That's it for the interface. The implementation I edit to look like this:

enaablergreyablem
Build and go and there is no change in the program. I didn't create the extra outlets for these fields in EnablerController, not did I hook up anything. So in IB, control click on each checkbox and drag to EnablerController. Select checkBoxChanged and click Connect to make the connection.

All the actions are set up in the same way. Now I add two more outlets to the EnablerController class so it looks like this:

enablernewoutlets


And I hook them all up by Control-dragging from the EnablerController to the interface elements I want to be able to control so they look like this:

enableroutletsset

Done. Now onto XCode and coding the application class.

hr


First I recreate the source files for the EnablerController class, overwriting the old ones and check to see what the code looks like. The interface file is thus:

enablercontrollerh1

And the implementation file thus:

enablercontrollerm1
Looks good to me. The (IBAction) and IBOutlet identifiers are clues for IB. They don't compile to anything. I build and run to see what it does now. Compilation error! I forgot to import the interface for EnnablerGreyableTextField into EnablerController. Now it runs. Still looks the same though with black static text because I haven't done some magic with the custom class I am using to control it:

enablerwindow1

Fixing the static text involves sending a setEnable message to my custom control with an argument of NO. I no longer think that I have to do anything very clever in -awakeFromNib, since IB seems to know all about my custom class. How do I know where my custom class instances are? Well since I have outlets to them -- repetitionStatic and wordStatic -- I just send the messages. awakeFromNib is the right place to do this. It is called for every object that the nib file instantiates after it has initialized everything but before it is displayed. So I can guarantee that the outlets have been set for me and my objects are all initialized. I add this code to EnablerController.m:

enablerawakefromnib

And here is the result:

enablerinitialwindow

Now lets try to control the enabling of the interface elements by the checkboxes. I think that I can look at the title of the checkbox that sends me the action and do a string comparison, then send setEnabled messages to each of the interface elements. So I code this:

enablercheckboxcode1

But I get a warning in the Progress code that the control may not respond to the enable message. Hmm. I compile and run and find that there is indeed a problem. Here is the window and the log. The progress indicator is not visible (or course) and the log shows some errors when I click on the Progress pop-up.

enablerprogressproblemwindow


enablerprogressproblemlog

Everything works except the progress indicator click. And I notice that I have a memory leak. Since I explicitly alloc the NSString, I have to release or autorelease it. Since I am really done with it when I exit the method, release is best. So I'll fix that in the code and I will create an instance variable to store the state of the progress indicator that I can use later. I declare a BOOL variable called progressIndicatorEnabled in the interface file and modify the code to look like this:

enablermemorycrash

And it crashes when I uncheck the Slider checkbox. So I don't understand memory management. If I comment out the [checkboxname release] it is fine, so I must be overreleasing. The fix is to set up checkboxname like this

checkboxname

and do no allocating and no releasing. I'm not convinced that this is correct, because I am relying on the sender to not deallocate it underneath me. I should probably be using stringFromString: and explicitly release.

Next step: join up the repetitions slider and text field so that they both follow the number of repetitions. And enable the text field. I forgot that.

hr


I added enabling the text field to the objects I had to enable when the slider checkbox was clicked, but it didn't work. My error was that I had not made the text field editable. There is a checkbox in the Attributes panel of the inspector in IB that does that.

I think I understand the memory problem too. I was allocating an object and assigning a pointer to it. [sender title] was giving me an autoreleased object (and hence making my pointer to NSString point elsewhere) and I was then releasing the autoreleased object. Crash when the run loop tries to deallocate the already deallocated object.

Now lets make the two parts of the Repetitions control, work together. The slider is already constrained to have whole values from 1 to 5. I set that up in IB like this:

enablerslidersettings

The problem will come with the text field. I want to accept only the numbers 1, 2, 3, 4, or 5, so I have to validate the input value somehow. I will also need to store the values for the repetitions and word somewhere. That should be in a new object, a model object, that knows nothing about the interface. It won't do very much, just store the current settings. I don't strictly speaking have to go to all this trouble, but I am eager to see how confused I can get.

So go to File->New File in Xcode, pick Cocoa Class, and create the files for a class called EnablerModel. I create the interface to look like this:

enablermodelh

and create accessor methods in the implementation file like this:

enablermodelm

I think I got the last one right. It retains the new word, releases the old word, then does the assignment. That way if the new word is the same as the old word (same object instance) it still works.

Since I will want to initialize instances of this class, I should set up some initializers and add them to the interface file so they can be used by other classes. Here is my code:

enablermodelinit

I have a default initializer, init, that calls a designated initializer, initWithRepetitions:andWord:. I don't think I need a dealloc method because I have nothing to undo when the object is deallocated.

I also realize that I have an object-oriented design problem here. The interface has the words hard-coded into it and I have to repeat the first one here. That is prone to error and prevents reuse. I can use the controller to set up the repetition count by simply reading it from the model and updating the interface, so no problem there. What I really need here is to have a model initializer that accepts an array of strings and have the controller read them out of the model and write them into the interface. That will have to wait until later: I'd like to see my Repetitions slider and text field do their thing first.

hr


Since the controller is in control, its job is to talk to the other reusable pieces and make everything happen. The first thing that needs to happen is that the interface and the model need initializing and set up in such a way that they are in sync. But first I have organized my classes in XCode so that it is clear what the role of each is. Right-click on the Classes folder to create new sub-folders.

enablerclassorg

I think that the right place to put all the set up is -awakeFromNib. I already have other set up in there to get the view correct, so I will follow that with code for the model. I have to create an instance variable in my model object to hold a pointer to the model and make sure I #import the interface file. The code I add is just this:

enablermodelsetup

I have definitely allocated this object, so I definitely need to release it somewhere. I'll add a dealloc to my controller and put the dealloc code there:

enablerdealloc
Well, it didn't crash, and it behaves the same as it used to. A quick look with the debugger seems to confirm that all is well. Now to make the repetitions slider output its value in the text field and vice versa.

A problem. How do I get the value of the slider from the NSSlider object? I look all over, but only find information on NSSlider, not all its superclasses that may have the method I am looking for. Eventually I poke around in the Class Browser (no joy in the documentation browser where I would expect it to be) and hit on displaying using the mode Flat, All Classes. There is no search field on the class browser! Hard to believe, but then it is a browser, not a finder. There is a list of bookmarks above the source pane, but it does not show the superclasses. But if I configure options (button top left) I find I can turn on a very useful thing: I can show the inherited members:

enablerclassbrowser


Some scrolling up and down (page up and page down don't work, and the pane does not accept focus) lands me -(int)intvalue and the source pane shows some more methods that I may want to consider:

enablervaluemethods

Since I am only using integer values, I will try -(int)intValue and -(void)setIntValue. Since these are all in NSControl, and NSTextField inherits from NSControl, I can also use these for the text field. So I code the action methods like this:

enablerrepsync

Build and run. Turn on the enabling checkbox and -- good grief -- it works! I can move the slider and have the text field change and change the text field and have the slider change.

enablerworkingreps

There are still some issues, however: the text field is not initialized to "1" like the slider is, and I have no validation on the text field. I'd like to beep if the number is out off range or non-integer.

Next: update the initialization code to set up the text field and figure out how to validate the number entered.

hr


Before writing the text field validation code I have to add the initialization of the view so that it matches the model. I reorganize -awakeFromNib a little and add two lines to do the initialization. model is my pointer to the model object, so getting values is simple. In getting this to compile, I find I had made two errors: I had not #imported EnablerModel.h in the controller code, and in the model interface file I have not declared my accessors. All fixed now. Here is my -awakeFromNib now:

enablerawakefromnib2

Build and run and it does what I want. On to the text field.

I have a user interface decision to make here. Do I a) prevent the user from entering anything invalid, b) coerce anything they enter into something legal, or c) indicate an error has occurred and prompt to reenter?

a) will involve working with either a delegate of the NSTextField or a subclass of it. The user will see the entered value not "sticking" and will wonder what is wrong. After all, there is nothing to tell them that integers 1 through 5 are the only valid inputs.

b) will be easy to implement. The user will see the slider snap to one of the ticks and, if out of range, the text field value be clipped and the slider be at one end of its travel. I can either leave the text field as entered, or I can display the integer equivalent.

c) will either need a beep (not very helpful), a spoken message, or a modal dialog box that describes the problem and prompts reentry.

I'm going to go for b). It is easy. And it is intuitive. The feedback for a non-integer value will come from both the slider position and the display of integer-only values. One or two errors on behalf of the user will result in a complete understanding of the system.

I can code this in the controller. A change in the repetition text field will send and action to my controller and invoke my repetitionTextFieldChanged: method. There is already code there to send the value to the slider. I just have to constrain the value I get from the text field and then write it back to the text field.

enablertextconstrain

It works! But this has a problem: my controller now has hard-coded values in it that should come from the view it is managing. Rummaging around in the class browser I find -(double)maxvalue and -(double)minvalue in NSSlider. So I can use those instead:

enablertextconstrain2
I don't think the (int) casts are anything close to necessary, but they help to reinforce that fact that -minValue and -maxValue don't return ints.

Now I look at the window for Enabler, the text field looks odd. It is too big and the text is left-justified. So I halve the width of the field in IB and center the text. So:

enablerwindowcentered


It is also interesting to note that the controller doesn't store the model data anywhere. This is as it should be. The controller just coordinates and controls.

Next is speaking. There is a document all about the speech synthesis manager that seems to tell me what I need to do. But that is Carbon. Better to go to ADC Home > Reference Library > Documentation > Cocoa > User Experience > Speech > and follow that.

hr


Adding speech looks very easy from the documentation. Create an instance of the synthesizer and send a -startSpeakingString message to it. By setting a delegate I can also know when it has stopped speaking and use that to control the progress indicator.

I will add initialization code to my controller's -init method. It is done that way in the example I am looking at. It seems like a good enough place since I can match it with the necessary -release in my -dealloc method. There is no involvement with the view, so -awakeFromNib does not appear to be a candidate. I will need an instance variable to hold a pointer to the synthesizer object too.

I added more comments to my interface file as well. It now looks like this:

enablermodelh2

My -init and -dealloc methods now look like this:

enablerspeechinit

Build and run, and there is no change in behavior, so that is good. Next is to add code to the -speakButtonPressed action to speak the word selected by the pop-up. First I'll check that I can speak anything and make it say "And now for something completely different" and then modify it to get the chosen word. That works. I can keep pressing the Speak button and it restarts on each press, even in the middle of speaking. I'd like to change that so that the button gets disabled when the progress indicator is on. First lets have it speak the chosen word the number of times selected:

enablerspeakloop
It speaks the word (I changed the default word to something longer so I can hear it -- "Everybody"), but it does it over and over, not once, and eventually crashes the app. I am not waiting for it to finish speaking -- that is obviously an error -- but it should only go once through the loop and that should not matter. Time for the debugger.

Dumb error. i-- is in the wrong place. It's been too long since I programmed. Fix that and no crash. But because I am not waiting, speaking 2 or more times just comes out once. Time to implement the delegate method -speechSynthesizer:didFinishSpeaking. It sends YES if speaking finished normally, NO otherwise (stopped or aborted).

I can now add my code to enable and disable the Speak button. I'll deal with the repeats after that.

enablerspeechdelegate

And that works. The button is grayed out during speaking. I could change it to a Stop button if I wanted to get fancy and have a click stop the speech.

The repeat has not been fixed of course. I can think of three ways to implement this: a) put some sort of wait in my for loop that wakes up when the speech is done, b) have the speechSynthesizer:didFinishSpeaking method start off the next repetition, or c) create a new class that speaks a word n times and put the loop in that.

a) Looks simple, but I have no idea what to us to achieve this. It would need some sort of sleep method and a way of signaling that a wake up is needed.

b) Also looks simple, but has the problem of knowing when to stop. If speechSynthesizer:didFinishSpeaking starts speaking again, it will need some sort of flag to tell it to not do that after the last repetition. Or I can to send -stopSpeaking at just the right time

c) Looks more complicated, but would allow me to encapsulate the problem and solve it elsewhere.

But maybe I can avoid all of this. If I give the speech synthesizer all my words separated by spaces in one string, it should do the repeating itself. That also deals with the fact that repeating a word by any of the above methods would not have put a gap between repetitions. So the solution looks like d):

d) Form a temporary string that contains the word to be spoken n times and say that once

Here is the code:

enablerspeakloop2

I am autoreleasing the string rather than releasing it because the speech synthesizer could still be using it. No crash, so I hope I got that right. But oddly, it only says the word once, whatever the setting of the slider. Debugging I find that [model numberOfRepeats] is returning zero. Odder still is the fact that a breakpoint on setNumberOfRepeats is never hit when I move the slider. Debugging reveals the answer: I never updated the model when the slider is moved or text field is changed. So I update the methods repetitionSliderMoved and repetitionTextFieldChanged with [model setNumberOfRepeats: newvalue]. And now it speaks the correct number.

So here are the actions for the text field and the slider:

enablerrepfields

And the Speak button code. I added code to turn on the progress indicator if it is enabled by the checkbox.

enablerspeakloop3

But now there are more problems. While the progress indicator appears OK, it doesn't rotate while speaking is occurring. Also, the words Good Good Good come out all bunched together like GoodGoodGood. The fix for that I find is to insert a period and space between each word instead of just a space. A fix for the progress indicator will take some reading because I thought these things just moved by themselves.

Also, I have a race condition. If I click on the Progress checkbox while it is speaking, then the state of the progress indicator gets out of whack with the progressIndicatorEnabled flag. I can fix that by always hiding the progress indicator at the end of speech rather than making it conditional on the state of the progressIndicatorEnabled flag. But what happens if I repeatedly click the checkbox while it is speaking? I will have to turn the progress indicator on if I enable the checkbox and off if I disable it. So I will need a flag that shows if I am speaking that the checkBoxChanged code can see.

So in -awakeFromNib I now have this:

enablerprogress

In -checkBoxChanged I have this:

enablerprogress2

And my speech code looks like this:

ennablerprogress3

And that all seems to work OK. A lot of code for one little feature. And the progress indicator still doesn't rotate! That is the next challenge.

hr

I found a couple of methods that make the progress indicator rotate: -startAnimation and -stopAnimation. I put them in the code where I make the indicator visible. So the code now looks like this:

enablerrotate

Getting the pop-up right is a bit more challenging. I have hard-coded the words I want to speak in the view and discovered that in order to get the initialization of the model and view to work correctly I had to duplicate some of the date (the default word). This is not good, since I have dependencies between things that should be independent.

The cure is to put all the data in the model and provide a method for the controller to get it out. The controller can then initialize the view with the data. So back to the model and add a new instance variable and method:

enablermodelh3

I am using an NSArray and not an NSMutableArray because I don't want it to be modified by the controller. There is no setter method because it is never set. The method is simple enough:

enablerwordlist

The wordlist needs to be initialized, so I'll put that in -init and get rid of -initWithRepetitions.

enablermodelinit2

I'm not including a dealloc because I don't think that I need one. The array is initialized with a convenience method and that means that the array is autoreleased for me. I hope, anyway. Build and run and it works still. Now I have to modify the controller to use this method to initialize the view:

enablerawakefromnib3

It didn't take very long to track down these methods using the documentation browser. I wasn't sure that I was doing the right thing, since there is a lot of functionality in NSPopUpButton and the implementation is complex. But it does work. I cleaned out the old pop-up in IB and put the words "One" and "Two" in. When I ran the app the words "Good", "Day", "To", "You" were there. Cocoa is easy to guess it seems.

The last part of the puzzle is to allow the pop-up to change the word selected. When the user selects a word the action method -wordChanged is called in the controller. That method gets the sender's id, so by asking the pop-up for the current selection, the controller can set the word.

enablerwordchanged

I got a warning that said "passing argument 1 of 'setWordToSay:' from incompatible pointer type" when I compiled it without the (NSString* ) cast, but it went away when I added the cast. Build and run and it doesn't work. I get this message:

enablerwordchangederror

so maybe the compiler is trying to tell me something. I'll recode this method so that the [sender selectedItem] call uses a temporary variable of type id and step through that carefully.

The sender is an NSPopUpButton. No surprise there. But the selectedItem is of type NSMenuItem. And digging into the docs, that has a method -title that returns the menu item's title. So I'll try that.

enablerwordchanged2

And it compiles and works. I can select different words and have it speak each of them from one to five times. I wonder how many lines of code that took. I cannot find a LOC counter in XCode that will do that.

I could continue to expand this little app. For instance I could add methods to the model that would have it store its data on disk or in user prefs. Or get the list of strings from a text file. Or many other things. But since I have now met my original spec, I will stop there. An improvement that was suggested to me would be worth implementing if this were production code: use tags for the checkboxes. I matched the names of the checkboxes instead. Not good, because if I localize the app or change the names, it will break functionality. Tags are just numbers added in IB to the checkboxes. The controller would just use a switch statement to pick the right code.

The Bagelturf site welcomes Donations of any size