MacCalendar.c

/*
    File:       MacCalendar
 
    Contains:   Control strip module for displaying a calendar.
 
    Written by: Martin Minow
 
    Copyright:  © 1994-1997 by Apple Computer, Inc., all rights reserved.
 
    Change History (most recent first):
 
    You may incorporate this sample code into your applications without
    restriction, though the sample code has been provided "AS IS" and the
    responsibility for its operation is 100% yours.  However, what you are
    not permitted to do is to redistribute the source as "DSC Sample Code"
    after having made changes. If you're going to re-distribute the source,
    we require that you make it clear in the source that the code was
    descended from Apple Sample Code, but that you've made changes.
 
    Based on the Status Bar Sample.c by Steve Christensen.
    
    File Type           sdev
    File Creator        SCAL    -- registered with DTS
    Resource Type       sdev
    Resource ID         0
    Resource Attributes purgeable
 
    Other options (MetroWerks, Think C 7.0):
        Set Require Prototypes, Check Pointer Types, All other warnings.
        Do not set trigraph recognition.
        Enable Apple extensions.
 
    MetroWerks link/project options:
        Link single segment
        Set Project type "Code Segment", Standard Header.
*/
 
/////////////////////////////////////////////////////////////////////////
 
// Pick up resource constants common to both control strip
// module and application.
 
#include "MacCalendarCommon.h"
 
/////////////////////////////////////////////////////////////////////////
 
// Pick up resource constants common to both C and Rez for
// the control strip module.
 
#include "MacCalendar.h"
 
/////////////////////////////////////////////////////////////////////////
 
// Pick up prototype for the core drawing code.
 
#include "DrawCalendar.h"
 
/////////////////////////////////////////////////////////////////////////
 
// Pick up system types.
 
#include <Fonts.h>
#include <Memory.h>
#include <Menus.h>
#include <Quickdraw.h>
#include <Resources.h>
#include <ToolUtils.h>
#include <Types.h>
#include <Windows.h>
#include <Icons.h>
#include <ControlStrip.h>
#include <Gestalt.h>
 
/////////////////////////////////////////////////////////////////////////
 
// Metrowerks uses A4 to reference globals. The A4-setup code was copied from the
// WDEF.c sample included in the Metrowerks DR3 distribution. MPW and Think C
// use PC-relative addressing in a single-segment code module.
 
#ifdef __MWERKS__
#include <A4Stuff.h>    //  also included in <MacHeaders>
#include <SetupA4.h>    //  required to handle callback functions
#endif
 
/////////////////////////////////////////////////////////////////////////
 
// Lots of global constants.
 
/*
 * Define the patterns as C-strings so they can be addressed as constants
 * within the program.
 */
#define kWhitePattern       ((ConstPatternParam) "\000\000\000\000\000\000\000\000")
#define kBlackPattern       ((ConstPatternParam) "\377\377\377\377\377\377\377\377")
 
enum {
    kIconWidth = 16         // Width of the icon on the control strip itself.
};
 
// Name under which our preferences are saved.
 
#define kCalendarPrefName   "\pMacCalendar Preferences"
 
// Indices into the STRN_Info STR# resource.
 
enum {
    kStringHelp = 1,
    kStringFontName,
    kStringFontSize,
    kStringFirstDayOfWeek,
    kStringDayNames
};
 
/*
 * This record defines the information we need to draw the calendar. It is initialized
 * when we are called with the sdevInitModule message, and passed to and from the
 * Status Bar manager.
 */
typedef struct GlobalRecord {
    Handle              iconSuite;              /* Status bar icon                          */
    Handle              textStrings;            /* Balloon help string etc.                 */
    PicHandle           rightArrowPicture;      /* Popup arrow                              */
    SavedSettingsHandle settings;               /* Preference settings                      */
    UInt32              lastSavedModCount;      /* The modCount setting when we last saved. */
} GlobalRecord, *GlobalPtr, **GlobalHandle;
 
/////////////////////////////////////////////////////////////////////////
 
// Save current status for restarts.
 
static OSErr
CtlStripSavePreferences(
        GlobalPtr               globalPtr
    )
{
        OSErr                   status;
 
        status = SBSavePreferences(kCalendarPrefName, (Handle) globalPtr->settings);
        if (status == noErr) {
            globalPtr->lastSavedModCount = (**globalPtr->settings).modCount;
        }
 
        return(status);
}
 
/////////////////////////////////////////////////////////////////////////
 
// Termination
 
static void
CtlStripCleanUp(
        GlobalHandle            globalHandle
    )
{
        register GlobalPtr      globalPtr;
 
        if (globalHandle != nil) {
            HLock((Handle) globalHandle);
            globalPtr = *globalHandle;
            if (globalPtr->iconSuite != NULL)
                DisposeIconSuite(globalPtr->iconSuite, TRUE);
            if (globalPtr->textStrings != NULL)
                DisposeHandle(globalPtr->textStrings);
            if (globalPtr->rightArrowPicture != NULL)
                DisposeHandle((Handle) globalPtr->rightArrowPicture);
            if (globalPtr->settings != NULL)
                DisposeHandle((Handle) globalPtr->settings);
            (void) ReplaceGestaltValue(kControlStripCreator, 0);
            DisposeHandle((Handle) globalHandle);
        }
}
 
/////////////////////////////////////////////////////////////////////////
 
// Initialization
 
static long
CtlStripInitialize(void)
{
        register GlobalHandle   globalHandle;
        register GlobalPtr      globalPtr;
        long                    result;
        Str255                  work;
        SavedSettingsHandle     prefsHandle;
        long                    tempLong;
        long                    gestaltResponse;
 
        globalHandle = nil;
        
        // We're using GestaltValue, so we have to make sure we have
        // System 7.5.
        
        result = Gestalt(gestaltSystemVersion, &gestaltResponse);
        if (result == noErr) {
            if (gestaltResponse < 0x0750) {
                result = unimpErr;
            }
        }
        
        // Register ourselves with Gestalt.  This will trigger
        // an error if we've been installed twice, which will cause the
        // second instance to fail sooner rather than fail later.
        
        if (result == noErr) {
            result = NewGestaltValue(kControlStripCreator, 0);
        }
        if (result == noErr) {
            globalHandle = (GlobalHandle) NewHandleSysClear(sizeof (GlobalRecord));
            result = noErr;
            if (globalHandle == NULL)
                result = MemError();
            else {
                HLock((Handle) globalHandle);
                globalPtr = *globalHandle;
                /*
                 * Load and detach the icon suite
                 */
                result = SBGetDetachIconSuite(&(globalPtr->iconSuite), ICON_StatusBar, svAllSmallData);
            }
        }
        if (result == noErr) {
            globalPtr->textStrings = GetResource('STR#', STRN_Info);
            result = ResError();
        }
        if (result == noErr) {
            DetachResource(globalPtr->textStrings);
            globalPtr->rightArrowPicture = GetPicture(PICT_RightArrow);
            if (globalPtr->rightArrowPicture == NULL)
                result = ResError();
        }
        if (result == noErr) {
            DetachResource((Handle) globalPtr->rightArrowPicture);
 
            /*
             * Get the saved preferences, if any, and configure the drawing
             * environment. Note that the sample status bar doesn't dispose
             * of the prefsHandle but I maintain that this is a bug.  While
             * the control strip documentation doesn't mention whether the
             * handle returned by SBLoadPreferences is a resource or a memory
             * handle, my testing indicates it's a memory handle.
             */
            prefsHandle = nil;
            result = SBLoadPreferences(kCalendarPrefName, (Handle *) &prefsHandle);
            if (result == noErr
             && prefsHandle != nil
             && GetHandleSize((Handle) prefsHandle) == sizeof (SavedSettings)
             && (**prefsHandle).signature == kControlStripCreator
             && (**prefsHandle).prefVersion == kPrefVersion) {
                /*
                 * Use the saved preference resource
                 */
                globalPtr->settings = prefsHandle;
                (**(globalPtr->settings)).modCount = 0;
                globalPtr->lastSavedModCount = 0;
            }
            else {
                /*
                 * Hmm, we don't have any preferences. Build a new preference resource.
                 */
                if (prefsHandle != nil) {
                    DisposeHandle( (Handle) prefsHandle);
                }
                prefsHandle = (SavedSettingsHandle) NewHandleSysClear(sizeof(SavedSettings));
                globalPtr->settings = prefsHandle;
                result = MemError();
                if (result == noErr) {
                    (**(globalPtr->settings)).signature = kControlStripCreator;
                    (**(globalPtr->settings)).prefVersion = kPrefVersion;
                    (**(globalPtr->settings)).modCount = 0;
                    globalPtr->lastSavedModCount = 1;
                    
                    SBGetDetachedIndString(work, globalPtr->textStrings, kStringDayNames);
                    pstrcpy((**(globalPtr->settings)).dayNameString, work);
                    
                    SBGetDetachedIndString(work, globalPtr->textStrings, kStringFontName);
                    pstrcpy((**(globalPtr->settings)).fontName, work);
                    
                    SBGetDetachedIndString(work, globalPtr->textStrings, kStringFontSize);
                    StringToNum(work, &tempLong);
                    (**(globalPtr->settings)).fontSize = tempLong;
                    
                    SBGetDetachedIndString(work, globalPtr->textStrings, kStringFirstDayOfWeek);
                    StringToNum(work, &tempLong);
                    (**(globalPtr->settings)).firstDayOfWeek = tempLong;
 
                }
            }
        }
 
        // Now that we successfully started up, publish our globals using
        // Gestalt.
        
        if (result == noErr) {
            result = ReplaceGestaltValue(kControlStripCreator, (long) globalPtr->settings);
        }
        /*
         * We've finished all initialization. If there is an error, exit through
         * CtlStripCleanUp to dispose of handles and other junk. If initialization
         * are successful, unlock the handle and return the handle cast to a long.
         */
        if (result != noErr) {
            CtlStripCleanUp(globalHandle);
        }
        else {
            HUnlock((Handle) globalHandle);
            result = (long) globalHandle;
        }
        return (result);
}
 
/////////////////////////////////////////////////////////////////////////
 
// Draw the icon in the status bar.
 
static long
CtlStripDrawStatusIcon(
        GlobalPtr               globalPtr,
        const Rect              *statusRect
    )
{
        Rect                    viewRect;
        short                   arrowHeight;
 
        viewRect = *statusRect;
        viewRect.right = viewRect.left + kIconWidth;
        (void) PlotIconSuite(&viewRect, atNone, ttNone, globalPtr->iconSuite);
        /*
         * Draw an right-arrow to show that we have a popup menu. Well, we don't
         * actually have a popup menu, but we do pop up a calendar when clicked on.
         */
        arrowHeight = height(PicFrame(rightArrowPicture));
        viewRect.left = viewRect.right;
        viewRect.right += width(PicFrame(rightArrowPicture));
        viewRect.top += ((height(viewRect) - arrowHeight) >> 1);
        viewRect.bottom = viewRect.top + arrowHeight;
        DrawPicture(globalPtr->rightArrowPicture, &viewRect);
        return (0);
}
 
/////////////////////////////////////////////////////////////////////////
 
// Position the calendar with respect to the status bar. If there is enough room
// above, put it above, else put it below. Left and right operate similarly.
 
enum {
    kCtlStripFrame = 4          /* The frame above/below the icon itself */
};
 
static void
GetDisplayRect(
        const Rect              *statusRect,
        Point                   displaySize,
        Rect                    *windowRect
    )
{
        Rect                    sBarRect;
 
        sBarRect = *statusRect;
        LocalToGlobal((Point *) &sBarRect.top);
        LocalToGlobal((Point *) &sBarRect.bottom);
        if (sBarRect.top - kCtlStripFrame - displaySize.v - (GetMBarHeight() + 2) > 0) {
            /*
             * The calendar is displayed above the status bar.
             */
            windowRect->bottom = sBarRect.top - kCtlStripFrame - 1;
            windowRect->top = windowRect->bottom - displaySize.v;
        }
        else {
            /*
             * The calendar is displayed below the status bar.
             */
            windowRect->top = sBarRect.bottom + kCtlStripFrame + 1;
            windowRect->bottom = windowRect->top + displaySize.v;
        }
        if (sBarRect.right - displaySize.h > 0) {
            /*
             * The calendar is displayed to the left of the calendar icon.
             */
            windowRect->right = sBarRect.right;
            windowRect->left = windowRect->right - displaySize.h;
        }
        else {
            /*
             * The calendar is displayed to the right of the calendar icon.
             */
            windowRect->left = sBarRect.left;
            windowRect->right = windowRect->left + displaySize.h;
        }
}
 
 
/////////////////////////////////////////////////////////////////////////
 
// Build the triangular "next month" and "previous month" buttons.
 
enum {
    kButtonSeparation = 4                   /* 1.0d3, was 2                 */
};
 
static void
MakeTriangularButtons(
        const Rect              *monthRect,
        PolyHandle              *leftButton,
        PolyHandle              *rightButton,
        Rect                    *leftButtonRect,
        Rect                    *rightButtonRect
    )
{
        FontInfo                fontInfo;
        short                   buttonSize;
        short                   halfSize;
        Rect                    bothButtonRect;
 
        GetFontInfo(&fontInfo);
        buttonSize = (fontInfo.ascent & ~1);    /* Round down to even value     */
        halfSize = buttonSize / 2;
        bothButtonRect = *monthRect;
        bothButtonRect.bottom -= (1 + fontInfo.leading);
        bothButtonRect.top = bothButtonRect.bottom - buttonSize;
        bothButtonRect.left =
            (width(*monthRect) >> 1) - buttonSize - kButtonSeparation;
        bothButtonRect.right =
            (width(*monthRect) >> 1) + buttonSize + kButtonSeparation;
        *leftButtonRect = bothButtonRect;
/* 1.0d4 +  */
        leftButtonRect->right = leftButtonRect->left + halfSize;
        *rightButtonRect = bothButtonRect;
        rightButtonRect->left = rightButtonRect->right - halfSize;
        *leftButton = OpenPoly();
            MoveTo(halfSize, 0);
            LineTo(halfSize, buttonSize);
            LineTo(0, halfSize);
            LineTo(halfSize, 0);
        ClosePoly();
        OffsetPoly(*leftButton, leftButtonRect->left, leftButtonRect->top);
        *rightButton = OpenPoly();
            MoveTo(0, 0);
            LineTo(halfSize, halfSize);
            LineTo(0, buttonSize);
            LineTo(0, 0);
        ClosePoly();
        OffsetPoly(*rightButton, rightButtonRect->left, rightButtonRect->top);
/* 1.0d4 -  */
}
 
/////////////////////////////////////////////////////////////////////////
 
// Now that we've done setup, do the actual mouse tracking. If the mouse hits the
// right button, advance the month and draw it. If it hits the left button, draw
// the previous month.
 
static void
DrawCalendarAndTrackMouse(
        GlobalPtr               globalPtr,
        WindowPtr               windowPtr,
        const Rect              *monthRect
    )
{
        Rect                    leftButtonRect;
        Rect                    rightButtonRect;
        PolyHandle              leftButton;
        PolyHandle              rightButton;
        unsigned long           nowSeconds;
        DateTimeRec             now;
        Point                   mousePt;
        short                   thisYear;
        short                   thisMonth;
        typedef enum {
            inNoButton = 0,
            inLeftButton = 1,
            inRightButton = 2
        } WhichButton;
        WhichButton             inButton;
        WhichButton             wasInButton;
        unsigned long           nextMonthTick;
        Boolean                 redrawButtons;
 
        FrameRect(monthRect);
        MakeTriangularButtons(
            monthRect,
            &leftButton,
            &rightButton,
            &leftButtonRect,
            &rightButtonRect
        );
        GetDateTime(&nowSeconds);
        SecondsToDate(nowSeconds, &now);
        inButton = wasInButton = inNoButton;
        thisYear = thisMonth = 0;
        InitCursor();
        while (WaitMouseUp()) {
            if (thisYear != now.year || thisMonth != now.month) {
                /*
                 * Draw the new month. Also make sure the buttons are drawn.
                 */
                redrawButtons = TRUE;
                EraseRect(&windowPtr->portRect);
                FrameRect(monthRect);
                switch (wasInButton) {
                case inLeftButton:
                    FillPoly(leftButton, kWhitePattern);
                    break;
                case inRightButton:
                    FillPoly(rightButton, kWhitePattern);
                    break;
                }
                FramePoly(leftButton);
                FramePoly(rightButton);
                DrawCalendar(
                    globalPtr->settings,
                    now.year,
                    now.month,
                    &windowPtr->portRect
                );
                thisYear = now.year;
                thisMonth = now.month;
            }                                           /* If drawing new month     */
            /*
             * Get the mouse and track it while it is in one of our buttons
             */
            GetMouse(&mousePt);
            if (PtInRect(mousePt, &leftButtonRect))
                inButton = inLeftButton;
            else if (PtInRect(mousePt, &rightButtonRect))
                inButton = inRightButton;
            else {
                inButton = inNoButton;
            }
            if (redrawButtons || inButton != wasInButton) {
                switch (wasInButton) {
                case inLeftButton:  FillPoly(leftButton, kWhitePattern);    break;
                case inRightButton: FillPoly(rightButton, kWhitePattern);   break;
                }
                switch (inButton) {
                case inLeftButton:  FillPoly(leftButton, kBlackPattern);    break;
                case inRightButton: FillPoly(rightButton, kBlackPattern);   break;
                }
                FramePoly(leftButton);
                FramePoly(rightButton);
                if (inButton != wasInButton && inButton != inNoButton)
                    nextMonthTick = 0;                  /* Force new month drawing  */
                redrawButtons = FALSE;
                wasInButton = inButton;
            }                                           /* If button click change   */
            if (inButton != inNoButton && TickCount() > nextMonthTick) {
                /*
                 * The user has clicked in a button, or has held the mouse
                 * down in a button for one second. Draw the appropriate month.
                 */
                nextMonthTick = TickCount() + 60;
                switch (inButton) {
                case inLeftButton:
                    if (--now.month <= 0) {             /* Previous month or year   */
                        now.month = 12;
                        --now.year;
                    }
                    break;
                case inRightButton:
                    if (++now.month > 12) {             /* Next month or year       */
                        now.month = 1;
                        ++now.year;
                    }
                    break;
                }                                       /* Which button was clicked */
            }                                           /* Moving to a new month    */
        }                                               /* Loop while mouse down    */
        KillPoly(leftButton);
        KillPoly(rightButton);
}
 
/////////////////////////////////////////////////////////////////////////
 
// Click in the status bar icon
 
static long
CtlStripMouseClick(
        GlobalPtr               globalPtr,
        const Rect              *statusRect
    )
{
        Rect                    windowRect;
        WindowPtr               windowPtr;
        GrafPtr                 savePort;
        Point                   displaySize;
        Rect                    monthRect;
 
        displaySize = GetCalendarDisplaySize(globalPtr->settings);
        displaySize.h += 2;
        displaySize.v += 2;
        GetDisplayRect(statusRect, displaySize, &windowRect);
        windowPtr = NewWindow(
                    NULL,
                    &windowRect,
                    "\p",
                    TRUE,
                    plainDBox,
                    (WindowPtr) -1L,
                    FALSE,                              /* No go-away box           */
                    0                                   /* No refCon                */
                );
        if (windowPtr != NULL) {
            GetPort(&savePort);
            SetPort(windowPtr);
            GetCalendarMonthRect(
                globalPtr->settings,
                &windowPtr->portRect,
                &monthRect
            );
            if (StillDown()) {
                /*
                 * Design the two triangular buttons that will be displayed on the
                 * bottom line of the calendar and create the polygons. Then draw
                 * the calendar and track the mouse while it's held down.
                 */
                DrawCalendarAndTrackMouse(
                    globalPtr,
                    windowPtr,
                    &monthRect
                );
            }
            SetPort(savePort);
            DisposeWindow(windowPtr);
        }
        return (0);
}
 
/////////////////////////////////////////////////////////////////////////
 
// Periodically we check the globals that we publish via Gestalt.  If they
// have been modified, we return sdevNeedToSave so that the Control Strip
// will call our CtlStripSavePreferences routine when an appropriate time
// to save comes around (ie the hard disk spins up).
 
static long
CtlStripPeriodicTickle(
        GlobalPtr               globalPtr,
        const Rect              *statusRect
    )
{
        #pragma unused(statusRect)
        OSErr result;
        
        result = 0;
        if (globalPtr->lastSavedModCount != (**globalPtr->settings).modCount) {
            result = (1 << sdevNeedToSave);
        }
        return (result);
}
 
/////////////////////////////////////////////////////////////////////////
 
// This "main" program is called by the Control Strip manager.  Note that,
// unlike the rest of this program, we must define a prototype for main
// because we can't just declare it "static".
 
pascal long                 main(
        unsigned long           message,
        GlobalHandle            globalHandle,
        const Rect              *statusRect,
        GrafPtr                 statusPort
    );
 
pascal long
main(
        unsigned long           message,
        GlobalHandle            globalHandle,
        const Rect              *statusRect,
        GrafPtr                 statusPort
    )
{
        #pragma unused(statusPort)
        short                   savedState;
        register GlobalPtr      globalPtr;
        long                    result;
        Str255                  helpString;
#ifdef __MWERKS__
        long                    oldA4 = SetCurrentA4();
#endif
 
        if (globalHandle != nil) {
            /*
             * We have already allocated the global record. Save its lock state, lock
             * the handle, and get the global pointer (so we can write (*globalPtr).something)
             */
            savedState = HGetState((Handle) globalHandle);
            HLock((Handle) globalHandle);
            globalPtr = *globalHandle;
        }
        result = 0;                                 /* Unknown message result       */
        switch (message) {
        case sdevInitModule:                        /* Initialize the module        */
            /*
             * Initialization always sets globalHandle to NULL to avoid the HSetState
             * at the exit routine. If CtlStripInitialize succeeds, it sets the result
             * to the global parameter.
             */
            globalHandle = NULL;
            result = CtlStripInitialize();          /* Do the initialization and    */
            break;                                  /* Return global or error code  */
        case sdevCloseModule:                       /* Clean up before closing      */
            CtlStripCleanUp(globalHandle);
            globalHandle = NULL;
            break;
        case sdevFeatures:                          /* Return feature bits          */
            result = (  (1<<sdevWantMouseClicks)    /* We handle mouse down         */
                      | (1<<sdevDontAutoTrack)      /* We track the mouse, too      */
                      | (1<<sdevHasCustomHelp)      /* Custom help string           */
                    );
            break;
        case sdevGetDisplayWidth:                   /* Return display width         */
            result = kIconWidth + width(PicFrame(rightArrowPicture));
            break;
        case sdevPeriodicTickle:                    /* Nothing else is happening    */
            result = CtlStripPeriodicTickle(globalPtr, statusRect);
            break;
        case sdevDrawStatus:                        /* Draw the status bar info     */
            result = CtlStripDrawStatusIcon(globalPtr, statusRect);
            break;
        case sdevMouseClick:                        /* Status bar click             */
            result = CtlStripMouseClick(globalPtr, statusRect);
            break;
        case sdevSaveSettings:                      /* Save changed settings        */
            result = CtlStripSavePreferences(globalPtr);
            break;
        case sdevShowBalloonHelp:                   /* Display custom balloon help  */
            /*
             * We don't really have a custom help string, but this shows how to do it.
             */
            SBGetDetachedIndString(helpString, globalPtr->textStrings, kStringHelp);
            SBShowHelpString(statusRect, helpString);
            break;
        default:
            // Ignore unknown messages.
            break;
        }
        
        if (globalHandle != NULL)                   /* If we have globals allocated */
            HSetState((Handle) globalHandle, savedState);   /* Restore lock state   */  
 
#ifdef __MWERKS__
        SetA4(oldA4);
#endif
        return (result);
}