"Colors" - A Palm Project Example Jan Schaumann <jschauma@netmeister.org> Abstract A brief walkthrough of the development of a simple program for the Palm OS. Table of contents 1. Introduction 2. Overview 3. "Colors" - A Palm Project Example 3.1 Project Definition/Specifications 3.2 Project Design/Layout 3.3 Resource Definitions 3.4 Main Program 3.5 Additional Routines and Procedures 3.6 Makefile 4. The Code 4.1 Callback.h 4.2 Colors.c 4.3 Colors.h 4.4 Colors.rcp 4.5 ColorsGeneral.c 4.6 ColorsRsc.h 4.7 EventHandlers.c 4.8 Hex2rgb.c 4.8.1 Plain C code for hex2rgb 4.9 Makefile 4.10 Rgb2hex 4.10.1 Plain C code for rgb2hex 1 Introduction This document shows and explains the different stages of the development of a program for the Palm OS Platform. It is assumed that you are working in a Linux/Unix environment and have successfully installed and familiarized yourself with the necessary tools (gcc, pilrc, POSE etc). If you haven't done so, please read the following two documents: * PalmMisc.pdf - available from http://www.netmeister.org/palm/PalmMisc.pdf * POSE-HOWTO - available from http://www.netmeister.org/palm/POSE-HOWTO.pdf 2 Overview We will create a simple application called "Colors", which will allow the user to convert the Red-, Green- and Blue-Values of a color into its hexadecimal value and vice versa. The development will proceed in the following steps: 1. Project Definition/Specifications 2. Project Design/Layout 3. Resource Definitions 4. Main Program 5. Additional routines and procedures 6. Makefile 7. Testing and Debuggin These are only the main steps and everybody who has done any software development knows that a lot of other steps are involved, and that "Testing and Debuggin" will take place at virtually every step of the project. 3 "Colors" - A Palm Project Example 3.1 Project Definition/Specifications It's important to state the aim of the application at the beginning, no matter if you are writing the application for somebody else or just for fun. By thinking about what the program needs and how it is organized, what features the user might want, your project will get much more organized and thus easier to develop. These specifications also facilitate splitting up the project among different devlopers. Since this is a very simple example, you might think that this step is not neccessary, but let's do it anyway for educational purposes: The application "Colors" will allow the user to convert the Red-, Green- and Blue-Values of a color into its hexadecimal value and vice versa. It should have a menubar containing the items "Edit" ("Cut", "Copy", "Paste") and "Options" ("About Colors"). Each command should have a graffiti shortcut character. By keeping the layout of our application close to other Palm-Applications, we ensure that the user feels familiar with it. We want to be able to cut, copy and paste from/to the clipboard by using the menu or the graffiti shortcuts; the other menu-item will pop up a small alert with useful (?) information about the program. When the user leaves the application, we want to store his/her latest result in a database and show it when the application is opened the next time. We also need a small error-alert in case the user enters invalid characters. 3.2 Project Design/Layout We now need to think about what the program interface will look like. From the specifications we know that we need the menu and editable text-fields for the Red-, Green, Blue-, and Hex-value of the color. We then want two buttons, one which will convert from hexadecimal to RGB and one which converts from RGB to hexadecimal. Note that there are of course many other ways of designing the program. Our interface might look something like this: Menu Red: ___ Green: ___ Blue: ___ From RGB From Hex HexValue: ___ We do not want a frame around the form, so we want to specify the size of the form to be 160x160. 3.3 Resource Definitions Designing the layout and then transferring this layout into code is very easy. All you need to do is create a text file containing the specifications, and a resource-header which will define the ID for each element. (You can have pilrc assign the ID's automatically, but it will be easier for you lateron in the code if you give a name to each element.) Here is what our "Colors.rcp" file will look like: #include "ColorsRsc.h" MENU ID ColorsMainMenuBar BEGIN PULLDOWN "Edit" BEGIN MENUITEM "Cut " ID Cut "X" MENUITEM "Copy " ID Copy "C" MENUITEM "Paste " ID Paste "P" END PULLDOWN "Options" BEGIN MENUITEM "About Colors " ID Info "A" END END FORM ID ColorsForm AT (0 0 160 160) MENUID 1000 BEGIN TITLE "Palm Color Converter" LABEL "Red: " AUTOID AT (10 30) FIELD ID ColorsFormFieldRed AT (75 PrevTop 15 15) RIGHTALIGN FONT 0 UNDERLINED SINGLELINE MAXCHARS 3 NUMERIC LABEL "Green: " AUTOID AT (10 PrevBottom+5) FIELD ID ColorsFormFieldGreen AT (75 PrevTop 15 15) RIGHTALIGN FONT 0 UNDERLINED SINGLELINE MAXCHARS 3 NUMERIC LABEL "Blue: " AUTOID AT (10 PrevBottom+5) FIELD ID ColorsFormFieldBlue AT (75 PrevTop 15 15) RIGHTALIGN FONT 0 UNDERLINED SINGLELINE MAXCHARS 3 NUMERIC BUTTON "From RGB" ID ColorsFormButtonFromRgb AT (20 PrevBottom+10 AUTO AUTO) BUTTON "From Hex" ID ColorsFormButtonFromHex AT (PrevRight+20 PrevTop AUTO AUTO) LABEL "Hexvalue: " AUTOID AT (10 PrevBottom+20) FIELD ID ColorsFormFieldHex AT (75 PrevTop 32 15) RIGHTALIGN FONT 0 UNDERLINED SINGLELINE MAXCHARS 6 END ALERT ID AboutAlert INFORMATION BEGIN TITLE "About Colors" MESSAGE "The Palm Color Converter lets you convert the Red-, Green- and Blue values of a color into it's hexadecimal value and vice versa." BUTTONS "OK" ALERT ID TheError INFORMATION BEGIN TITLE "Error" MESSAGE "^1" BUTTONS "OK" END And that's all! The IDs are defined in the included headerfile "ColorsRsc.h": // ColorsRsc.h // defines constants for all the applications resources // // App Name: "Colors" // // App Version: "0.1" // Main Form #define ColorsForm 1000 // main menu #define ColorsMainMenuBar 1000 #define Cut 1001 #define Copy 1002 #define Paste 1003 #define Info 1004 // fields and buttons #define ColorsFormFieldRed 1010 #define ColorsFormFieldGreen 1011 #define ColorsFormFieldBlue 1012 #define ColorsFormButtonFromRgb 1013 #define ColorsFormButtonFromHex 1014 #define ColorsFormFieldHex 1015 // Alerts #define AboutAlert 2000 #define TheError 3000 As always, it's good practice to comment your code. Once you've written "Colors.rcp" and "ColorsRcp.h", you can preview your layout using pilrcui Colors.rcp: 3.4 Main Program Now it's time to write the main function (PilotMain), which is standard: DWord PilotMain(Word launchCode, Ptr cmdPBP, Word launchFlags) { Err err = 0; if (launchCode == sysAppLaunchCmdNormalLaunch) { if ((err = StartApplication()) == 0) { EventLoop(); StopApplication(); } } return err; } For this simple application we only check if it is launched via the standard launch code, we ignore all other possible ways of launching the application. StartApplication is standard as well - it just calls OpenDatabase and calls FrmGotoForm(ColorsForm) to display our main (and only) form. OpenDatabase creates the database for our application if it doesn't exist, and creates record zero containing a null string. If all this took place without any errors, EventLoop is entered (which will wait for events triggered by the users actions) and is left only when interrupted by the systems call to stop the application. If this is the case, we call StopApplication which will write the necessary data into the database. You can review the complete code for "Colors" at the end of this document. 3.5 Additional Routines and Procedures. In order for our form to process (handle) any incoming events, we need to write a Form Handler, which executes certain pieces of code according to the events we want to handle. Following is the function ColorsFormHandleEvent in parts: static Boolean ColorsFormHandleEvent(EventPtr event) { //initialization of local variables #ifdef __GNUC__ CALLBACK_PROLOGUE #endif index = 0; theForm = FrmGetActiveForm(); switch (event->eType) { case frmOpenEvent: FrmDrawForm(theForm); // here we also read the last record from the // database and insert it into the appropriate field } handled = true; break; case ctlSelectEvent: // here we handle the two buttons objID = event->data.ctlSelect.controlID; if (objID == ColorsFormButtonFromRgb) { convertFromRgb(); } else { convertFromHex(); } handled = true; break; case menuEvent: // here we handle the menu-events handled = true; } #ifdef __GNUC__ CALLBACK_EPILOGUE #endif return handled; } Note the inclusion of CALLBACK_PROLOGUE and CALLBACK_EPILOGUE. We need these since we are using GCC to compile the code. The GCC compiler's calling conventions differ from those in the Palm OS. In particular, the GCC compiler expects at startup that it can set up the A4 register (which it uses to access global variables) and that it will remain set throughout the life of the application. Unfortunately, this is not true when a GCC application calls a Palm OS routine that either directly or indirectly calls back to a GCC function. This can lead to spectacular applications crashes. Refer to the complete code at the end of this document to see what CALLBACK_PROLOGUE and CALLBACK_EPILOGUE do. This function calls convertFromRgb or convertFromHex, the functions actually responsible for the conversion, depending on which button the user taps. 3.6 Makefile Once we've written all the code, we need to compile and link it. This may not be very complicated for a small project like this, but for bigger projects this would be tedious work. I will not go into the details of Makefiles, see http://www.tw.gnu.org/software/make/make.html for further information. Here's the Makefile I used for our little project: APP =Colors ICONTEXT ="Colors" APPID =WRLD RCP =$(APP).rcp PRC =$(APP).prc SRC =$(APP).c GRC =$(APP).grc BIN =$(APP).bin CC =m68k-palmos-coff-gcc PILRC =pilrc TXT2BITM =txt2bitm OBJRES =m68k-palmos-coff-obj-res BUILDPRC =build-prc CFLAGS =-O0 -g $(DEFINES) $(INCLUDES) all: clean $(PRC) $(PRC): grc.stamp bin.stamp; $(BUILDPRC) $(PRC) $(ICONTEXT) $(APPID) *.grc *.bin $(LINKFILES) ls -l *.prc grc.stamp: $(APP) ; $(OBJRES) $(APP) touch $@ $(APP): $(SRC:.c=.o) ; $(CC) $(CFLAGS) $^ -o $@ bin.stamp: $(RCP) ; $(PILRC) $^ $(BINDIR) touch $@ %.o: %.c ; $(CC) $(CFLAGS) -c $< -o $@ clean: rm -rf *.o $(APP) *.bin *.grc *.prc *.stamp Note that I always "make clean", since I split up the functions over several files. There are other (maybe more elegant) possibilities to do this with Makefiles and I won't try to argue that this method is better. Once compilation is complete, you will find the file "Colors.prc" in your directory - this is the application ready to install on your Palm (or POSE). 4 The Code Following is the code for the following files: * Callbacks.h * Colors.c * Colors.h * Colors.rcp * ColorsGeneral.c * ColorsRsc.h * EventHandlers.c * Hex2rgb.c * Makefile * Rgb2hex.c 4.1 Callbacks.h #ifndef __CALLBACK_H__ #define __CALLBACK_H__ register void *reg_a4 asm("%a4"); #define CALLBACK_PROLOGUE \ void *save_a4 = reg_a4; asm("move.l %%a5,%%a4; sub.l #edata,%%a4" : :); #define CALLBACK_EPILOGUE reg_a4 = save_a4; #endif 4.2 Colors.c #include "Colors.h" #include "Rgb2hex.c" #include "Hex2rgb.c" #include "EventHandlers.c" #include "ColorsGeneral.c" static void EventLoop(void) { EventType event; Word error; do { EvtGetEvent(&event, evtWaitForever); if (!SysHandleEvent(&event)) { if (!MenuHandleEvent(0, &event, &error)) { if (!ApplicationHandleEvent(&event)){ FrmDispatchEvent(&event); } } } } while (event.eType != appStopEvent); } static Err OpenDatabase(void) { Err err = 0; UInt index = 0; VoidHand RecHandle; Ptr RecPointer; char nullstring = 0; myDB = DmOpenDatabaseByTypeCreator(myDBType, myAppID, dmModeReadWrite); if (!myDB) { if ((err = DmCreateDatabase(0, myDBName, myAppID, myDBType, false))!=0) return err; myDB = DmOpenDatabaseByTypeCreator(myDBType, myAppID, dmModeReadWrite); RecHandle = DmNewRecord(myDB, &index, 1); RecPointer = MemHandleLock(RecHandle); DmWrite(RecPointer, 0, &nullstring, 1); MemPtrUnlock(RecPointer); DmReleaseRecord(myDB, index, true); } return 0; } static Err StartApplication(void) { Err err = 0; err = OpenDatabase(); if (err) return err; FrmGotoForm(ColorsForm); return 0; } static Err StopApplication(void) { VoidHand myRecord; FormPtr theForm; char *theRecord; FieldPtr theField; Ptr RecPointer; UInt index = 0; theForm = FrmGetActiveForm(); theField = FrmGetObjectPtr(theForm, FrmGetObjectIndex (theForm, ColorsFormFieldHex)); theRecord = theField->text; if (theRecord!=NULL) { DmRemoveRecord(myDB, index); myRecord = DmNewRecord(myDB, &index, 1); myRecord = DmResizeRecord(myDB, index, 6); RecPointer = MemHandleLock(myRecord); DmWrite(RecPointer, 0, theRecord, 6); MemPtrUnlock(RecPointer); DmReleaseRecord(myDB, index, true); } DmCloseDatabase(myDB); } DWord PilotMain(Word launchCode, Ptr cmdPBP, Word launchFlags) { Err err = 0; if (launchCode == sysAppLaunchCmdNormalLaunch) { if ((err = StartApplication()) == 0) { EventLoop(); StopApplication(); } } return err; } 4.3 Colors.h #ifndef __COLORS_H #define __COLORS_H #include <Pilot.h> #include <stdio.h> #include <stdlib.h> #include "ColorsRsc.h" #ifdef __GNUC__ #include "Callbacks.h" #endif #define myAppID 'CLRS' #define myDBType 'Data' DmOpenRef myDB; char myDBName[] = "ColorsDB"; static int myIsXDigit(char); static void convertFromHex(void); static void AlertError(char *); static Boolean ColorsFormHandleEvent(EventPtr); static Boolean ApplicationHandleEvent(EventPtr); static void EventLoop(void); static Err OpenDatabase(void); static Err StartApplication(void); static Err StopApplication(void); DWord PilotMain(Word launchCode, Ptr cmdPBP, Word launchFlags); static void convertFromRgb(void); #endif 4.4 Colors.rcp See Section 3.3 4.5 ColorsGeneral.c #include "Colors.h" static void AlertError(char *theMessage) { FrmCustomAlert(TheError, theMessage, NULL, NULL); } 4.6 ColorsRsc.h See Section 3.3 4.7 EventHandlers.c #include "Colors.h" static Boolean ColorsFormHandleEvent(EventPtr event) { Boolean handled = false; int objID; FormPtr theForm; FieldPtr theField; VoidHand myRecord; UInt index; Ptr RecPointer; char *theRecord; #ifdef __GNUC__ CALLBACK_PROLOGUE #endif index = 0; theForm = FrmGetActiveForm(); switch (event->eType) { case frmOpenEvent: // get the record myRecord = DmGetRecord(myDB, index); theRecord = MemHandleLock(myRecord); MemHandleUnlock(myRecord); DmReleaseRecord(myDB, index, true); FrmDrawForm(theForm); if ((theRecord != NULL)&&(StrLen(theRecord)==6)) { // we have a valid record, display it FldInsert(FrmGetObjectPtr(theForm, FrmGetObjectIndex(theForm, ColorsFormFieldHex)), theRecord, 6); // and show the rgb - this way we don't need to store those! convertFromHex(); } handled = true; break; case ctlSelectEvent: objID = event->data.ctlSelect.controlID; if (objID == ColorsFormButtonFromRgb) { convertFromRgb(); } else { convertFromHex(); } handled = true; break; case menuEvent: if (event->data.menu.itemID == Cut) { // Cut objID = FrmGetFocus(theForm); FldCut(FrmGetObjectPtr(theForm, objID)); } else if (event->data.menu.itemID == Copy) { // Copy objID = FrmGetFocus(theForm); FldCopy(FrmGetObjectPtr(theForm, objID)); break; } else if (event->data.menu.itemID == Paste) { // Paste objID = FrmGetFocus(theForm); FldPaste(FrmGetObjectPtr(theForm, objID)); break; } else if (event->data.menu.itemID == Info) // display info FrmAlert(AboutAlert); handled = true; } #ifdef __GNUC__ CALLBACK_EPILOGUE #endif return handled; } static Boolean ApplicationHandleEvent(EventPtr event) { FormPtr frm; Int formId; Boolean handled = false; if (event->eType == frmLoadEvent) { // Load the form resource specified in the event, then activate it formId = event->data.frmLoad.formID; frm = FrmInitForm(formId); FrmSetActiveForm(frm); // Set the event Handler for the form. The handler of the currently active form // is called by FrmDispatchEvent each time it is called switch (formId) { case ColorsForm: FrmSetEventHandler(frm, ColorsFormHandleEvent); break; } handled = true; } return handled; } 4.8 Hex2rgb.c #include "Colors.h" static int myIsXDigit(char c) { if ((c=='0')||(c==NULL)) return 0; if (c=='1') return 1; if (c=='2') return 2; if (c=='3') return 3; if (c=='4') return 4; if (c=='5') return 5; if (c=='6') return 6; if (c=='7') return 7; if (c=='8') return 8; if (c=='9') return 9; if ((c=='a')||(c=='A')) return 10; if ((c=='b')||(c=='B')) return 11; if ((c=='c')||(c=='C')) return 12; if ((c=='d')||(c=='D')) return 13; if ((c=='e')||(c=='E')) return 14; if ((c=='f')||(c=='F')) return 15; return -1; } static void convertFromHex(void) { int i; char *FldHex; int flag=0; FormPtr theForm; char hex[6] = {'0','0','0','0','0','0'}; int tmp[3] = {0,0,0}; theForm = FrmGetActiveForm(); // delete old stuff FldDelete(FrmGetObjectPtr(theForm, FrmGetObjectIndex(theForm, ColorsFormFieldRed)), 0, 3); FldDelete(FrmGetObjectPtr(theForm, FrmGetObjectIndex(theForm, ColorsFormFieldGreen)), 0, 3); FldDelete(FrmGetObjectPtr(theForm, FrmGetObjectIndex(theForm, ColorsFormFieldBlue)), 0, 3); // get input FldHex = FldGetTextPtr(FrmGetObjectPtr(theForm, FrmGetObjectIndex(theForm, ColorsFormFieldHex))); // check if input is correct if (FldHex!=NULL) { for (i=0; i<6; i++) { // check if input is correct if (!flag){ hex[i] = FldHex[i]; if (myIsXDigit(hex[i])<0) { AlertError("Incorrect Input!"); flag = 1; } } } if (StrLen(hex)<6) { AlertError("Not enough characters!"); flag = 1; } if (!flag) { for (i=0;i<3;i++) { tmp[i] = 0; tmp[i] += myIsXDigit(hex[i*2])*16; tmp[i] += myIsXDigit(hex[i*2+1]); } } } else { for (i=0;i<3;i++) tmp[i]=0; } if (!flag) { FldInsert(FrmGetObjectPtr(theForm, FrmGetObjectIndex(theForm, ColorsFormFieldRed)), StrIToA(hex,tmp[0]), StrLen(StrIToA(hex, tmp[0]))); FldInsert(FrmGetObjectPtr(theForm, FrmGetObjectIndex(theForm, ColorsFormFieldGreen)), StrIToA(hex,tmp[1]), StrLen(StrIToA(hex, tmp[1]))); FldInsert(FrmGetObjectPtr(theForm, FrmGetObjectIndex(theForm, ColorsFormFieldBlue)), StrIToA(hex,tmp[2]), StrLen(StrIToA(hex, tmp[2]))); } } Note that the much more elegant way to convert the value in plain C as shown below will not work, as the printf -functions are implemented in a different way, just as filestreams are not as easy as in plain C. 4.8.1 Plain C code for hex2rgb #include <ctype.h> #include <stdio.h> #include <stdlib.h> char *color[] = { "Red:", "Green:", "Blue:" }; void error( const char *str ) { fprintf(stderr, "%s\n", str); exit(1); } int main(int argc, char *argv[]) { int i; unsigned long l; char *input = argv[1]; if(argc != 2) error("Usage: hex2rgb xxxxxx, where x=[a-fA-F0-9]"); if(strlen(input) != 6) error("The hexadecimal value of a color must have 6 characters!"); for( i = 0; i < strlen( input ); i++ ) if( !isxdigit( input[i] ) ) error( "Input is not a valid hexnumber" ); i = sscanf(input, "%6lx", &l); if(i <= 0) error("Error in hexvalue!"); for(i = 0; i<3; i++) { fprintf(stdout, "%d ", (int)( l & 0xFF0000 ) >> 16 ); l <<= 8; } fprintf(stdout, "\n"); return 0; } 4.9 Makefile See Section 3.6 4.10 Rgb2hex #include "Colors.h" static void convertFromRgb(void) { char *FldRed; char *FldGreen; char *FldBlue; int flag=0; FormPtr theForm; char tmp[8]; char result[6] = {'0','0','0','0','0','0'}; theForm = FrmGetActiveForm(); // delete old entry FldDelete(FrmGetObjectPtr(theForm, FrmGetObjectIndex(theForm, ColorsFormFieldHex)), 0, 6); // get the input FldRed = FldGetTextPtr(FrmGetObjectPtr(theForm, FrmGetObjectIndex(theForm, ColorsFormFieldRed))); FldGreen = FldGetTextPtr(FrmGetObjectPtr(theForm, FrmGetObjectIndex(theForm, ColorsFormFieldGreen))); FldBlue = FldGetTextPtr(FrmGetObjectPtr(theForm, FrmGetObjectIndex(theForm, ColorsFormFieldBlue))); // check if input is correct if ((FldRed!=NULL)&&(atoi(FldRed)>255)) { AlertError("Incorrect input!"); flag = 1; } if ((FldGreen!=NULL)&&(atoi(FldGreen)>255)) { AlertError("Incorrect input!"); flag = 1; } if ((FldBlue!=NULL)&&(atoi(FldBlue)>255)) { AlertError("Incorrect input!"); flag = 1; } // all's swell, we go on if (!flag) { if (FldRed == NULL) { result[0] = '0'; result[1] = '0'; } else { StrIToH(tmp, atoi(FldRed)); result[0] = tmp[6]; result[1] = tmp[7]; } if (FldGreen == NULL) { result[2] = '0'; result[3] = '0'; } else { StrIToH(tmp, atoi(FldGreen)); result[2] = tmp[6]; result[3] = tmp[7]; } if (FldBlue == NULL) { result[4] = '0'; result[5] = '0'; } else { StrIToH(tmp, atoi(FldBlue)); result[4] = tmp[6]; result[5] = tmp[7]; } result[6] = '\0'; // write the correct hexvalue into the appropriate field FldInsert(FrmGetObjectPtr(theForm, FrmGetObjectIndex(theForm, ColorsFormFieldHex)), result, 6); } } 4.10.1 Plain C code for rgb2hex As before, you would implement this function in a slightly different way if using plain C: #include <stdlib.h> #include <stdio.h> #include <string.h> void error( const char *str ) { fprintf( stderr, "%s\n", str ); exit(1); } int main(int argc, char **argv) { char *input[3]; int i; if (argc!=4) { error("Usage: rgb2hex R G B (where 0 >= R,G,B (integer) <= 255)"); exit(1); } for (i=0; i<3; i++) { input[i] = argv[i+1]; } for (i=0; i<strlen(input[0]); i++) { if (!isdigit(input[0][i])) { error("Incorrect input!"); exit(1); } } for (i=0; i<strlen(input[1]); i++) { if (!isdigit(input[1][i])) { error("Incorrect input!"); exit(1); } if ((atoi(input[0])>255)||(atoi(input[1])>255)||(atoi(input[2])>255)) { error("Incorret input!"); exit(1); } fprintf(stdout,"%02x%02x%02x\n", atoi(input[0]), atoi(input[1]), atoi(input[2])); }