/** * 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 // 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 (80) #define WINDOW_TITLE "Topaz's Pizza Timer" #define WINDOW_CHROME_WIDTH (4) struct NewWindow winlayout = { 20, 20, // x, y WINDOW_WIDTH, WINDOW_HEIGHT, // w, h 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_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 WINDOW_TITLE, // title NULL, // default screen NULL, // bitmap WINDOW_WIDTH, WINDOW_HEIGHT, // min size 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 void *visualInfo; struct Gadget *timerGadget, *hourSlider, *minuteSlider, *secondSlider; // 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 priorHours, originalUiHours; unsigned int priorMinutes, originalUiMinutes; unsigned int priorSeconds, originalUiSeconds; char timerText[9]; ULONG timerStartTime = 0, timerDistance, timerBuild; BOOL timerIsRunning = FALSE; BOOL timerStarted = FALSE; BOOL terminated = FALSE; /** * Initialize system stuff. */ // 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; } // 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; } // 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; } /** * Tear down system stuff. */ 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); } /** * 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; // TODO: constantize these ng.ng_TopEdge = 11; ng.ng_Height = 12; ng.ng_Width = 0; ng.ng_GadgetText = NULL; ng.ng_TextAttr = &Topaz80; ng.ng_VisualInfo = visualInfo; ng.ng_GadgetID = NULL; ng.ng_Flags = PLACETEXT_IN; // Timer display ng.ng_Width = WINDOW_WIDTH - WINDOW_CHROME_WIDTH * 2; ng.ng_TextAttr = &Topaz160; ng.ng_Height = 20; timerGadget = currentGadget = CreateGadget( TEXT_KIND, currentGadget, &ng, GTTX_Text, &timerText, GTTX_Justification, GTJ_CENTER, TAG_END ); ng.ng_TextAttr = &Topaz80; ng.ng_Height = 12; ng.ng_Width = (WINDOW_WIDTH - WINDOW_CHROME_WIDTH * 2) / 2; ng.ng_TopEdge += 18; if (timerIsRunning) { ng.ng_GadgetText = "_Stop"; } else { ng.ng_GadgetText = "_Start"; } ng.ng_GadgetID = START_STOP_BUTTON_ID; currentGadget = CreateGadget( BUTTON_KIND, currentGadget, &ng, GT_Underscore, '_', TAG_END ); ng.ng_LeftEdge += (WINDOW_WIDTH - WINDOW_CHROME_WIDTH * 2) / 2; ng.ng_GadgetText = "_Reset"; ng.ng_GadgetID = RESET_BUTTON_ID; currentGadget = CreateGadget( BUTTON_KIND, currentGadget, &ng, GT_Underscore, '_', 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; hourSlider = currentGadget = CreateGadget( SLIDER_KIND, currentGadget, &ng, 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 ); ng.ng_TopEdge += 12; ng.ng_GadgetText = "Mins: "; ng.ng_GadgetID = MINUTES_SLIDER_ID; minuteSlider = currentGadget = CreateGadget( SLIDER_KIND, currentGadget, &ng, GTSL_Min, 0, GTSL_Max, 59, GTSL_Level, uiMinutes, GTSL_MaxLevelLen, 2, GTSL_LevelFormat, "%2ld", GA_Disabled, timerIsRunning, TAG_END ); ng.ng_TopEdge += 12; ng.ng_GadgetText = "Secs: "; ng.ng_GadgetID = SECONDS_SLIDER_ID; secondSlider = currentGadget = CreateGadget( SLIDER_KIND, currentGadget, &ng, GTSL_Min, 0, GTSL_Max, 59, GTSL_Level, uiSeconds, GTSL_MaxLevelLen, 2, GTSL_LevelFormat, "%2ld", GA_Disabled, timerIsRunning, TAG_END ); return glist; } 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() { // TODO: Menu bar with about menu and change the title to match the time struct IntuiMessage *iMessage; struct MsgPort *TimerPort; ULONG windowSignal, timerSignal, foundSignals; if (0 == setup()) { teardown(); return 1; } // 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; } // these create the bit mask for Wait() to listen to events on windowSignal = 1L << window->UserPort->mp_SigBit; timerSignal = 1L << TimerPort->mp_SigBit; renderUI(); buildMenu(); while (!terminated) { // 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 foundSignals = Wait(windowSignal | timerSignal); if (foundSignals & windowSignal) { // drain the window event queue while ((!terminated) && (iMessage = GT_GetIMsg(window->UserPort))) { handleIntuitionMessage(iMessage); } } if ((foundSignals & timerSignal) && timerIsRunning) { handleTimerMessage(); } } clearUI(); clearMenu(); CloseWindow(window); teardown(); return 0; }