Adds a cooked mouse mode which gives click, double click and so on already processed. It is written as an interpreter for a minilanguage Notes: Sat Jan 15 16:01:50 EST 2005 rsc As rob and rog noted, setmousemode() is not the right way to express this. It should definitely be a filter that just gives you back a channel with slightly different mouse events. cook.c needs significant cleaning up. Also, I don't understand the language. Better documentation here would help. And what is a slide? Or a chord slide? Also an example of how this interacts with things like updating the screen to display the selection during a long click would help. Reference: /n/sources/patch/sorry/cooked-mouse Date: Sat Jan 15 22:03:54 CET 2005 Reviewed-by: rsc --- /sys/src/libdraw/mouse.c Sat Jan 15 22:03:54 2005 +++ /sys/src/libdraw/mouse.c Sat Jan 15 22:03:53 2005 @@ -5,6 +5,33 @@ #include #include +extern char state[1024]; +Channel *cookc; /* chan(Mouse)[0] */ +Channel *modec; /* chan(int)[0] */ +int mousemode; + +int +Mfmt(Fmt* f) +{ + int m; + char buf[8]; + + m = va_arg(f->args, int); + memset(buf, '-', sizeof(buf)); + seprint(buf+5, buf+sizeof(buf), "%1.1x", m&7); + if (m&MCLICK) + buf[4] = 'c'; + if (m&MDOUBLE) + buf[3] = 'd'; + if (m&MSELECT) + buf[2] = 's'; + if (m&MCHORD) + buf[1] = 'm'; + if (m&MEND) + buf[0] = 'e'; + return fmtprint(f, buf); +} + void moveto(Mousectl *m, Point pt) { @@ -12,6 +39,7 @@ m->xy = pt; } + void closemouse(Mousectl *mc) { @@ -30,6 +58,7 @@ free(mc); } + int readmouse(Mousectl *mc) { @@ -42,6 +71,7 @@ return 0; } + static void _ioproc(void *arg) @@ -76,24 +106,31 @@ m.xy.y = atoi(buf+1+1*12); m.buttons = atoi(buf+1+2*12); m.msec = atoi(buf+1+3*12); - send(mc->c, &m); + if (mousemode==MCOOKED) + send(cookc, &m); + else { + send(mc->c, &m); /* - * mc->Mouse is updated after send so it doesn't have wrong value if we block during send. - * This means that programs should receive into mc->Mouse (see readmouse() above) if - * they want full synchrony. + * mc->Mouse is updated after send so it doesn't have + * wrong value if we block during send. + * This means that programs should receive into mc->Mouse + * (see readmouse() above) if they want full synchrony. */ - mc->Mouse = m; + mc->Mouse = m; + } break; } } } + Mousectl* initmouse(char *file, Image *i) { Mousectl *mc; char *t, *sl; + state[0]='\0'; mc = mallocz(sizeof(Mousectl), 1); if(file == nil) file = "/dev/mouse"; @@ -119,10 +156,13 @@ mc->image = i; mc->c = chancreate(sizeof(Mouse), 0); mc->resizec = chancreate(sizeof(int), 2); + mousemode = MRAW ; + modec = chancreate(sizeof(int), 0); proccreate(_ioproc, mc, 4096); return mc; } + void setcursor(Mousectl *mc, Cursor *c) { @@ -137,3 +177,5 @@ write(mc->cfd, curs, sizeof curs); } } + + --- /sys/src/libdraw/cook.c Thu Jan 1 00:00:00 1970 +++ /sys/src/libdraw/cook.c Sat Jan 15 22:03:53 2005 @@ -0,0 +1,634 @@ +#include +#include +#include +#include +#include +#include +#include + +/* +The cooked mode is implemented as an interpreter. In /sys/src/libdraw/cook.c:85 +is the program which implements the interpretation of events in a minilanguage. +D and U mean up and down of a button and lower case +letters are variables representing buttons. For example DiDjUjUi means pressing +one button, pressing another, releasing the second one pressed and then releasing the +first. T means the time passed is less than the quantum of time. +M means the mouse is moved more than the quantum of movement. TTT> means +three quantum times or more. +Events generated can consume the state, or be preffixes (colouring one mouse struct or +all the generated in the event). + +*/ + +#define MCHORDBITS(b,nth) (((b)&0xf)<<(20 +(nth)*4)) + +enum { + TIMERSTEP = 100, /* in ms */ + SMALL = 1, /* in pixels */ + MAXTOK = 10, /* for translation of events flag->TOKEN*/ + + NOTMATCH=-1, + IISPREFP=-2, +/* + * as explained in mouse(2), take care with MOUSEFLAGS + */ + N = 0, + L = 1, + M = 2, + R = 4, + + T_CANCEL = 2000 +}; + +enum SMDATATYPE{ + TIMER, + MOUSE +}; + +typedef struct Smdata Smdata; +struct Smdata { + enum SMDATATYPE type; + uint now; /* miliseconds */ + Mouse; +}; + + +int verbstate; +char state[1024]; + +extern Channel *cookc; /* chan(Mouse)[0] */ +extern Channel *modec; /* chan(int)[0] */ +extern int mousemode; +static Smdata smdata; + +static Channel *cntdownc = nil; /* chan(int)[0], countdown start/end */ + +typedef struct Tatom Tatom; +struct Tatom { + int mskold; + int msknew; + int negold; + int negnew; + char *tok; +}; + +static Tatom ttable[]={ + {L,L,0,1,"U1"}, + {M,M,0,1,"U2"}, + {R,R,0,1,"U3"}, + {L,L,1,0,"D1"}, + {M,M,1,0,"D2"}, + {R,R,1,0,"D3"}, +}; + +typedef struct Pref Pref; +struct Pref { + char *preffix; + int isdone; //has been done + int ispref; //when !preffix intepret this match as nop + int isconsumer; //consume from the array + int flags; +}; + +/* + !ispreffix & !isconsumer == ispreffixonce == 00 + events,0,ispreffix,isconsumer,flags, longest first. + BUG: probably most are unnecessary. + Order is important in consuming events. +*/ + +static Pref preffixes[]={ + {"DiT",0,0,0,MCLICK}, //click preffixonce + {"DiUiTT>",0,0,1,MCLICK|MEND}, //click + {"DiUiM",0,0,1,MCLICK|MEND}, //click + {"DiUiTDiUi",0,0,1,MDOUBLE|MCLICK|MEND}, //double click + {"DiDjDk",0,0,1,MDOUBLE|MCLICK|MCHORD|MEND}, //triple click-> double click + {"DiM",0,1,0,MSELECT}, //slide preffix all + {"DiMUi",0,0,1,MSELECT|MEND}, //slide + {"DiDj",0,0,0,MCHORD}, //Chord preffix once + {"DiDjM",0,0,1,MCLICK|MCHORD|MEND}, //Chord + {"DiDjUxUy",0,0,1,MCLICK|MCHORD|MEND}, //Chord + {"DiDjUi",0,0,1,MCLICK|MCHORD|MEND}, //Chord + {"DiDjUxM",0,0,1,MCLICK|MCHORD|MEND}, //Chord + {"DiDjMUx",0,0,1,MCLICK|MCHORD|MEND}, //Chord + {"DiDjMDx",0,0,1,MCLICK|MCHORD|MEND}, //Chord + {"DiDjDxM",0,0,1,MCLICK|MCHORD|MEND}, //Chord + {"DiDjUjDk",0,0,1,MDOUBLE|MCLICK|MCHORD|MEND}, //Chord Chord + {"DiMDj",0,0,0,MSELECT|MCHORD}, //SLChord preffix once + {"DiMDjUxUy",0,0,1,MSELECT|MCHORD|MEND}, //SLChord + {"DiMDjM",0,0,1,MSELECT|MCHORD|MEND}, //SLChord + {"DiMDjUjDk",0,0,1,MSELECT|MDOUBLE|MCLICK|MCHORD|MEND}, //Slide Chord Chord +//{"DiMDjUjDkM",0,0,1,MSELECT|MDOUBLE|MCLICK|MCHORD|MEND}, //Slide Chord Chord +}; + + +static uint +msec(void) +{ + return nsec()/1000000; +} + + +static char * +transtable(Mouse mold, Mouse mnew) +{ + Tatom *t; + char *data; + int bold,bnew; + int cndold,cndnew; + int i; + + bold=mold.buttons; + bnew=mnew.buttons; + + data=malloc(MAXTOK*nelem(ttable)+1); + + if(!data) + sysfatal("transtable: Error mallocating data %r"); + else + data[0]='\0'; + + for(i=0;imskold; + cndnew=bnew & t->msknew; + if(t->negold) + cndold=!cndold; + if(t->negnew) + cndnew=!cndnew; + if(cndold && cndnew) + strcat(data,t->tok); + } + if(data[0]) + return data; + else + return nil; +} + + +static int +ismove(Mouse m1, Mouse m2) +{ + Rectangle near; + int x0, y0, x1, y1; + + /* build a rectangle centered in ma->xy with sides aprox 2*SMALL */ + x0 = (m1.xy.x > SMALL) ? m1.xy.x-SMALL : m1.xy.x ; + y0 = (m1.xy.y > SMALL) ? m1.xy.y-SMALL : m1.xy.y ; + x1 = m1.xy.x + SMALL ; + y1 = m1.xy.y + SMALL ; + near = Rect(x0, y0, x1, y1); + + return(!ptinrect(m2.xy, near)); +} + + +static int +commonpref(char *s1, char *s2) +{ + int len,i; + + len=strspn(s1,s2); + for(i=0;i=lin && justt && lpat>0){ + // print("!length lmatch=%d lin=%d\n",lmatch,lin); + return(NOTMATCH); + } + else if(lmatch>=lin && lpat>0 && (*pat!='>')){ + // print("!length lmatch=%d lpat=%d\n",lmatch,lpat); + return(IISPREFP); + } + else if(lmatch>=lin && !justt){ + // print("!length lmatch=%d lin=%d\n",lmatch,lin); + return(lmatch); + } + + } + else + res=0; + + if(!lmatch && (*in=='T')){ + justt=1; + } + + + if(res && (in[-1]=='T')){ + isteing=1; + } + + //print("*pat:%c, res: %d\n",*pat,res); + switch(*pat){ + case 'M': + + if((*in=='T')&& !isteing){ + in++; + lmatch++; + break; + }else if (*in=='M'){ + in++; + ninrow++; + lmatch++; + justt=0; + break; + } + else if((*in!='M') && (ninrow!=0)){ + pat++; + ninrow=0; + justt=0; + } + else if(*in=='\0'){ + return(IISPREFP); + } + else + return(NOTMATCH); + isteing=0; + break; + case 'T': + pat++; + justt=0; + break; + case '>': + if((lmatch>0) && (*in=='T') && (in[-1]=='T')&& (pat[-1]=='T')){ + in++; + lmatch++; + justt=0; + } + else if((lmatch>0) && (in[-1]=='T')&& (pat[-1]=='T') && !justt){ + pat++; + justt=0; + } + else if(justt) + return(IISPREFP); + else + return(NOTMATCH); + isteing=0; //not teing, matched an explicit rule + break; + default: //variables + if((*in=='T') && !isteing){ + in++; + lmatch++; + } + else if((lmatch>0)&&(*pat=='\0')){ + return(lmatch); + } + else if(fl=isvar(pat,in,vars)){ + *flags|=0x1<<(fl-1); + *flags |= chordb(*flags, 0x1<<(fl-1)); //XXX + in++; + pat++; + lmatch++; + justt=0; + } + else{ + return(NOTMATCH); + } + isteing=0; + break; + } + } + return(NOTMATCH); +} + + +static char * +translate(Smdata *i,Mouse mold,int old) +{ + char data[10],*res, *b; + Mouse mnew; + + + data[0]='\0'; + mnew = i->Mouse; + if(i->type==TIMER){ + if ((i->now-old) < T_CANCEL) { + strcat(data,"T"); + } + + } + else{ + //print("!timer\n"); + if(ismove(mold,mnew)){ + strcat(data,"M"); + } + if(b=transtable(mold,mnew)){ + strcat(data,b); + free(b); + } + } + +/* if(i->type==TIMER){ + if ((i->now-old) > T_CANCEL) + strcat(data,"T?"); + } +*/ + res=strdup(data); + + + return(res); +} + +static void +clean(void) +{ + int j; + for(j=0;jMouse; + + data=translate(i,mold,old); + if(i->type!=TIMER){ + mold=m; + } + + i->Mouse.buttons&=!MBUTTONS; + if((i->type==TIMER) && (data[0]!='?')){ + old=i->now; + } + if(!data && (data[0]=='\0')) + return(0); + if((i->type!=TIMER) || (data[0]!='?')) + nbsend(cntdownc, nil); //activate timer if we get something + + strcat(state,data); + free(data); + slen=strlen(state); + if(verbstate && (i->type!=TIMER)) + print("STATE: '%s'\n",state); + if(state[0]=='\0') + return(0); + for(j=0;jpreffix,state,&fl); + matchall=(lmatch==slen)&&(lmatch>0); + yetnotmatch=(lmatch==IISPREFP); + matchpref=(lmatch0); + i->Mouse.buttons|=fl; + fl=0; + + //preffix for all + if((matchall||matchpref) && pref->ispref ){ + i->Mouse.buttons|=pref->flags; + pref->isdone=1; + if(verbstate) + print("PREF %s %s\n",pref->preffix,state); + output=1; + } + if(matchpref && !pref->ispref && !pref->isconsumer && !pref->isdone) + { + if(verbstate) + print("ONCE PREF %s %s\n",pref->preffix,state); + pref->isdone=1; + i->Mouse.buttons|=pref->flags; + output=1; + } + if(matchall && pref->isconsumer){ + if(verbstate) + print("MATCHALL %s\n",pref->preffix); + i->Mouse.buttons|=pref->flags; + if(pref->isconsumer){ + if(verbstate) + print("CONSUME %s\n",state); + consume(lmatch); + } + if(!pref->ispref) + clean(); + + output=1; + } + + if(yetnotmatch){ //is on the way to matching + keepstate=1; + continue; + } + + } + if(!keepstate){ + if(verbstate && (strlen(state)>=2)) + print("NOMATCH: '%s'\n",state); + consume(strlen(state)); + clean(); + } + if(output){ + /* UGLY: clear MCHORD bits + * when event is not a chord. + */ + if (!(i->Mouse.buttons&MCHORD)) + i->Mouse.buttons &= ~MCHORDALL; + return(1); + } else + return(0); +} + + +/* + * not only cook but also implements mode changes + */ +static void +cookthread(void *arg) +{ + enum { + ACDOWN, + ACOOK, + AMODE, + NALT + }; + + Mousectl *mc; + Mouse m; + int mode; + static Alt alts[NALT+1]; + + mc = arg; + threadsetname("cookthread"); + + alts[ACDOWN].c = cntdownc; + alts[ACDOWN].v = nil; + alts[ACDOWN].op = CHANRCV; + alts[ACOOK].c = cookc; + alts[ACOOK].v = &m; + alts[ACOOK].op = CHANRCV; + alts[AMODE].c = modec; + alts[AMODE].v = &mode; + alts[AMODE].op = CHANRCV; + alts[NALT].op = CHANEND; + + for (;;) { + switch(alt(alts)) { + case ACDOWN: + smdata.type = TIMER; + smdata.now = msec(); + if (cooker(&smdata)) { + send(mc->c, &(smdata.Mouse)); + /* + * mc->Mouse is updated after send so it doesn't have + * wrong value if we block during send. + * This means that programs should receive from mc->Mouse + * (see readmouse() above) if they want full synchrony. + */ + mc->Mouse = smdata.Mouse; + } + break; + case ACOOK: + smdata.type = MOUSE; + smdata.Mouse = m; + if (cooker(&smdata)) { + send(mc->c, &(smdata.Mouse)); + mc->Mouse = smdata.Mouse; + } + break; + case AMODE: + mousemode = mode; + send(modec, &mousemode); + break; + } /* end of switch */ + } /* end of for */ +} + + +static void +cntdownproc(void*) +{ + rfork(RFFDG); + threadsetname("cntdownproc"); + recv(cntdownc, nil); + for(;;) { + sleep(TIMERSTEP); + send(cntdownc, nil); + recv(cntdownc, nil); + } +} + +/* + * mode change do not care if the apication is + * expecting for some MEND flag so take care when + * you call setmousemode(MRAW) . + */ +int +setmousemode(Mousectl *mc, int mode) +{ + switch(mode) { + case MCOOKED: + consume(strlen(state)); + clean(); + /* create thread & proc on first call to cooked mode */ + if (cntdownc==nil) { + cntdownc = chancreate(sizeof(int), 0); + cookc = chancreate(sizeof(Mouse), 0); + proccreate(cntdownproc, nil, 8192); + threadcreate(cookthread, mc, 8192); + } /* fall through */ + case MRAW: + send(modec, &mode); + recv(modec, &mode); + return(mode); + break; + default: + return(-1); + } +} --- /sys/src/libdraw/mkfile Sat Jan 15 22:03:53 2005 +++ /sys/src/libdraw/mkfile Sat Jan 15 22:03:53 2005 @@ -10,6 +10,7 @@ border.$O\ buildfont.$O\ bytesperline.$O\ + cook.$O\ chan.$O\ cloadimage.$O\ computil.$O\ --- /sys/include/mouse.h Sat Jan 15 22:03:54 2005 +++ /sys/include/mouse.h Sat Jan 15 22:03:54 2005 @@ -5,9 +5,34 @@ typedef struct Menu Menu; typedef struct Mousectl Mousectl; +#pragma varargck type "M" int +extern int Mfmt(Fmt*); + +enum { + MBUTTONS = 7, /* ones on buttons bits */ + + // cooked event flags + MCLICK = 0x00000100, + MDOUBLE = 0x00000200, + MSELECT = 0x00000400, + MCHORD = 0x00000800, + MEND = 0x00001000, + MFLAGS = 0x00001f00, + MCHORD0 = 0x00700000, // 1st chord button + MCHORD1 = 0x07000000, // 2nd chord button + MCHORD2 = 0x70000000, // 3rd chord button + MCHORDALL = 0xfff00000, // chord button order + + // setmousemode args + MRAW = 0, + MCOOKED = 1 +}; + +#define MCHORDB(b,nth) (((b)>>(20 +(nth)*4))&0xf) + struct Mouse { - int buttons; /* bit array: LMR=124 */ + int buttons; /* bit array: LMR=124 and flags */ Point xy; ulong msec; }; @@ -19,28 +44,31 @@ Channel *resizec; /* chan(int)[2] */ /* buffered in case client is waiting for a mouse action before handling resize */ - char *file; + char *file; int mfd; /* to mouse file */ int cfd; /* to cursor file */ int pid; /* of slave proc */ - Image* image; /* of associated window/display */ + Image* image; /* of associated window/display */ }; struct Menu { char **item; char *(*gen)(int); - int lasthit; + int lasthit; }; + /* * Mouse */ extern Mousectl* initmouse(char*, Image*); -extern void moveto(Mousectl*, Point); +extern int setmousemode(Mousectl*, int); +extern void moveto(Mousectl*, Point); extern int readmouse(Mousectl*); -extern void closemouse(Mousectl*); -extern void setcursor(Mousectl*, Cursor*); -extern void drawgetrect(Rectangle, int); +extern void closemouse(Mousectl*); +extern void setcursor(Mousectl*, Cursor*); +extern void drawgetrect(Rectangle, int); extern Rectangle getrect(int, Mousectl*); extern int menuhit(int, Mousectl*, Menu*, Screen*); +extern int verbstate; --- /sys/man/2/mouse Sat Jan 15 22:03:54 2005 +++ /sys/man/2/mouse Sat Jan 15 22:03:53 2005 @@ -1,6 +1,6 @@ .TH MOUSE 2 .SH NAME -initmouse, readmouse, closemouse, moveto, cursorswitch, getrect, drawgetrect, menuhit, setcursor \- mouse control +initmouse, setmousemode, readmouse, closemouse, moveto, cursorswitch, getrect, drawgetrect, menuhit, setcursor, Mfmt \- mouse control .SH SYNOPSIS .nf .B @@ -20,6 +20,9 @@ Mousectl *initmouse(char *file, Image *i) .PP .B +int setmousemode(Mousectl* mc, int mode) +.PP +.B int readmouse(Mousectl *mc) .PP .B @@ -42,6 +45,12 @@ .PP .B int menuhit(int but, Mousectl *mc, Menu *menu, Screen *scr) +.PP +.B +int MCHORDB(int buttons, int idx) +.PP +.B +int Mfmt(Fmt*) .fi .SH DESCRIPTION These functions access and control a mouse in a multi-threaded environment. @@ -63,7 +72,7 @@ typedef struct Mouse Mouse; struct Mouse { - int buttons; /* bit array: LMR=124 */ + int buttons; /* bit array: LMR=124 and flags */ Point xy; ulong msec; }; @@ -74,8 +83,9 @@ .B xy records the position of the cursor, .B buttons -the state of the buttons (three bits representing, from bit 0 up, the buttons from left to right, -0 if the button is released, 1 if it is pressed), +the state of the buttons (in raw mode, three bits representing, from bit 0 up, the buttons from left to right, +0 if the button is released, 1 if it is pressed; in cooked mode, it also informs about mouse events, see bellow +for more information on mouse modes), and .BR msec , a millisecond time stamp. @@ -118,17 +128,86 @@ after a call to .IR initdraw . .PP -Once the -.B Mousectl -is set up, -mouse motion will be reported by messages of type -.B Mouse -sent on the -.B Channel -.BR Mousectl.c . -Typically, a message will be sent every time a read of +There are two ways of handling the mouse, in raw mode, messages are +sent to +.B Channel Mousectl.c +every time the state of the mouse changes. In cooked mode, digested +mouse events are transmited by +.B Channel Mousectl.c. +The mouse is initialized by default on raw mode. Once +.I initmouse +has been successfully called, +.I Setmousemode +can be used to switch between modes. +.I Mode +is one of MRAW or MCOOKED. +.I Setmousemode +returns the new mode, -1 on error. +.PP +On raw mode, a message will be sent every time a read of .B /dev/mouse -succeeds, which is every time the state of the mouse changes. +succeeds. +.PP +On cooked mode, +.B Channel Mousectl.c +will only report complete or partial mouse events. +.PP +While on cooked mode, +.B Mouse.buttons +is colored with some additional flags, MCLICK, MCHORD, MDOUBLE, MEND and other info. +.PP +Mouse events are clicks (MCLICK), +selections (MSELECT) and chords (MCHORD). Any event can be double (MDOUBLE). +A single click is done by pressing and releasing in quick sucession a mouse button +without moving the mouse. +A double click is two sucessive clicks of the same button in a short time without +moving the mouse. A +chord is achieved by pressing two mouse buttons succesively and then +releasing them in any order. Pressing a mouse button and moving the +mouse gives you a selection. The selection can be terminated by releasing +the button or by performing a chord. +.PP +When an event is completely recognized, it is sent with the MEND flag set. +For example, an application interested in double click for the first button +would check MDOUBLE|MCLICK|MEND|1. +.PP +While the event is still in the oven, some other half cooked or +partial events may be sent so that the +application can, for example draw a selection in the screen. +Examples of this are click, chord and slide. +Click happens whenever a button starts being pressed and none where pressed before. +It marks the start of any event. Slide happens when +any button is pressed and the mouse is moved. It sets the rest +of the +.B Mouse.buttons +of the complete event. Chord happens when a second button +starts being pressed. +Note that the starting click of a slide may not have the MSELECT flag set, as +partial events are in a sense independant. +.PP +Partial and complete chords set additionally the three upper nibbles of +.B Mouse.buttons +with the order of the first three buttons +pressed. The macro MCHORDB returns which button was pressed in idx, for example +the first button pressed would be +.B MCHORDB(m->buttons,0). +.PP +The cooked mode is backwards compatible except for chord events where all the +buttons which have been pressed until that moment appear as pressed on the +.B Mouse.buttons. Another issue is that some libraries wait for events with +no buttons pressed. This has to be changed to work with cooked mode. +For example pressing button 1 then pressing and releasing button 2 +and then (still not releasing button 1), pressing button 3 would give a hard double +chord event with the three buttons pressed. In raw mode it would have given +just two buttons pressed. +.PP +The variable verbstate can be set to one to print the state of the interpreter +of events for debbuging purposes. The function +.B Mfmt +can be used to format mouse events to aid debugging. +.PP +If you are waiting for a MEND flag, changing to raw mode can have +unpleasant consecuences. .PP When the window is resized, a message is sent on .BR Mousectl.resizec .