3.3 - New Time Zone Dialog#

In this section, we will create a dialog and add it to our clock application.

You will learn:

Creating a Panel#

A Panel is a special kind of window. Read the Cocoa document for more details: Windows and Panels.

Since I have a clock already, I want to know the time in different time zones. My idea is that when I click the title of the NSBox, a panel will show up and ask the time zone. Once the time zone is inputted, it will display the time in that area. Since NSPanel is a subclass of NSWindow, the usage of NSPanel is similar to NSWindow. Again, I need a “controller” to control the “view”, which is NSPanel in this case. The “view” is generated by Gorm, and I need to write the “controller” by myself. In this example, I’ll show how to load the gorm file. There are many built-in panel in GNUstep. I also use one in this example.

First, we need to build the interface for the panel. Open Gorm, and choose the menu item “Document → New Module → New Empty”. Look at the palettes. There is one for “Panel”.

Figure 4-22. Panel in Gorm

Drag the Panel out of Palettes. Build the interface as below.

Figure 4-23. Interface of time zone panel

You can change the size of panel in the inspector. Here are the attributes of the panel.

Figure 4-24. Panel attributes

Controlling the panel#

Now, I have the “view”. Then where is the “controller”? Generally, you can write a new class as the controller of this view, but this is a small program, so you don’t have to write a class just for the controller. So I decide to use the class TimeView as the controller for this panel. So TimeView acts as the custom view for that main window interface, and as the controller for the panel. Since TimeView is the controller of this panel, I need to connect the outlets and actions. Therefore, I need to create the class TimeView again in this gorm file, even though there is already one in TimeMachine.gorm file.

Editor’s note

This seems like a bad idea, but changing it would require a lot of refactoring of this tutorial.

You already know how to create the class TimeView. I add two outlets, zonePanel and zoneField, and two actions, okAction: and cancelAction:.

Figure 4-25. Outlets for time zone panel

Figure 4-26. Actions for time zone panel

But rather than creating an instance to connect to the panel, let’s set the owner of this panel to our TimeView. This reduces the amount of instances in our project.

Select the NSOwner in Gorm main window, then select class TimeView in the Attributes tab of the inspector.

Figure 4-27. Set NSOwner to TimeView class

By this way, I can connect the panel to the NSOwner, which is an instance of class TimeView. Connect the two buttons to the actions in NSOwner, the outlet zoneField to the NSTextField in the panel, and the outlet zonePanel to the panel. Pay attention to how the NSOwner connects to the panel.

Figure 4-28. Connect outlet

Save this interface as TimeZonePanel.gorm, and quit Gorm. Don’t generate the files for class TimeView because I already have the files. GNUstep can figure out where the classes are and where the outlets/actions are.

The code#

Now, I need to add the new outlets and actions into the files of TimeView. Here is the header.

TimeView.h:

#import <AppKit/AppKit.h>
#import "ClockView.h"

@interface TimeView : NSControl
{
    id zonePanel;
    id zoneField;
    NSBox* box;
    NSTextField *labelDate, *labelTime;
    NSTextField *localDate, *localTime;
    NSCalendarDate* date;
    ClockView* clockView;
}

- (NSCalendarDate *) date;
- (void) setDate: (NSCalendarDate *) date;
- (void) okAction: (id) sender;
- (void) cancelAction: (id) sender;
@end

I add the outlets and actions by myself.

TimeView.m:

- (void) mouseDown: (NSEvent *) event {
    NSRect titleFrame = [box titleRect];
    NSPoint windowLocation = [event locationInWindow];
    NSPoint viewLocation = [self convertPoint: windowLocation fromView: [self superview]];
    BOOL status = NSMouseInRect(viewLocation, titleFrame, NO);
    if (status == YES) {
        [NSBundle loadNibNamed: @"TimeZonePanel.gorm" owner: self]; 
        [NSApp runModalForWindow: self->zonePanel];
    }
}

The method -mouseDown: is called when mouse is clicked within this view. Here, we calculate whether the mouse is clicked in the area of the title of the NSBox. If so, we use [NSBundle loadNibNamed: owner:] to load the window, and [NSApp runModalForWindow] to display it. Read Cocoa’s document about “How Modal Windows Work”.

Now, I just need to finish the actions part in TimeView.m.

TimeView.m:

- (void) cancelAction: (id) sender
{
    [NSApp abortModal];
    [self->zonePanel close];
}

- (void) okAction: (id) sender
{
    NSTimeZone* tempZone;
    tempZone = [NSTimeZone timeZoneWithName: [zoneField stringValue]];
    [NSApp stopModal];
    [zonePanel close];
    if (tempZone == nil) {
        NSRunAlertPanel(@"Warning!",
                        @"Wrong Time Zone !!",
                        @"OK", nil, nil);
    } else {
        [date setTimeZone: tempZone];
        [box setTitle: [tempZone description]];
        [self setDate: date];
    }
}

In method -okAction:, I use a built-in panel, NSRunAlertPanel. There are several built-in panels in GNUstep ready to use. Now, you can display the current time in different time zone.

Better keyboard access#

It is inconvenient to use this pop-up panel because you have to click the NSTextField before typing. Sometimes, it is more convenient to control the user interface via keyboard rather than mouse.

When a window pop-up, it is the “First Responder” – the first object to receive events and keypresses. But usually we want some other objects in this window to receive the keypresses. Therefore, we need to change the “first responder” of this window by using [NSWindow makeFirstResponder:].

When I want to use Tab key to switch between different views in the window, you need to assign the nextKeyView to the next view when the Tab key is pressed so that the Application Kit knows where the responder should be.

Finally, when I finish typing in the NSTextField, I want to hit the Return key to click so that I don’t need to move my hand out of the keyboard. In this case, since NSTextField is also a subclass of NSControl, I can set the target and action of NSTextField to be the same as the NSButton . Therefore, when I hit Return, it is equivalent to clicking on the button.

These are small tune-ups for the application, but it makes it easier to use the application.

First, let’s set the “first responder” of the window to the NSTextField:

TimeView.m:

- (void) mouseDown: (NSEvent *) event
{
    NSRect titleFrame = [self->box titleRect];
    NSPoint windowLocation = [event locationInWindow];
    NSPoint viewLocation = [self convertPoint: windowLocation fromView: [self superview]];
    BOOL status = NSMouseInRect(viewLocation, titleFrame, NO);
    if (status == YES) {
        [NSBundle loadNibNamed: @"TimeZonePanel.gorm" owner: self]; 
        [self->zonePanel makeFirstResponder: zoneField];
        [NSApp runModalForWindow: self->zonePanel];
    }
}

Only one line is enough. Now, when this panel shows up, the cursor will automatically be in the NSTextField.

Second, let’s set the target and action of NSTextField to be the same as the NSButton . Open the TimeZonePanel.gorm, and connect the NSTextField to the method -okAction: of the NSOwner. That’s it. Whenever you hit the Return key in the NSTextField, the method -okAction: is called.

Figure 4-29. Connection NSTextField action

Third, we need to connect the nextKeyView outlet between the views in the window. I’ll connect the nextKeyView of NSTextField to the NSButton , the nextKeyView outlet of the NSButton to the NSButton , and the nextKeyView outlet of the NSButton to the NSTextField. By doing that, I can switch between these views using the Tab key. Here, I just show how the nextKeyView of NSTextField connects to the NSButton .

Figure 4-30. Connect nextKeyView

Source code: Panel-src.tar.gz.