Cocoa Recipes for Mac OS X: Add Controls to the Document Window
- Step 1: Add Controls to the Diary Window
- Step 2: Implement the Add Entry Push Button
- Step 3: Implement the Add Tag Push Button
- Step 4: Validate the Add Tag Push Button
- Step 5: Implement and Validate the Navigation Buttons
- Step 6: Implement and Validate the Date Picker
- Step 7: Implement and Validate the Search Field
- Step 8: Build and Run the Application
- Step 9: Save and Archive the Project
- Conclusion
Highlights
- Creating simple push buttons
- Setting tab order in a window (the key view loop)
- Writing action methods and using the sender parameter
- Using the NSLog() function for debugging
- Using format strings
- Displaying Unicode characters
- Manipulating text in the Cocoa text system
- Controlling undo and redo in the Cocoa text system
- Validating (enabling and disabling) buttons and other controls
- Using Objective-C selectors
- Creating and using Objective-C formal protocols
- Creating push buttons with images
- Creating a date picker
- Using dates in Cocoa
- Creating a search field
In Recipe 3, you created a powerful text editor, and you arranged to save its contents to disk and to read them back so that the user can maintain a diary recording notable culinary events. You refined the original Vermont Recipes application specification to provide for several controls at the bottom of the diary window to help update and use the diary. You add the controls and wire them up in this recipe.
Controls are a commonplace of every application. They range from the simplest push button to complex devices like the date picker. The basic techniques you use to create them and make them work are similar for all of them.
In this recipe, you start by creating two simple push buttons to insert the title of a new diary entry and to add tags to an existing entry. Next, you create four navigation buttons, with graphics, to enable the user to scroll to the previous or next diary entry and to the first and last entries. Finally, you create two complex controls, a date picker and a search field, for more sophisticated navigation within the diary.
In addition to learning how to create controls, you learn in this recipe about programmatic manipulation of text in the Cocoa text system.
Step 1: Add Controls to the Diary Window
The detailed specification for the Chef's Diary at the beginning of Recipe 3 calls for several controls in the space at the bottom of the window. You start in this step by using Interface Builder to add the controls to the window (Figure 4.1). In subsequent steps, you will hook each of them up in turn to perform a specific task.
Figure 4.1 The Vermont Recipes Chef's Diary window.
Start by opening the Vermont Recipes 2.0.0 folder in which you left the working Vermont Recipes project folder at the end of Recipe 3, leaving the compressed project folder you archived at that time where it is. Open the Vermont Recipes. xcodeproj file in the working project folder to launch Xcode and open the project window.
Increment the CFBundleVersion value by selecting the Vermont Recipes target in the Targets group, opening its information window, and selecting the Properties tab. Change the value in the Version field from 3 to 4, and save and close the information window. When you open the About window after building and running the application at the end of this recipe, you will see the application's version displayed as 2.0.0 (4).
Open the DiaryWindow nib file in Interface Builder.
In the Library window, navigate to Library > Cocoa > Views & Cells > Buttons, and drag two Push Buttons to the empty space at the bottom of the diary window. Position them one above the other toward the left side of the window, using the guides that temporarily appear to help you put them in the right place. These guides conform to the requirements of Apple's Human Interface Guidelines, making it easy for you to build a user interface that meets Mac users' expectations. The bottom button should snap to the guides defining the margins at the left and bottom edges of the window. The top button should snap to the guides for defining the margin at the left edge of the window and the spacing above the bottom button. If you need more room, Command-drag the bottom of the split view higher.
Double-click the top button to select its title for editing, or click in the Title field in the Button Cell Attributes inspector, and enter Add Entry.
Using the same technique, rename the bottom button Add Tag.
If you look closely, you notice that the title of the top button was a little too long to fit in the default width of the button, and Interface Builder automatically widened it to make room as you typed. Select the bottom button, and drag its left and right edges and move it as needed until guides appear, showing you that it is lined up with the left and right edges of the top button.
Another way to make the buttons the same size is to use the Button Size inspector. Before you resized the bottom button, the W (for width) field in its Button Size inspector and the same field in the Size inspector for the top button indicate different widths. You could simply have changed the W field in the Button Size inspector for the bottom button to match that for the top button. The button would have grown wider when you committed the change. You might still have had to move it to line up with the guides.
Before fixing the two new buttons' autosizing behavior, have a little fun with their default behavior. Choose File > Simulate Interface and resize the window. You see that their locations remain fixed relative to the upper-left corner of the window, creating the comical impression that they are moving up into the split view or disappearing off the bottom edge of the window.
Quit the Cocoa Simulator. To fix the problem, select the top button. In the Autosizing section of the Button Size inspector, disable the top strut and enable the bottom strut, leaving the left strut enabled. Do the same with the bottom button. This is standard practice with controls that are located in the bottom-left quadrant of a resizable window.
Now run the Cocoa Simulator again, and the buttons behave properly, remaining locked in place relative to the lower-left corner of the window.
Go back to the Library window and navigate to Library > Cocoa > Views & Cells > Inputs & Values. Scroll down to the Date Picker and drag it to the upper-right corner of the empty space at the bottom of the Chef's Diary window.
In the Date Picker Attributes inspector, make sure the "Month, Day and Year" and the "Hour, Minute, and Second" radio buttons are selected, and leave the other settings as you find them. Once you've finished this recipe, each entry in the diary will be marked by a date-time heading, down to the second, and the date picker will automatically display the date and time of the current entry. Displaying the seconds allows users to make multiple entries less than a minute apart.
Select the date picker in the window. You don't see the hour, minute, and second entries because by default the control is sized too small to reveal them. Choose Layout > Size to Fit, and the control widens to show both the date and the time elements. The button now runs off the edge of the window, so reposition it with the help of the guides.
In the Date Picker Size inspector, disable the left and top struts and enable the right and bottom struts to lock the control to the lower-right corner of the window.
Go back to the Library window, and still in the Inputs & Values section, drag a Search Field into the diary window and drop it below the date picker, using the guides to position it properly in the corner. The heights of the Add Tag push button and the search field are different. I normally use the guides to line up the center or the text baselines in this situation. This can be done by selecting the Add Entry button and the search field together and choosing Layout > Alignment > Align Horizontal Centers or Align Baselines.
Drag the search field's left edge to align with the left edge of the date picker, so that they are the same length.
In the Search Field Size inspector, set the struts the same as you did for the date picker.
Return to the Library window, and in Library > Cocoa > Views & Cells > Buttons, select a Square Button. Drag it into the diary window and drop it immediately to the left of the date picker.
In the Button Size inspector, note that the square button is 48 pixels wide by 48 pixels high. Enter 24 in both the W and H fields. After you commit each entry by pressing the Tab or Enter key, the button's dimensions visibly change. You are going to set up the square button and three others like it as navigation buttons grouped in a square arrangement, so they must be relatively small.
Still in the Button Size inspector, change the struts to match those of the search field and the date picker. All of these are navigation buttons, so they should be grouped together in the lower-right corner of the window.
Hold down the Option key and drag the square button downward. In Interface Builder, Option-drag creates an identical copy of a user interface element, including its springs and struts settings, and places it wherever you drop it.
Do the same thing two more times, until you have four buttons arranged in a square with their edges touching. The buttons are identical, including the springs and struts settings.
Select all four square buttons. You can do this by clicking one of them and Shift-clicking the other three, but it is easier to drag a selection rectangle until all four are selected. Drag the group until a guide shows you that it is the correct distance to the left of the search field and the date picker. The height of the group of square buttons does not exactly match the height of the search field and the date picker combined. With the four square buttons still selected as a group, press the up and down arrow keys to nudge the group up and down until it appears centered.
Now comes the interesting part. These four square buttons are to include graphic elements indicating their functions. The user will understand them at a glance if you apply up and down arrows similar to those on a DVD or CD player, but where are you going to find images like that? This is a perennial problem for software developers, few of whom are endowed with sufficient artistic talent to draw effective graphics. See the "How to Obtain Graphic Images" sidebar for some suggestions.
- For this book, I have created my own arrow images. In order to follow along with these instructions, download the Vermont Recipes project from the book's Web site and locate the four arrow images in the project folder. They are named ArrowBottom.pdf, ArrowDown.pdf, ArrowTop.pdf, and ArrowUp.pdf. Drag each of them into your working Vermont Recipes project folder, leaving them at the top level of the folder. If you don't have access to the book's Web site, find any PDF images and rename them to match these arrow names. They should be scalable vector PDF images.
- In Xcode, select the Resources group in the Groups & Files pane, and then choose Project > Add to Project. In the Open panel, navigate to the project folder, select all four images, and click Add. Set up the second sheet as you have done several times before and click Add. The four arrow images appear in the Resources group.
- The Resources group is getting a bit full, so create a new group within it and place the four arrow images in the new subgroup. One way to do this is to select all four images and then use the contextual menu on them and choose Group. A new subgroup is created for you with its default name selected for editing. Enter Images. The Group command placed the four arrow images in the subgroup for you.
- Go back to Interface Builder and select the top-left square button. In the Button Attributes inspector, open the Image pop-up menu, and there you see your four buttons. Choose ArrowTop. The upward-pointing arrow with a bar across the top appears in the button in the diary window. Because the image is a scalable vector PDF file, it is properly scaled with no effort on your part. To control scaling, use the Scaling pop-up menu. It is set by default to Proportionally Down, which works for these arrow buttons. If you used an image that is too small, change the setting to "Proportional Up or Down." Go through the same exercise with the other three square buttons, placing the ArrowBottom image in the lower-left corner, the ArrowUp image in the upper-right corner, and the ArrowDown image in the lower-right corner.
- To make sure the controls are positioned properly relative to one another and the sides and bottom of the window, move them around until you are satisfied that text baselines line up across the window, edges are aligned vertically, and margins comply with the guides to the extent that the other alignments allow. In the case of the navigation buttons, line up the images, not the borders; you will turn them into borderless buttons in a moment. Finally, adjust the placement of the bottom edge of the split view. Command-drag the bottom edge of the split view until the guides show that you have left the proper amount of space between it and the uppermost controls.
- Run the Cocoa Simulator now and resize the diary window. If you make it narrow enough, you notice that the controls overlap one another. To prevent this from happening, set the minimum width of the window to a value that leaves a reasonably wide space between the push buttons on the left and the new navigation buttons on the right. Apple's Human Interface Guidelines counsel that white space is one of the most effective tools to inform the user of functional groupings in the user interface. The two push buttons on the left insert new material into the diary's text, while the controls grouped on the right relate to navigation and selection of text.
The easiest way to set the window's minimum width is to resize it in Interface Builder. Hold down the Command key to make sure you resize and reposition all of the window's internal views and controls at once while you resize the window. When you have resized the window to the desired minimum size, turn to the Window Size inspector, select the Minimum Size checkbox, and click its Use Current button. The numbers in the Width and Height fields change to reflect the current size of the window. Use the Cocoa Simulator to confirm that it can no longer be resized to a smaller size.
Now that you've finished with the Cocoa Simulator, go back to the Button Attributes inspector and deselect the Bordered checkbox in the Visual section for all four navigation buttons. The window looks less cluttered without the square borders outlining the images, and the images are more easily understood if they stand free.
Finally, you should address the order in which the Tab key selects controls and other views. This is known as the window's tab order. Most windows have an initial first responder, and each of its views has a next key view. You use the initialFirstResponder and nextKeyView outlets to connect the views in the window in a complete circle known as the key view loop. If you don't set up the key view loop yourself, Cocoa does a reasonable job of guessing, but you shouldn't leave this to chance.
The typical user expects to begin typing in the diary window's text view after opening the diary window, without first having to click in the text view to select it for editing. Therefore, you should designate the text view in the top pane of the split view as the initial first responder.
From the initial first responder, tabbing proceeds from view to view in the window in an order that you can determine. Tabbing automatically skips views that are currently disabled. Within a complex control like the date picker, tabbing is already set up for you to move from element to element within the control in an appropriate order. To tab out of a text view, the user must press Control-Tab, since pressing Tab alone inserts a tab character into the text view. This is not true of text fields such as the search field, since tabs normally cannot be inserted in them. In System Preferences, users can elect to tab between views of any kind, not just text views and text fields, so you must always set the tab order for all of them.
The tab order of the views in the window should proceed roughly from top to bottom and left to right, but it is important to maintain functional groupings. A sensible tab order in the diary window is to start with the text view in the top pane of the split view, and then proceed to the Add Entry button, then to the Add Tag button, and then over to the group of navigation controls on the right. In that group, tabbing should select the top and bottom arrows, then the up and down arrows, and finally the date picker and the search field in that order. The last control should lead back to the text view, because tab order must always form a full circle.
The text views in the top and bottom panes of the split view are interesting. I suggest that the user should not be able to tab from the top text view to the bottom text view because the bottom one is usually collapsed. To a user pressing Control-Tab to tab out of the top text view, it would appear that nothing happened, and the Add Entry button would be selected only with a second press of Control-Tab. Both text views display the same text storage object, so even if both of them are expanded, a user does not need to tab from the top one to the bottom one. But if the user happens to be typing in the bottom pane, its next key view should be the Add Entry button, just as the top pane's next key view is the Add Entry button.
Start by selecting the diary window. In the Window Connections inspector, drag from the marker next to the initialFirstResponder outlet to the upper part of the top pane in the diary window's split view. You know you have selected the text view embedded in the scroll view when the term Text View appears in the window.
Next, select the text field in the top pane of the split view, and drag from the nextKeyView outlet to the Add Entry button. Do the same from the text field in the bottom pane of the split view, putting the nib file's window into outline or browser mode to make it easy to select the bottom text view without having to reposition the divider in the diary window. Select each remaining control in the window in turn and connect its nextKeyView outlet to the next control, ending back at the top text view. An alternate way to do this for each control is to Control-drag from one control to the next and select the nextKeyView outlet in its HUD.
- Save the nib file, and then run the Cocoa Simulator to ensure that the new controls behave properly as you resize the window.