diff --git a/main b/main index df9ca73..bad21ae 100755 Binary files a/main and b/main differ diff --git a/main.c b/main.c index 042edab..0f7d3ec 100644 --- a/main.c +++ b/main.c @@ -1,24 +1,41 @@ +/** + * Topaz's Pizza Timer + * Built for a future video + * https://theindustriousrabbit.video + * + * Copyright 2022 John Bintz + * MIT License + */ + #include +// basic amiga stuff +#include +#include + +// allow us to load and scale fonts +#include + +// timer stuff. using proto/timer.h did not work well, likely +// because of how you have to open the device. +#include + // this is the manual for the original intuition stuff. // not much here is different: https://ia801905.us.archive.org/33/items/Amiga_Intuition_Reference_Manaual_1985_Adn-Wesley_Publishing_Company/Amiga_Intuition_Reference_Manaual_1985_Addison-Wesley_Publishing_Company.pdf -#include -#include -#include -#include - -#include -#include - // gadtools gives a proper set of components for building UIs. // http://amigadev.elowar.com/read/ADCD_2.1/Libraries_Manual_guide/node0278.html +#include #include +#include + +// drawin' stuff +// This is where we get font handling #include +// the main app window #define WINDOW_WIDTH (240) -#define WINDOW_HEIGHT (100) -#define WINDOW_TITLE "Topaz Timer" - +#define WINDOW_HEIGHT (80) +#define WINDOW_TITLE "Topaz's Pizza Timer" #define WINDOW_CHROME_WIDTH (4) struct NewWindow winlayout = { @@ -27,7 +44,7 @@ struct NewWindow winlayout = { 0, 1, // detailpen, blockpen, // you have to add the different gadget types you're looking for // http://amigadev.elowar.com/read/ADCD_2.1/Includes_and_Autodocs_2._guide/node0106.html - IDCMP_CLOSEWINDOW | BUTTONIDCMP | SLIDERIDCMP, // IDCMP flags + IDCMP_REFRESHWINDOW | IDCMP_CLOSEWINDOW | BUTTONIDCMP | SLIDERIDCMP | IDCMP_MENUPICK, // IDCMP flags WFLG_SMART_REFRESH | WFLG_DRAGBAR | WFLG_DEPTHGADGET | WFLG_CLOSEGADGET | WFLG_ACTIVATE, // window flag from Window struct NULL, // FirstGadget NULL, // menu checkmark @@ -38,75 +55,130 @@ struct NewWindow winlayout = { WINDOW_WIDTH, WINDOW_HEIGHT, // max size, WBENCHSCREEN // screen where you want the window to open }; +struct Screen *screen; +struct Window *window; +struct Gadget *windowGadgets; +//struct NewWindow aboutWindowLayout = { }; +struct Window *aboutWindow; +struct Gadget *aboutWindowGadgets; + +// Fonts! struct TextAttr Topaz80 = { "topaz.font", 8, 0, 0 }; // There's no native 16 pixel Topaz font so we'll use // diskfont.library to make one. struct TextAttr Topaz160 = { "topaz.font", 16, 0, 0 }; +struct TextFont *topaz80Font; +struct TextFont *topaz160Font; +// Gadtools stuff #define START_STOP_BUTTON_ID 0 #define RESET_BUTTON_ID 1 #define HOURS_SLIDER_ID 2 #define MINUTES_SLIDER_ID 3 #define SECONDS_SLIDER_ID 5 -#define TIMER_COUNTDOWN_COUNT 3 - -struct Window *window; -struct TextFont *topaz80Font; -struct TextFont *topaz160Font; -struct Screen *screen; void *visualInfo; +struct Gadget *timerGadget, *hourSlider, *minuteSlider, *secondSlider; -struct Gadget *timerGadget; +// gadtools menus +// https://wiki.amigaos.net/wiki/GadTools_Menus +struct MenuData { + int id; +}; +#define MENU_ABOUT_ID 0 +#define MENU_QUIT_ID 1 + +struct MenuData MENU_ABOUT = { MENU_ABOUT_ID }; +struct MenuData MENU_QUIT = { MENU_QUIT_ID }; + +struct NewMenu appMenu[] = { + { NM_TITLE, "Pizza Timer", 0, 0, 0, 0, }, + { NM_ITEM, "About...", 0, 0, 0, &MENU_ABOUT }, + { NM_ITEM, "Quit", "Q", 0, 0, &MENU_QUIT }, + { NM_END, NULL, 0, 0, 0, 0, }, +}; + +struct Menu *menu; + +// timer stuff +#define TIMER_INTERVAL 200000 + +struct Device *TimerBase; +struct timerequest *TimerIO; +struct timeval currentSystemTime; + +// our business logic +// for how long should I cook this pizza? unsigned int uiHours = 0; unsigned int uiMinutes = 12; unsigned int uiSeconds = 0; -unsigned int activeHours; -unsigned int activeMinutes; -unsigned int activeSeconds; +unsigned int priorHours, originalUiHours; +unsigned int priorMinutes, originalUiMinutes; +unsigned int priorSeconds, originalUiSeconds; char timerText[9]; - -BOOL timerIsRunning; - -struct Device *TimerBase; -static struct IORequest timereq; +ULONG timerStartTime = 0, timerDistance, timerBuild; +BOOL timerIsRunning = FALSE; +BOOL timerStarted = FALSE; +BOOL terminated = FALSE; /** * Initialize system stuff. */ -int setup() { - // http://amigadev.elowar.com/read/ADCD_2.1/Includes_and_Autodocs_3._guide/node0308.html +// This function, despite taking no parameters, still needs void as a +// parameter. +// http://www.hipooniosamigasite.org/amigadocs/files/LOON-DOCS-DISKS/LOON5/LatticeC.pt3 +int setup(void) { + struct MsgPort *timerPort; + // make sure the font exists on the computer + // http://amigadev.elowar.com/read/ADCD_2.1/Includes_and_Autodocs_3._guide/node0308.html if (NULL == (topaz80Font = OpenFont(&Topaz80))) { return 0; } + // load this via disk so it can be scaled up + // http://amigadev.elowar.com/read/ADCD_2.1/Includes_and_Autodocs_3._guide/node0137.html if (NULL == (topaz160Font = OpenDiskFont(&Topaz160))) { return 0; } + // get a handle on the Workbench screen if (NULL == (screen = LockPubScreen(NULL))) { return 0; } + // Get...visual info. GadTools needs this, and it's all private. + // secrets. secrets between gadtools and the amiga. + // don't snoop. they wouldn't like it. if (NULL == (visualInfo = GetVisualInfo(screen, TAG_END))) { return 0; } - if (0 != (OpenDevice("timer.device", 0, &timereq, 0))) { + // set up the async timer message port + // we create this port, but it ends up in the timerequest structure when + // we're done with the device open, so we don't have to worry about + // hanging onto a pointer to it. + if (NULL == (timerPort = CreatePort(0, 0))) { + return 0; + } + // create an IO object we can get responses to on our timer port + if (NULL == (TimerIO = (struct timerequest *)CreateExtIO(timerPort, sizeof(struct timerequest)))) { + return 0; + } + // OpenDevice returns an error code on failure, 0 on success, like a Unix command + // Give OpenDevice the thing we will use to communicate with the timer device + if (0 != (OpenDevice(TIMERNAME, UNIT_MICROHZ, (struct IORequest *)TimerIO, 0L))) { return 0; } - TimerBase = timereq.io_Device; - - activeHours = uiHours; - activeMinutes = uiMinutes; - activeSeconds = uiSeconds; + // this allows us to use GetSysTime, rather than performing an IO operation + // to get the current time. + TimerBase = TimerIO->tr_node.io_Device; return 1; } @@ -114,19 +186,38 @@ int setup() { /** * Tear down system stuff. */ -void teardown() { +void teardown(void) { + // http://amigadev.elowar.com/read/ADCD_2.1/Devices_Manual_guide/node0196.html + struct MsgPort *tempTimerPort; + + if (TimerIO) { + CloseDevice((struct IORequest *)TimerIO); + tempTimerPort = TimerIO->tr_node.io_Message.mn_ReplyPort; + + if (tempTimerPort) { + DeletePort(tempTimerPort); + } + } + // all right, then. keep your secrets. if (visualInfo) FreeVisualInfo(visualInfo); if (screen) UnlockPubScreen(NULL, screen); if (topaz80Font) CloseFont(topaz80Font); if (topaz160Font) CloseFont(topaz160Font); - if (timereq.io_Device) CloseDevice(&timereq); } -struct Gadget *buildUI() { +/** + * Build the GadTools gadgets for this UI. + * Normally I'd move this to a separate piece of code, + * but for example purposes I want everything in this one file + * for easier searching. + */ +struct Gadget *buildUI(void) { struct NewGadget ng; struct Gadget *currentGadget; struct Gadget *glist; + // Add an empty gadget for GadTools to store extra data. + // http://amigadev.elowar.com/read/ADCD_2.1/Includes_and_Autodocs_3._guide/node0274.html currentGadget = CreateContext(&glist); ng.ng_LeftEdge = WINDOW_CHROME_WIDTH; @@ -139,7 +230,6 @@ struct Gadget *buildUI() { ng.ng_TextAttr = &Topaz80; ng.ng_VisualInfo = visualInfo; - // TODO: use constants to indicate which gadget is which ng.ng_GadgetID = NULL; ng.ng_Flags = PLACETEXT_IN; @@ -152,9 +242,8 @@ struct Gadget *buildUI() { TEXT_KIND, currentGadget, &ng, - GTTX_Text, "", + GTTX_Text, &timerText, GTTX_Justification, GTJ_CENTER, - GTTX_Border, TRUE, TAG_END ); @@ -188,26 +277,30 @@ struct Gadget *buildUI() { currentGadget, &ng, GT_Underscore, '_', - GA_Disabled, timerIsRunning, + GA_Disabled, timerIsRunning || !timerStarted, TAG_END ); ng.ng_LeftEdge = 85; ng.ng_Width = (WINDOW_WIDTH - WINDOW_CHROME_WIDTH * 2) - 85 + 4; ng.ng_TopEdge += 12; + // The level is also displayed to the left of the slider by default. + // We need to leave space for its rendering, which is done separately + // from label rendering. ng.ng_GadgetText = "Hours: "; ng.ng_Flags = PLACETEXT_LEFT; ng.ng_GadgetID = HOURS_SLIDER_ID; - currentGadget = CreateGadget( + hourSlider = currentGadget = CreateGadget( SLIDER_KIND, currentGadget, &ng, - GT_Underscore, '_', GTSL_Min, 0, GTSL_Max, 23, GTSL_Level, uiHours, + // You need both MaxLevelLen and LevelFormat for the label to display. GTSL_MaxLevelLen, 2, + // The level is also a long, so use long number formattting. GTSL_LevelFormat, "%2ld", GA_Disabled, timerIsRunning, TAG_END @@ -217,11 +310,10 @@ struct Gadget *buildUI() { ng.ng_GadgetText = "Mins: "; ng.ng_GadgetID = MINUTES_SLIDER_ID; - currentGadget = CreateGadget( + minuteSlider = currentGadget = CreateGadget( SLIDER_KIND, currentGadget, &ng, - GT_Underscore, '_', GTSL_Min, 0, GTSL_Max, 59, GTSL_Level, uiMinutes, @@ -235,11 +327,10 @@ struct Gadget *buildUI() { ng.ng_GadgetText = "Secs: "; ng.ng_GadgetID = SECONDS_SLIDER_ID; - currentGadget = CreateGadget( + secondSlider = currentGadget = CreateGadget( SLIDER_KIND, currentGadget, &ng, - GT_Underscore, '_', GTSL_Min, 0, GTSL_Max, 59, GTSL_Level, uiSeconds, @@ -252,170 +343,324 @@ struct Gadget *buildUI() { return glist; } -void setTimerText() { - // TODO: don't rerender if the time hasn't changed - sprintf(timerText, "%02d:%02d:%02d", activeHours, activeMinutes, activeSeconds); - GT_SetGadgetAttrs( - timerGadget, - window, - NULL, - GTTX_Text, &timerText, - TAG_DONE +void setTimerText(void) { + // Only change the timer widget text if the time is different + // from the last render. This prevents unnecessary renders and potential + // flashes of UI redraw. + if ( + (priorHours != uiHours) || + (priorMinutes != uiMinutes) || + (priorSeconds != uiSeconds) + ) { + // _Technically_ this will change the label's contents all on its own, + // but the newly-rendered label stomps on the old one without clearing + // it out, so we need to force the full label redraw with GT_SetGadgetAttrs. + sprintf(timerText, "%02d:%02d:%02d", uiHours, uiMinutes, uiSeconds); + + GT_SetGadgetAttrs( + timerGadget, + window, + NULL, + GTTX_Text, &timerText, + TAG_DONE + ); + + priorHours = uiHours; + priorMinutes = uiMinutes; + priorSeconds = uiSeconds; + } +} + + +/** + * Ask the timer to send a message to our message port + * after a defined number of milliseconds. + */ +void startTimer(void) { + TimerIO->tr_node.io_Command = TR_ADDREQUEST; + TimerIO->tr_time.tv_secs = 0; + TimerIO->tr_time.tv_micro = TIMER_INTERVAL; + + // SendIO is async, it doesn't wait for the IO to complete before continuing + SendIO((struct IORequest *)TimerIO); +} + +/** + * Render the UI. + */ +void renderUI(void) { + windowGadgets = buildUI(); + + // Use -1 for working with all gadgets. + // You need to add, refresh, and refresh window when adding + // GadTools gadgets. + AddGList(window, windowGadgets, -1, -1, NULL); + RefreshGList(windowGadgets, window, NULL, -1); + setTimerText(); + GT_RefreshWindow(window, NULL); +} + +/** + * Clear the current UI. + */ +void clearUI(void) { + RemoveGList(window, windowGadgets, -1); + FreeGadgets(windowGadgets); +} + +/** + * Toggle the timer between started and stopped. + */ +void handleToggleTimer(void) { + timerIsRunning = !timerIsRunning; + + // TODO: don't reset the timer when it's stopped/started + + if (timerIsRunning) { + // http://amigadev.elowar.com/read/ADCD_2.1/Includes_and_Autodocs_2._guide/node04FA.html + GetSysTime(¤tSystemTime); + + // timers are microsecond resolution + timerStartTime = currentSystemTime.tv_secs * 1000000 + currentSystemTime.tv_micro; + + priorHours = NULL; + priorMinutes = NULL; + priorSeconds = NULL; + + originalUiHours = uiHours; + originalUiMinutes = uiMinutes; + originalUiSeconds = uiSeconds; + + timerStarted = TRUE; + + // start the async timer + startTimer(); + } else { + GT_SetGadgetAttrs( + hourSlider, + window, + NULL, + GTSL_Level, uiHours, + TAG_DONE + ); + + GT_SetGadgetAttrs( + minuteSlider, + window, + NULL, + GTSL_Level, uiMinutes, + TAG_DONE + ); + + GT_SetGadgetAttrs( + secondSlider, + window, + NULL, + GTSL_Level, uiSeconds, + TAG_DONE + ); + } + + clearUI(); + renderUI(); +} + +/** + * Reset the timer. + */ +void handleResetTimer(void) { + uiHours = originalUiHours; + uiMinutes = originalUiMinutes; + uiSeconds = originalUiSeconds; + + timerStarted = FALSE; + + setTimerText(); + clearUI(); + renderUI(); +} + +/** + * Process a single Intuition message. + */ +void handleIntuitionMessage(struct IntuiMessage *iMessage) { + struct Gadget *targetGadget; + struct Menu *targetMenu; + struct MenuData *menuData; + + BOOL rerenderTimer = FALSE; + + switch (iMessage->Class) { + // We've released a button. + case IDCMP_GADGETUP: + targetGadget = (struct Gadget *)iMessage->IAddress; + + switch (targetGadget->GadgetID) { + case START_STOP_BUTTON_ID: + handleToggleTimer(); + + break; + case RESET_BUTTON_ID: + handleResetTimer(); + rerenderTimer = TRUE; + + break; + } + + break; + // We picked a menu item. + case IDCMP_MENUPICK: + // https://en.wikibooks.org/wiki/Aros/Developer/Docs/Libraries/GadTools + targetMenu = (struct Menu *)ItemAddress(menu, iMessage->Code); + + menuData = GTMENUITEM_USERDATA(targetMenu); + + switch (menuData->id) { + // I originally tried to use MENU_QUIT.id here + // https://stackoverflow.com/questions/14069737/switch-case-error-case-label-does-not-reduce-to-an-integer-constant + case MENU_QUIT_ID: + terminated = TRUE; + break; + } + break; + // We've moved the mouse. In slider gadget talk, we've changed + // the value of the slider. + case IDCMP_MOUSEMOVE: + targetGadget = (struct Gadget *)iMessage->IAddress; + + switch (targetGadget->GadgetID) { + case HOURS_SLIDER_ID: + uiHours = iMessage->Code; + rerenderTimer = TRUE; + break; + case MINUTES_SLIDER_ID: + uiMinutes = iMessage->Code; + rerenderTimer = TRUE; + break; + case SECONDS_SLIDER_ID: + uiSeconds = iMessage->Code; + rerenderTimer = TRUE; + break; + } + + if (rerenderTimer) setTimerText(); + break; + + // bye bye + case IDCMP_CLOSEWINDOW: + terminated = TRUE; + break; + + // this is required if your window is refreshable and you're using gadtools. + // http://amigadev.elowar.com/read/ADCD_2.1/Libraries_Manual_guide/node026F.html + case IDCMP_REFRESHWINDOW: + GT_BeginRefresh(window); + GT_EndRefresh(window, TRUE); + break; + } +} + +void endTimer(void) { + // TODO: play an included IFF 8SVX sound + DisplayBeep(screen); + + uiHours = uiMinutes = uiSeconds = 0; + setTimerText(); + + timerIsRunning = FALSE; + timerStarted = FALSE; + + clearUI(); + renderUI(); +} + +void handleTimerMessage(void) { + GetSysTime(¤tSystemTime); + + timerDistance = ((currentSystemTime.tv_secs * 1000000 + currentSystemTime.tv_micro) - timerStartTime) / 1000000; + timerBuild = (originalUiHours * 3600 + originalUiMinutes * 60 + originalUiSeconds) - timerDistance; + + if (timerBuild <= 0) { + endTimer(); + } else { + uiHours = timerBuild / 3600; + uiMinutes = timerBuild / 60 % 60; + uiSeconds = timerBuild % 60; + setTimerText(); + + // this is like a setTimeout loop in JavaScript. keep re-running + // the same timer until we don't need it anymore. + startTimer(); + } +} + +// https://wiki.amigaos.net/wiki/GadTools_Menus +void buildMenu(void) { + menu = CreateMenus( + appMenu, TAG_END ); + + LayoutMenus(menu, visualInfo, TAG_END); + SetMenuStrip(window, menu); +} + +void clearMenu(void) { + ClearMenuStrip(window); + FreeMenus(menu); } int main() { - struct Gadget *gad; - - BOOL terminated = FALSE; - BOOL rerenderTimer = FALSE; - + // TODO: Menu bar with about menu and change the title to match the time struct IntuiMessage *iMessage; - struct Gadget *targetGadget; - struct timeval currentSystemTime; - - unsigned int timerRerenderCountdown = 0; - - // timeval tv_secs is ULONG and that will let us have accurate - // time counting via timer.device. - ULONG timerStartTime, timerDistance, timerBuild; - - timerIsRunning = FALSE; + struct MsgPort *TimerPort; + ULONG windowSignal, timerSignal, foundSignals; if (0 == setup()) { teardown(); return 1; } - GetSysTime(¤tSystemTime); - timerStartTime = currentSystemTime.tv_secs; + // get our timer message port so we can await on timer events + TimerPort = TimerIO->tr_node.io_Message.mn_ReplyPort; + // business logic setup + timerIsRunning = FALSE; + + // open the empty window window = OpenWindow(&winlayout); + if (!window) { + teardown(); + return 1; + } - gad = buildUI(); + // these create the bit mask for Wait() to listen to events on + windowSignal = 1L << window->UserPort->mp_SigBit; + timerSignal = 1L << TimerPort->mp_SigBit; - // use -1 for working with all gadgets - AddGList(window, gad, -1, -1, NULL); - RefreshGList(gad, window, NULL, -1); - GT_RefreshWindow(window, NULL); + renderUI(); - setTimerText(); + buildMenu(); - // what the fuck, you have to set the labels afterwards? - // you know what, it's better if it's explicit rather than the - // pointer shit it was trying to do before. - //GT_SetGadgetAttrs(one, window, NULL, GTTX_Text, "wow", TAG_DONE); - - // after doing anything with gadgets, you need to refresh them - //RefreshGList(gad, window, NULL, 3); - - // you son of a bitch, this is what you need - printf("sig signal %d\n", window->UserPort->mp_SigBit); while (!terminated) { - // what is a userport on a window // http://amigadev.elowar.com/read/ADCD_2.1/Libraries_Manual_guide/node02EB.html // http://www.amigadev.elowar.com/read/ADCD_2.1/Includes_and_Autodocs_2._guide/node038A.html - //WaitPort(window->UserPort); - // but we need a timer + foundSignals = Wait(windowSignal | timerSignal); - // this shoud be responsive enoug - Delay(4); - - if (timerIsRunning) { - timerRerenderCountdown += 1; - if (timerRerenderCountdown > TIMER_COUNTDOWN_COUNT) { - timerRerenderCountdown = 0; - - GetSysTime(¤tSystemTime); - - timerDistance = currentSystemTime.tv_secs - timerStartTime; - - timerBuild = (uiHours * 3600 + uiMinutes * 60 + uiSeconds) - timerDistance; - - activeHours = timerBuild / 3600; - activeMinutes = timerBuild / 60 % 60; - activeSeconds = timerBuild % 60; - - setTimerText(); + if (foundSignals & windowSignal) { + // drain the window event queue + while ((!terminated) && (iMessage = GT_GetIMsg(window->UserPort))) { + handleIntuitionMessage(iMessage); } } - while ((!terminated) && (iMessage = GT_GetIMsg(window->UserPort))) { - switch (iMessage->Class) { - case IDCMP_GADGETUP: - targetGadget = (struct Gadget *)iMessage->IAddress; - - switch (targetGadget->GadgetID) { - case START_STOP_BUTTON_ID: - timerIsRunning = !timerIsRunning; - - if (timerIsRunning) { - GetSysTime(¤tSystemTime); - - timerStartTime = currentSystemTime.tv_secs; - timerRerenderCountdown = 0; - rerenderTimer = TRUE; - } - - RemoveGList(window, gad, -1); - FreeGadgets(gad); - - gad = buildUI(); - AddGList(window, gad, -1, -1, NULL); - RefreshGList(gad, window, NULL, -1); - GT_RefreshWindow(window, NULL); - - break; - case RESET_BUTTON_ID: - rerenderTimer = TRUE; - activeHours = uiHours; - activeMinutes = uiMinutes; - activeSeconds = uiSeconds; - break; - } - - break; - case IDCMP_MOUSEMOVE: - targetGadget = (struct Gadget *)iMessage->IAddress; - - switch (targetGadget->GadgetID) { - case HOURS_SLIDER_ID: - activeHours = uiHours = iMessage->Code; - rerenderTimer = TRUE; - break; - case MINUTES_SLIDER_ID: - activeMinutes = uiMinutes = iMessage->Code; - rerenderTimer = TRUE; - break; - case SECONDS_SLIDER_ID: - activeSeconds = uiSeconds = iMessage->Code; - rerenderTimer = TRUE; - break; - } - break; - - case IDCMP_CLOSEWINDOW: - terminated = TRUE; - break; - case IDCMP_REFRESHWINDOW: - GT_BeginRefresh(window); - GT_EndRefresh(window, TRUE); - break; - } - - if (rerenderTimer) { - setTimerText(); - rerenderTimer = FALSE; - } + if ((foundSignals & timerSignal) && timerIsRunning) { + handleTimerMessage(); } } - RemoveGList(window, gad, -1); - FreeGadgets(gad); - - if (window) { - CloseWindow(window); - } + clearUI(); + clearMenu(); + CloseWindow(window); teardown(); diff --git a/main.h b/main.h new file mode 100644 index 0000000..e69de29