# 3.2 - Analog Clock In this section, you will add an analog clock to your clock application. ![](3.2_nfig21.png) In order to do this, you will create a custom view and use `NSBezierPath` to draw on it. You can either work on your project folder from the previous section, or copy that folder. You could also start over -- it seems like this guide was meant for starting over. ## Custom views Gorm can work on custom views, which are designed by the programmer, and not supplied by the Application Kit. In this section, we'll start off by adding a custom view: **Figure 4-12. TimeMachine with custom view** ![](GSPT_files/CustomView-11.jpg) Since I want to use the custom view in Gorm, I have to design the class first. The custom view can inherit from `NSView`, or `NSControl`, depending on what kind of functions you want. Actually, `NSControl` is a subclass of `NSView`. So I will inherit from `NSControl`. Click on the small circle of `NSResponder` to open its subclasses, then do the same thing on `NSView` and `NSControl`. Now, you can see that many GUI components inherit from `NSControl`, ex. `NSTextField`. **Figure 4-13. `NSControl` in Gorm** ![](GSPT_files/CustomView-01.jpg) ## Creating a custom view I want my custom view, called `TimeView`, to inherit from `NSControl`. Choose `NSControl`, then select the menu item "Classes→Create Subclass...". Double-click to change the name. **Figure 4-14. Add subclass of `NSControl`** ![](GSPT_files/CustomView-02.jpg) You can notice that class `TimeView` also inherits 3 outlets and 7 actions from `NSControl`. Once the class `TimeView` is created, I can use it as a custom view. Build the interface as below: **Figure 4-15. Interface with custom view** ![](GSPT_files/CustomView-03.jpg) Look at the Attributes tab in the inspector for `CustomView`. Choose the class `TimeView`. **Figure 4-16. Change class of custom view** ![](GSPT_files/CustomView-04.jpg) The `CustomView` becomes `TimeView`. That's it ! **Figure 4-17. Custom view with `TimeView` class** ![](GSPT_files/CustomView-05.jpg) As I did before, create another class for the "controller". Add one outlet for this `TimeView`, and one action for the button. Name the outlet `timeView` and the action `showCurrentTime:`. **Figure 4-18. Add outlet** ![](GSPT_files/CustomView-06.jpg) **Figure 4-19. Add action** ![](GSPT_files/CustomView-07.jpg) Create an instance of `Controller`. Connect the button to the action `showCurrentTime:`, and the outlet `timeView` to the custom view `TimeView`. **Figure 4-20. Connect outlet** ![](GSPT_files/CustomView-08.jpg) ![](GSPT_files/CustomView-09.jpg) ![](GSPT_files/CustomView-10.jpg) Finally, create the class file for the classes `TimeView` and `Controller`. Save this application as `TimeMachine.gorm`. ## Coding our custom view Now, we need to code the class `TimeView`. The class `TimeView` is actually four `NSTextField`s in an `NSBox`. The reason that I made them in one class is because I can reuse it later on. Classes inherited from `NSView` will be initialized by calling method `-initWithFrame:`. Therefore, I only need to rewrite the method `-initWithFrame:` in the class `TimeView`. Here are the files: `TimeView.h:` ```objc #import @interface TimeView : NSControl { NSTextField *labelDate, *labelTime; NSTextField *localDate, *localTime; NSCalendarDate* date; } - (NSCalendarDate*) date; - (void) setDate: (NSCalendarDate*) date; @end ``` `TimeView.m:` ```objc #import #import "TimeView.h" @implementation TimeView - (id) initWithFrame: (NSRect) frame { NSBox *box; self = [super initWithFrame: frame]; box = [[NSBox alloc] initWithFrame: NSMakeRect( 0, 0, // x=0, y=0 frame.size.width, frame.size.height ) ]; [box setBorderType: NSGrooveBorder]; [box setTitlePosition: NSAtTop]; [box setTitle: @"Local Time"]; self->labelDate = [[NSTextField alloc] initWithFrame: NSMakeRect(10, 45, 35, 20)]; // x=10, y=45, width=35, height=20 [self->labelDate setStringValue: @"Date: "]; [self->labelDate setBezeled: NO]; [self->labelDate setBackgroundColor: [NSColor windowBackgroundColor]]; [self->labelDate setEditable: NO]; self->labelTime = [[NSTextField alloc] initWithFrame: NSMakeRect(10, 15, 35, 20)]; // x=10, y=15, width=35, height=20 [self->labelTime setStringValue: @"Time: "]; [self->labelTime setBezeled: NO]; [self->labelTime setBackgroundColor: [NSColor windowBackgroundColor]]; [self->labelTime setEditable: NO]; self->localDate = [[NSTextField alloc] initWithFrame: NSMakeRect(55, 45, 130, 20)]; self->localTime = [[NSTextField alloc] initWithFrame: NSMakeRect(55, 15, 130, 20)]; [box addSubview: self->labelDate]; [box addSubview: self->labelTime]; [box addSubview: self->localDate]; [box addSubview: self->localTime]; [self->labelDate release]; [self->labelTime release]; [self->localDate release]; [self->localTime release]; [self addSubview: box]; [box release]; return self; } - (NSCalendarDate *) date { return self->date; } - (void) setDate: (NSCalendarDate *) aDate { self->date = aDate; [self->date setCalendarFormat: @"%a, %b %e, %Y"]; [self->localDate setStringValue: [self->date description]]; [self->date setCalendarFormat: @"%H : %M : %S"]; [self->localTime setStringValue: [self->date description]]; } - (void) dealloc { [self->date release]; [super dealloc]; } @end ``` When Gorm generates the class files, it contains template code in it. Since we don't need any of the template code, we can safely remove them. In `TimeView.h`, we declare four `NSTextField`s to add to our view, and one `NSCalendarDate` to store the date. We also declare two accessory methods to set and get the date. In `-initWithFrame:`, we put four `NSTextField`s into an `NSBox`. And since class `TimeView` is a subclass of NSView, we add the `NSBox` as the subview of our `TimeView`. Other parts of this application should be very easy. Here are the files: `Controller.h:` ```objc #import #import "TimeView.h" @interface Controller : NSObject { id timeView; } - (void) showCurrentTime: (id)sender; @end ``` `Controller.m:` ```objc #import #import "Controller.h" @implementation Controller - (void) showCurrentTime: (id)sender { NSCalendarDate *date = [NSCalendarDate date]; [self->timeView setDate: date]; } @end ``` `main.m:` ```objc #import int main(int argc, const char *argv[]) { return NSApplicationMain (argc, argv); } ``` `GNUmakefile:` ```makefile include $(GNUSTEP_MAKEFILES)/common.make APP_NAME = TimeMachine TimeMachine_HEADERS = Controller.h TimeView.h TimeMachine_OBJC_FILES = main.m Controller.m TimeView.m TimeMachine_RESOURCE_FILES = TimeMachineInfo.plist TimeMachine.gorm TimeMachine_MAIN_MODEL_FILE = TimeMachine.gorm include $(GNUSTEP_MAKEFILES)/application.make ``` You should notice that I didn't instantiate the class `TimeView` in the class `Controller` because when I add an custom view to the window, it is instantiated automatically. I only need to specify the class the custom view should be. On the contrary, I have to instantiate the class `Controller` in Gorm because it is not a GUI component. Without instantiation, I can't connect the "controller" to the "view". If you encounter a compiler error, please see [the note below](#CompileNote). ```{figure} 3.2_nfig20.5.png Our app right now. ``` ------------------------------------------------------------------------ ## Drawing on our custom view Now, let's make an analog clock using our custom view: ```{figure} 3.2_nfig21.png Custom view with analog clock ``` It's very simple. We only need to add a new GUI component in the class TimeView. We'll call this new class `ClockView`. Since `ClockView` will be a subview of `TimeView`, when `TimeView` is updated, we also need to update the `ClockView`. `ClockView.h:` ```objc #import #import @interface ClockView : NSView { NSPoint posHour, posMinute; } - (void) setDate: (NSCalendarDate *) aDate; @end ``` `ClockView.m:` ```objc #import "ClockView.h" @implementation ClockView - (id) init { self = [super init]; self->posHour = NSMakePoint(0,0); self->posMinute = NSMakePoint(0,0); return self; } - (void) drawRect: (NSRect) frame { NSPoint origin = NSMakePoint(frame.size.width/2, frame.size.height/2); NSBezierPath* BP = [NSBezierPath bezierPathWithRect: [self bounds]]; [[NSColor yellowColor] set]; [BP fill]; BP = [NSBezierPath bezierPathWithRect: NSMakeRect(1, 1, frame.size.width-2, frame.size.height-2)]; [[NSColor blackColor] set]; [BP stroke]; BP = [NSBezierPath bezierPath]; [BP setLineWidth: 3]; [BP moveToPoint: origin]; [BP relativeLineToPoint: self->posHour]; [BP stroke]; [BP setLineWidth: 1]; [BP moveToPoint: origin]; [BP relativeLineToPoint: self->posMinute]; [BP stroke]; } - (void) setDate: (NSCalendarDate *) date { int hour = [date hourOfDay]; int minute = [date minuteOfHour]; float hour_x = 40*sin((M_PI*hour/6)+(M_PI*minute/360)); float hour_y = 40*cos((M_PI*hour/6)+(M_PI*minute/360)); float minute_x = 60*sin(M_PI*minute/30); float minute_y = 60*cos(M_PI*minute/30); self->posHour = NSMakePoint(hour_x, hour_y); self->posMinute = NSMakePoint(minute_x, minute_y); [self setNeedsDisplay: YES]; } @end ``` `ClockView` inherits from `NSView`. The most important method it should override is `-drawRect:`. When this view need to update, `-drawRect:` will be called. Therefore, I put all the drawing in this method. `NSBezierPath` is how GNUstep draws. I assign the path, set the color, then draw. There is a good article about drawing: [*Introduction to Cocoa Graphics, Part I*](https://web.archive.org/web/20120313141602if_/http://www.macdevcenter.com/pub/a/mac/2001/10/19/cocoa.html), [*Part II*](https://web.archive.org/web/20120313141607if_/http://macdevcenter.com/pub/a/mac/2001/11/06/cocoa.html). ```{admonition} Exercise A bit of code is needed to include `ClockView` in `TimeView`. One is to add ClockView as a subview of `NSBox` in `TimeView`. Another is to update ClockView when `TimeView` is update. In method -setDate: of `ClockView`, it uses `[self setNeedsDisplay: YES]` to make this view update. This modification is easy to do. You can play around it. If you don't want to do this exercise, you can see the modifications below. ``` ## Adding the analog clock to our custom view `TimeView.h`: ```objc #import #import "ClockView.h" @interface TimeView : NSControl { NSTextField *labelDate, *labelTime; NSTextField *localDate, *localTime; NSCalendarDate* date; ClockView* clockView; } - (NSCalendarDate*) date; - (void) setDate: (NSCalendarDate*) date; @end ``` `TimeView.m`: ```objc #import #import "TimeView.h" @implementation TimeView - (id) initWithFrame: (NSRect) frame { NSBox *box; self = [super initWithFrame: frame]; box = [[NSBox alloc] initWithFrame: NSMakeRect( 0, 0, // x=0, y=0 frame.size.width, frame.size.height ) ]; [box setBorderType: NSGrooveBorder]; [box setTitlePosition: NSAtTop]; [box setTitle: @"Local Time"]; self->clockView = [[ClockView alloc] initWithFrame: NSMakeRect( 0, 70, // x=0, y=70 frame.size.width, frame.size.height ) ]; self->labelDate = [[NSTextField alloc] initWithFrame: NSMakeRect(10, 45, 35, 20)]; // x=10, y=45, width=35, height=20 [self->labelDate setStringValue: @"Date: "]; [self->labelDate setBezeled: NO]; [self->labelDate setBackgroundColor: [NSColor windowBackgroundColor]]; [self->labelDate setEditable: NO]; self->labelTime = [[NSTextField alloc] initWithFrame: NSMakeRect(10, 15, 35, 20)]; // x=10, y=15, width=35, height=20 [self->labelTime setStringValue: @"Time: "]; [self->labelTime setBezeled: NO]; [self->labelTime setBackgroundColor: [NSColor windowBackgroundColor]]; [self->labelTime setEditable: NO]; self->localDate = [[NSTextField alloc] initWithFrame: NSMakeRect(55, 45, 130, 20)]; self->localTime = [[NSTextField alloc] initWithFrame: NSMakeRect(55, 15, 130, 20)]; [box addSubview: self->clockView]; [box addSubview: self->labelDate]; [box addSubview: self->labelTime]; [box addSubview: self->localDate]; [box addSubview: self->localTime]; [self->clockView release]; [self->labelDate release]; [self->labelTime release]; [self->localDate release]; [self->localTime release]; [self addSubview: box]; [box release]; return self; } - (NSCalendarDate *) date { return self->date; } - (void) setDate: (NSCalendarDate *) aDate { self->date = aDate; [self->date setCalendarFormat: @"%a, %b %e, %Y"]; [self->localDate setStringValue: [self->date description]]; [self->date setCalendarFormat: @"%H : %M : %S"]; [self->localTime setStringValue: [self->date description]]; [self->clockView setDate: self->date]; } - (void) dealloc { [self->date release]; [super dealloc]; } @end ``` `GNUmakefile`: ```objc include $(GNUSTEP_MAKEFILES)/common.make APP_NAME = TimeMachine TimeMachine_HEADERS = Controller.h TimeView.h ClockView.h TimeMachine_OBJC_FILES = main.m Controller.m TimeView.m ClockView.m TimeMachine_RESOURCE_FILES = TimeMachineInfo.plist TimeMachine.gorm TimeMachine_MAIN_MODEL_FILE = TimeMachine.gorm # Uncomment below if you get a compiler error about "undefined symbol: _Z15_Unwind_VRS_Set..." ADDITIONAL_LDFLAGS += -lgcc_s include $(GNUSTEP_MAKEFILES)/application.make ``` ```{figure} 3.2_nfig21.png Our app right now. ``` ## Building our app ### Note After compiling, I got the following error: ``` /home/pi/Projects/GNUstep/GSTutorial/3.1_TimeMachine/TimeMachine.app/TimeMachine: symbol lookup error: /usr/local/lib/libobjc.so.4.6: undefined symbol: _Z15_Unwind_VRS_SetP15_Unwind_Context20_Unwind_VRS_RegClassj30_Unwind_VRS_DataRepresentationPv ``` which is complaining that the function ```c++ _Unwind_VRS_Set(_Unwind_Context*, _Unwind_VRS_RegClass, unsigned int, _Unwind_VRS_DataRepresentation, void*) ``` was not found. You can work around this by adding ```makefile ADDITIONAL_LDFLAGS += -lgcc_s # Link to GCC C runtime # or ADDITIONAL_LDFLAGS += -lstdc++ # Link to GCC C++ standard library # or ADDITIONAL_LDFLAGS += -lc++ # Link to Clang C++ standard library ``` to your `GNUmakefile`, before the `include $(GNUSTEP_MAKEFILES)/application.make`.