/* * JOURNAL.C - Records all mouse and keyboard activity so that * it can be played back for demonstration of products, * reporting errors, etc. * * Copyright (c) 1987 by Davide P. Cervone * You may use this code provided this copyright notice is kept intact. */ #include "journal.h" /* * Version number and author: */ char version[32] = "Journal v1.0 (June 1987)"; char *author = "Copyright (c) 1987 by Davide P. Cervone"; /* * Macros used to check for end-of-journal */ #define CTRL_AMIGA (IEQUALIFIER_CONTROL | IEQUALIFIER_LCOMMAND) #define KEY_E 0x12 #define CTRL_AMIGA_E(e) ((((e)->ie_Qualifier & CTRL_AMIGA) == CTRL_AMIGA) &&\ ((e)->ie_Code == KEY_E)) /* * Match a command-line argument against a string (case insensitive) */ #define ARGMATCH(s) (stricmp(s,*Argv) == 0) /* * Functions that JOURNAL can perform */ #define SHOW_USAGE 0 #define WRITE_JOURNAL 1 #define JUST_EXIT 2 /* * Largest mouse move we want to record */ #define MAXMOUSEMOVE 32 /* * Macros to tell whether a mouse movement event can be compressed with * other mouse movement events */ #define MOUSEMOVE(e)\ ((e)->ie_Class == IECLASS_RAWMOUSE && (e)->ie_Code == IECODE_NOBUTTON &&\ ((e)->ie_Qualifier & IEQUALIFIER_RELATIVEMOUSE)) #define BIGX(e) ((e)->ie_X >= XMINMOUSE || (e)->ie_X <= -XMINMOUSE) #define BIGY(e) ((e)->ie_Y >= YMINMOUSE || (e)->ie_Y <= -YMINMOUSE) #define BIGTICKS(e) ((e)->my_Ticks > LONGTIME) #define NOTSAVED(e) ((e)->my_Saved == FALSE) #define SETSAVED(e) ((e)->my_Saved = TRUE) /* * Global Variables: */ struct MsgPort *InputPort = NULL; /* Port used to talk to Input.Device */ struct IOStdReq *InputBlock = NULL; /* request block used with Input.Device */ struct Task *theTask = NULL; /* pointer to our task */ LONG InputDevice = 0; /* flag whether Input.Device is open */ LONG theSignal = 0; /* signal used when an event is ready */ LONG ErrSignal = 0; /* signal used when an error occured */ LONG theMask; /* 1 << theSignal */ LONG ErrMask; /* 1 << ErrSignal */ UWORD Ticks = 0; /* number of timer ticks between events */ LONG TimerMics = 0; /* last timer event's micros field */ WORD xmove = XDEFMIN; /* distance to compress into one event */ WORD ymove = YDEFMIN; /* distance to compress into one event */ WORD smoothxmove = 1; /* distance for smoothed events */ WORD smoothymove = 1; /* distnace for smoothed events */ WORD xminmove, yminmove; /* distance actually in use */ UWORD SmoothMask = IEQUALIFIER_LCOMMAND; /* what keys are required for smoothing */ UWORD SmoothTrigger = 0xFFFF; /* any of these keys trigger smoothing */ int Action = WRITE_JOURNAL; /* action to be perfomed by JOURNAL */ int ArgMatched = FALSE; /* TRUE if a parameter matched OK */ int Argc; /* global version of argc */ char **Argv; /* global version of argv */ struct InputEvent **EventPtr = NULL; /* pointer to (pointer to next event) */ struct InputEvent *OldEvent = NULL; /* pointer to last event */ struct SmallEvent TinyEvent; /* packed event (ready to record) */ FILE *OutFile = NULL; /* where the events will be written */ char *JournalFile = NULL; /* name of the output file */ int NotDone = TRUE; /* continue looking for events? */ int NotFirstEvent = FALSE; /* TRUE after an event was recorded */ int PointerNotHomed = TRUE; /* TRUE until pointer is moved */ struct Interrupt HandlerData = /* used to add an input handler */ { {NULL, NULL, 0, 51, NULL}, /* Node structure (nl_Pri = 51) */ NULL, /* data pointer */ &myHandlerStub /* code pointer */ }; struct InputEvent PointerToHome = /* event to put pointer in upper-left */ { NULL, /* pointer to next event */ IECLASS_RAWMOUSE, /* ie_Class = RAWMOUSE */ 0, /* ie_SubClass */ IECODE_NOBUTTON, /* ie_Code = NOBUTTON (just a move) */ IEQUALIFIER_RELATIVEMOUSE, /* ie_Qualifier = relative move */ {-1000,-1000}, /* move far to left and top */ {0L,0L} /* seconds and micros */ }; struct SmallEvent TimeEvent = /* pause written at beginning of file */ { {0xE2, 0, 0}, /* MOUSEMOVE, NOBUTTON, X=0, Y=0 */ IEQUALIFIER_RELATIVEMOUSE, /* Qualifier */ 0x00A00000 /* 1 second pause */ }; /* * myHandler() * * This is the input handler that makes copies of the input events and sends * them the to main process to be written to the output file. * * The first time around, we add the PointerToHome event into the stream * so that the pointer is put into a known position. * * We check the event type of each event in the list, and do the following: * for Timer events, we increment the tick count (which tells how many ticks * have occured since the last recorded event); for raw key events, we check * whether a CTRL-AMIGA-E has been pressed (if so, we signal the main process * with a CTRL-E which tells it to remove the handler and quit); for raw * mouse and raw key events, we allocate memory for a new copy of the event * (and signal an error if we can't), and copy the pertinent information * from the current event into the copy event and mark it as not-yet-saved. * We link it into the copied-event list (via EventPtr), and signal the * main task that a new event is ready, and then zero the tick count. * * Any other type of event is ignored. * * When we are through with the event list, we return it so that Intuition * can use it to do its thing. */ struct InputEvent *myHandler(event,data) struct InputEvent *event; APTR data; { struct InputEvent *theEvent = event; struct InputEvent *theCopy; Forbid(); if (PointerNotHomed) { PointerToHome.ie_NextEvent = event; event = &PointerToHome; PointerNotHomed = FALSE; } while(theEvent) { switch(theEvent->ie_Class) { case IECLASS_TIMER: Ticks++; TimerMics = theEvent->ie_Mics; break; case IECLASS_RAWKEY: if (CTRL_AMIGA_E(theEvent)) Signal(theTask,SIGBREAKF_CTRL_E); case IECLASS_RAWMOUSE: theCopy = NEWEVENT; if (theCopy == NULL) { Signal(theTask,ErrMask); } else { theCopy->ie_NextEvent = NULL; theCopy->ie_Class = theEvent->ie_Class; theCopy->ie_Code = theEvent->ie_Code; theCopy->ie_Qualifier = theEvent->ie_Qualifier; theCopy->ie_EventAddress = theEvent->ie_EventAddress; theCopy->my_Time = TIME; theCopy->my_Ticks = Ticks; theCopy->my_Saved = FALSE; *EventPtr = theCopy; EventPtr = &(theCopy->ie_NextEvent); Signal(theTask,theMask); Ticks = 0; } break; } theEvent = theEvent->ie_NextEvent; } Permit(); return(event); } /* * Ctrl_C() * * Dummy routine to disable Lattice-C CTRL-C trapping. */ #ifndef MANX int Ctrl_C() { return(0); } #endif /* * DoExit() * * General purpose exit routine. If 's' is not NULL, then print an * error message with up to three parameters. Free any memory, close * any open files, delete any ports, free any used signals, etc. */ void DoExit(s,x1,x2,x3) char *s, *x1, *x2, *x3; { long status = 0; if (s != NULL) { printf(s,x1,x2,x3); printf("\n"); status = RETURN_ERROR; } if (OldEvent) FREEVENT(OldEvent); if (OutFile) fclose(OutFile); if (InputDevice) CloseDevice(InputBlock); if (InputBlock) DeleteStdIO(InputBlock); if (InputPort) DeletePort(InputPort); if (theSignal) FreeSignal(theSignal); if (ErrSignal) FreeSignal(ErrSignal); exit(status); } /* * CheckNumber() * * Check a command-line argument for the given keyword, and if it matches, * makes sure that the next parameter is a positive numeric value that * is less than the maximum expected value. */ void CheckNumber(keyword,value) char *keyword; WORD *value; { long lvalue = *value; if (Argc > 1 && ARGMATCH(keyword)) { ArgMatched = TRUE; Argc--; if (sscanf(*(++Argv),"%ld",&lvalue) != 1) { printf("%s must be numeric: '%s'\n",keyword,*Argv); Action = JUST_EXIT; } if (lvalue < 1 || lvalue > MAXMOUSEMOVE) { printf("%s must be positive and less than %d: '%ld'\n", keyword,MAXMOUSEMOVE,lvalue); Action = JUST_EXIT; } *value = lvalue; } } /* * CheckHexNum() * * Check a command-line argument for the given keyword, and if it * matches, make sure that the next parameter is a legal HEX value. */ void CheckHexNum(keyword,value) char *keyword; WORD *value; { ULONG lvalue = *value; if (Argc > 1 && ARGMATCH(keyword)) { ArgMatched = TRUE; Argc--; if (sscanf(*(++Argv),"%lx",&lvalue) != 1) { printf("%s must be a HEX number: '%s'\n",keyword,*Argv); Action = JUST_EXIT; } *value = lvalue; } } /* * ParseArguements() * * Check that all the command-line arguments are valid and set the * proper variables as requested by the user. If no keyword is specified, * assume "TO". If no file is specified, then show the usage. DX and DY * set the "granularity" of the mouse moves recorded (moves are combined * into a single event until it's movement excedes either DX or DY). * Similarly, SMOOTHX and SMOOTHY set alternate DX and DY values * for when extra precision is needed (i.e., when drawing curves in a paint * program). SMOOTH specifies what qualifier keys MUST be present to * activate the SMOOTHX and SMOOTHY values, and TRIGGER specifies a set of * qualifiers any one of which (together with the SMOOTH qualifiers) * will active the SMOOTHX and SMOOTHY values. In other words, all the * SMOOTH qualifiers plus at least one of the TRIGGER qualifiers must be * pressed in order to activate the smooth values. For example, if SMOOTH * is 0 and TRIGGER is 0x6000, then holding down either the left or the * right button will activate the smooth values. If SMOOTH is 0x0040 rather * than 0, then the left Amiga button must also be held down in order to * activate SMOOTHX and SMOOTHY. The qualifier flags are listed in * DEVICES/INPUTEVENT.H * * The default values are DX = 8, DY = 8, SMOOTHX = 1, SMOOTHY = 1, * SMOOTH = left Amiga, TRIGGER = 0xFFFF. */ int ParseArguments(argc,argv) int argc; char **argv; { Argc = argc; Argv = argv; while (--Argc > 0) { ArgMatched = FALSE; Argv++; if (Argc > 1 && ARGMATCH("TO")) { JournalFile = *(++Argv); Argc--; ArgMatched = TRUE; } CheckNumber("DX",&xmove); CheckNumber("DY",&ymove); CheckNumber("SMOOTHX",&smoothxmove); CheckNumber("SMOOTHY",&smoothymove); CheckHexNum("SMOOTH",&SmoothMask); CheckHexNum("TRIGGER",&SmoothTrigger); if (ArgMatched == FALSE) { if (JournalFile == NULL) JournalFile = *Argv; else Action = SHOW_USAGE; } } if (JournalFile == NULL && Action == WRITE_JOURNAL) Action = SHOW_USAGE; return(Action); } /* * OpenJournal() * * Open the journal file and check for errors. Write the version * information to the file. */ void OpenJournal() { OutFile = fopen(JournalFile,"w"); if (OutFile == NULL) DoExit("Can't Open Journal File '%s', error %ld",JournalFile,_OSERR); if (fwrite(version,sizeof(version),1,OutFile) != 1) DoExit("Error writing to output file: %ld",_OSERR); } /* * GetSignal() * * Allocate a signal (error if none available) and set the mask to * the proper value. */ void GetSignal(theSignal,theMask) LONG *theSignal, *theMask; { LONG signal; if ((signal = AllocSignal(-ONE)) == -ONE) DoExit("Can't Get Signal"); *theSignal = signal; *theMask = (ONE << signal); } /* * SetupTask() * * Find the task pointer for the main task (so the input handler can * signal it). Clear the CTRL signal flags (so we don't get any left * over from before JOURNAL was run) and allocate some signals for * new events and errors (so the input handler can signal them). */ void SetupTask() { theTask = FindTask(NULL); SetSignal(0L,SIGBREAKF_ANY); GetSignal(&theSignal,&theMask); GetSignal(&ErrSignal,&ErrMask); #ifndef MANX onbreak(&Ctrl_C); #endif } /* * SetupEvents() * * Get a fake old-event to start off with, and mark it as saved (so we don't * really try to use it). Make it the end of the list (set its next pointer * to NULL. Tell the input handler where to start allocating new events * by setting EventPtr to point the the next-pointer. When the input * handler allocates a new copy of an event, it will link it to this one * so the main process can find it by following the next-pointer from the * old event. */ void SetupEvents() { if ((OldEvent = NEWEVENT) == NULL) DoExit("No Memory for OldEvent"); SETSAVED(OldEvent); OldEvent->ie_NextEvent = NULL; EventPtr = &(OldEvent->ie_NextEvent); } /* * AddHandler() * * Add the input handler to the input.device handler chain. Since the * priority is 51, it will appear BEFORE intuition, so all it should * see are raw key, raw mouse, timer, and disk insert/remove events. */ void AddHandler() { long status; if ((InputPort = CreatePort(0,0)) == NULL) DoExit("Can't Create Port"); if ((InputBlock = CreateStdIO(InputPort)) == NULL) DoExit("Can't Create Standard IO Block"); InputDevice = (OpenDevice("input.device",0,InputBlock,0) == 0); if (InputDevice == 0) DoExit("Can't Open Input Device"); InputBlock->io_Command = IND_ADDHANDLER; InputBlock->io_Data = (APTR) &HandlerData; if (status = DoIO(InputBlock)) DoExit("Error from DoIO: %ld",status); printf("%s - Press CTRL-AMIGA-E to End Journal\n",version); } /* * RemoveHandler() * * Remove the input handler from the input.device handler chain. */ void RemoveHandler() { long status; if (InputDevice && InputBlock) { InputBlock->io_Command = IND_REMHANDLER; InputBlock->io_Data = (APTR) &HandlerData; if (status = DoIO(InputBlock)) DoExit("Error from DoIO: %ld",status); } printf("Journal Complete\n"); } /* * SaveEvent() * * Pack an InputEvent into a SmallEvent (by shifting bits around) so that * it takes up less space in the output file. Write the SmallEvent to the * output file, and mark it as already-saved. */ void SaveEvent(theEvent) struct InputEvent *theEvent; { if (theEvent->my_Time > MILLION) theEvent->my_Time += MILLION; TinyEvent.se_XY = 0; TinyEvent.se_Type = theEvent->ie_Class; TinyEvent.se_Qualifier = theEvent->ie_Qualifier; TinyEvent.se_Long2 = (theEvent->my_Ticks << 20) | (theEvent->my_Time & 0xFFFFF); if (theEvent->ie_Class == IECLASS_RAWKEY) { TinyEvent.se_Code = theEvent->ie_Code; TinyEvent.se_Prev = theEvent->my_Prev; } else { TinyEvent.se_Type |= (theEvent->ie_Code & IECODE_UP_PREFIX) | ((theEvent->ie_Code & 0x03) << 5); TinyEvent.se_XY |= (theEvent->ie_X & 0xFFF) | ((theEvent->ie_Y & 0xFFF) << 12); } if (fwrite((char *)&TinyEvent,sizeof(TinyEvent),1,OutFile) != 1) DoExit("Error writing to output file: %ld",_OSERR); SETSAVED(theEvent); NotFirstEvent = TRUE; } /* * SaveTime() * * Save a fake mouse event that doesn't move anywhere but that includes a * tick count. That is, pause without moving the mouse. */ void SaveTime(theEvent) struct InputEvent *theEvent; { if (NotFirstEvent) TimeEvent.se_Ticks = (theEvent->my_Ticks << 20); if (fwrite((char *)&TimeEvent,sizeof(TimeEvent),1,OutFile) != 1) DoExit("Error writing to output file: %ld",_OSERR); theEvent->my_Ticks = 0; } /* * SaveEventList() * * Write the events in the event list (built by the input handler) out * to the output file, compressing multiple, small mouse moves into larger, * single mouse moves (to save space in the output file) in the following * way: * * if the current event is a mouse move (not a button press), then * set its event count to 1 (the number of events compressed into it), * if the user is requesting smooth movement, then use the smooth * movement variables, otherwise use the course (normal) values. * if the event's x or y movement is big enough, or if there was a long * pause before the movement occured, then * if the old event was not saved, save it. * if the pause was long enough, save a separate pause (so that the * smoothing algorithm in PLAYBACK does not spread the pause over * the entire mouse move). * save the current event. * otherwise, (we can compress the movement) * if there was an old mouse event that was not saved, * add it to the current event, * if the new x or y movement is big enough to record, do so. * otherwise, (this was not a mouse movement) * if there was a previous mouse movement that was not saved, save it. * finally, save the current event. * At this point the OldEvent is either posted, or has been combined with the * current event, so we can free the old event. The current event then * becomes the old event. */ void SaveEventList() { struct InputEvent *theEvent; while ((theEvent = OldEvent->ie_NextEvent) != NULL) { if (MOUSEMOVE(theEvent)) { theEvent->my_Count &= (~COUNTMASK); theEvent->my_Count++; if ((theEvent->ie_Qualifier & SmoothMask) == SmoothMask && (theEvent->ie_Qualifier & SmoothTrigger)) { xminmove = smoothxmove; yminmove = smoothymove; } else { xminmove = xmove; yminmove = ymove; } if (BIGX(theEvent) || BIGY(theEvent) || BIGTICKS(theEvent)) { if (NOTSAVED(OldEvent)) SaveEvent(OldEvent); if (BIGTICKS(theEvent)) SaveTime(theEvent); SaveEvent(theEvent); } else { if (NOTSAVED(OldEvent)) { theEvent->ie_X += OldEvent->ie_X; theEvent->ie_Y += OldEvent->ie_Y; theEvent->my_Ticks += OldEvent->my_Ticks; theEvent->my_Count += OldEvent->my_Count & COUNTMASK; if (BIGX(theEvent) || BIGY(theEvent)) SaveEvent(theEvent); } } } else { if (NOTSAVED(OldEvent)) SaveEvent(OldEvent); SaveEvent(theEvent); } FREEVENT(OldEvent); OldEvent = theEvent; } } /* * RecordJournal() * * Open the journal file, set up the task and signals, and set up the * initial pointers for the event list. Then add the input handler * into the Input.Device handler chain. * * Wait for the input handler to signal us that an event is ready (or that * an error occured), or that the user to press CTRL-AMIGA-E. If it's the * latter, cancel the Wait loop, otherwise save the events that are in the * list into the file. If the error signal was sent, inform the user that * some events were lost. * * Once we are signaled to end the journal, remove the handler, and * record any remaining, unsaved events to the file. */ void RecordJournal() { LONG signals; LONG SigMask; OpenJournal(); SetupTask(); SetupEvents(); SigMask = theMask | ErrMask | SIGBREAKF_CTRL_E; AddHandler(&myHandler); while (NotDone) { signals = Wait(SigMask); if (signals & SIGBREAKF_CTRL_E) NotDone = FALSE; else SaveEventList(); if (signals & ErrMask) printf("[ Out of memory - some events not recorded ]\n"); } RemoveHandler(&myHandler); SaveEventList(); } /* * main() * * Parse the command-line arguments and perform the proper function * (either show the usage, write a journal, or fall through and exit). */ void main(argc,argv) int argc; char **argv; { switch(ParseArguments(argc,argv)) { case SHOW_USAGE: printf("Usage: JOURNAL [TO] file [DX x] [DY y]\n"); printf(" [SMOOTHX x] [SMOOTHY y] [SMOOTH mask]"); printf( " [TRIGGER mask]\n"); break; case WRITE_JOURNAL: RecordJournal(); break; } DoExit(NULL); }