diff options
Diffstat (limited to 'appl/collab/clients/whiteboard.b')
| -rw-r--r-- | appl/collab/clients/whiteboard.b | 586 |
1 files changed, 586 insertions, 0 deletions
diff --git a/appl/collab/clients/whiteboard.b b/appl/collab/clients/whiteboard.b new file mode 100644 index 00000000..e05e1b45 --- /dev/null +++ b/appl/collab/clients/whiteboard.b @@ -0,0 +1,586 @@ +implement Whiteboard; + +include "sys.m"; + sys: Sys; + +include "draw.m"; + draw: Draw; + Screen, Display, Image, Rect, Point, Font: import draw; + +include "tk.m"; + tk: Tk; + +include "tkclient.m"; + tkclient: Tkclient; + +Whiteboard: module { + init: fn(ctxt: ref Draw->Context, args: list of string); +}; + +ERASEWIDTH: con 6; + + +stderr: ref Sys->FD; +srvfd: ref Sys->FD; +disp: ref Display; +font: ref Draw->Font; +drawctxt: ref Draw->Context; + +tksetup := array[] of { + "frame .f -bd 2", + "frame .c -bg white -width 234 -height 279", + "menu .penmenu", + ".penmenu add command -command {send cmd pen 0} -bitmap @/icons/whiteboard/0.bit", + ".penmenu add command -command {send cmd pen 1} -bitmap @/icons/whiteboard/1.bit", + ".penmenu add command -command {send cmd pen 2} -bitmap @/icons/whiteboard/2.bit", + ".penmenu add command -command {send cmd pen erase} -bitmap @/icons/whiteboard/erase.bit", + "menubutton .pen -menu .penmenu -bitmap @/icons/whiteboard/1.bit", + "button .colour -bg black -activebackground black -command {send cmd getcolour}", + "pack .c -in .f", + "pack .f -side top -anchor center", + "pack .pen -side left", + "pack .colour -side left -fill both -expand 1", + "update", +}; + +tkconnected := array[] of { + "bind .c <Button-1> {send cmd down %x %y}", + "bind .c <ButtonRelease-1> {send cmd up %x %y}", + "update", +}; + +init(ctxt: ref Draw->Context, args: list of string) +{ + sys = load Sys Sys->PATH; + sys->pctl(Sys->NEWPGRP, nil); + stderr = sys->fildes(2); + + draw = load Draw Draw->PATH; + tk = load Tk Tk->PATH; + + tkclient = load Tkclient Tkclient->PATH; + if (tkclient == nil) + badmod(Tkclient->PATH); + + if (len args < 2) { + sys->fprint(stderr, "Usage: whiteboard [servicedir] id\n"); + raise "fail:init"; + } + + args = tl args; + servicedir := "/n/remote/services"; + if(len args == 2) + (servicedir, args) = (hd args, tl args); + wbid := hd args; + + disp = ctxt.display; + if (disp == nil) { + sys->fprint(stderr, "bad Draw->Context\n"); + raise "fail:init"; + } + drawctxt = ctxt; + + tkclient->init(); + (win, winctl) := tkclient->toplevel(ctxt, nil, "Whiteboard", 0); + font = Font.open(disp, tkcmd(win, ". cget -font")); + if(font == nil) + font = Font.open(disp, "*default*"); + cmd := chan of string; + tk->namechan(win, cmd, "cmd"); + tkcmds(win, tksetup); + tkclient->onscreen(win, nil); + tkclient->startinput(win, "kbd" :: "ptr" :: nil); + cimage := makeimage(win); + + sc := chan of array of (Point, Point); + cc := chan of (string, ref Image, ref Sys->FD, ref Sys->FD); + connected := 0; + sfd: ref Sys->FD; + ctlfd: ref Sys->FD; # must keep this open to keep service active + + showtext(cimage, "connecting..."); + spawn connect(servicedir, wbid, cc); + + err: string; + strokeimg: ref Image; +Connect: + for (;;) alt { + (err, strokeimg, sfd, ctlfd) = <-cc => + if (err == nil) + break Connect; + else + showtext(cimage, "Error: " + err); + + s := <-winctl or + s = <-win.wreq or + s = <-win.ctxt.ctl => + oldimg := win.image; + err = tkclient->wmctl(win, s); + if(s[0] == '!' && err == nil && win.image != oldimg){ + cimage = makeimage(win); + showtext(cimage, "connecting..."); + } + p := <-win.ctxt.ptr => + tk->pointer(win, *p); + c := <-win.ctxt.kbd => + tk->keyboard(win, c); + } + + tkcmd(win, ".c configure -width " + string strokeimg.r.dx()); + tkcmd(win, ".c configure -height " + string strokeimg.r.dy()); + tkcmds(win, tkconnected); + tkcmd(win, "update"); + cimage.draw(cimage.r, strokeimg, nil, strokeimg.r.min); + + strokesin := chan of (int, int, array of Point); + strokesout := chan of (int, int, Point, Point); + spawn reader(sfd, strokesin); + spawn writer(sfd, strokesout); + + pendown := 0; + p0, p1: Point; + + getcolour := 0; + white := disp.white; + whitepen := disp.newimage(Rect(Point(0,0), Point(1,1)), Draw->CMAP8, 1, Draw->White); + pencolour := Draw->Black; + penwidth := 1; + erase := 0; + drawpen := disp.newimage(Rect(Point(0,0), Point(1,1)), Draw->CMAP8, 1, pencolour); + + for (;;) alt { + s := <-winctl or + s = <-win.ctxt.ctl or + s = <-win.wreq => + oldimg := win.image; + err = tkclient->wmctl(win, s); + if(s[0] == '!' && err == nil && win.image != oldimg){ + cimage = makeimage(win); + cimage.draw(cimage.r, strokeimg, nil, strokeimg.r.min); + } + p := <-win.ctxt.ptr => + tk->pointer(win, *p); + c := <-win.ctxt.kbd => + tk->keyboard(win, c); + (colour, width, strokes) := <-strokesin => + if (strokes == nil) + tkclient->settitle(win, "Whiteboard (Disconnected)"); + else { + pen := disp.newimage(Rect(Point(0,0), Point(1,1)), Draw->CMAP8, 1, colour); + drawstrokes(cimage, cimage.r.min, pen, width, strokes); + drawstrokes(strokeimg, strokeimg.r.min, pen, width, strokes); + } + + c := <-cmd => + (nil, toks) := sys->tokenize(c, " "); + case hd toks { + "down" => + toks = tl toks; + x := int hd toks; + y := int hd tl toks; + if (!pendown) { + pendown = 1; + p0 = Point(x, y); + continue; + } + p1 = Point(x, y); + if (p1.x == p0.x && p1.y == p0.y) + continue; + pen := drawpen; + colour := pencolour; + width := penwidth; + if (erase) { + pen = whitepen; + colour = Draw->White; + width = ERASEWIDTH; + } + drawstroke(cimage, cimage.r.min, p0, p1, pen, width); + drawstroke(strokeimg, strokeimg.r.min, p0, p1, pen, width); + strokesout <-= (colour, width, p0, p1); + p0 = p1; + "up" => + pendown = 0; + + "getcolour" => + pendown = 0; + if (!getcolour) + spawn colourmenu(cmd); + "colour" => + pendown = 0; + getcolour = 0; + toks = tl toks; + if (toks == nil) + # colourmenu was dismissed + continue; + erase = 0; + tkcmd(win, ".pen configure -bitmap @/icons/whiteboard/" + string penwidth + ".bit"); + tkcmd(win, "update"); + pencolour = int hd toks; + toks = tl toks; + tkcolour := hd toks; + drawpen = disp.newimage(Rect(Point(0,0), Point(1,1)), Draw->CMAP8, 1, pencolour); + tkcmd(win, ".colour configure -bg " + tkcolour + " -activebackground " + tkcolour); + tkcmd(win, "update"); + + "pen" => + pendown = 0; + p := hd tl toks; + i := ""; + if (p == "erase") { + erase = 1; + i = "erase.bit"; + } else { + erase = 0; + penwidth = int p; + i = p + ".bit"; + } + tkcmd(win, ".pen configure -bitmap @/icons/whiteboard/" + i); + tkcmd(win, "update"); + } + + } +} + +makeimage(win: ref Tk->Toplevel): ref Draw->Image +{ + if(win.image == nil) + return nil; + scr := Screen.allocate(win.image, win.image.display.white, 0); + w := scr.newwindow(tk->rect(win, ".c", Tk->Local), Draw->Refnone, Draw->Nofill); + return w; +} + +showtext(img: ref Image, s: string) +{ + r := img.r; + r.max.y = img.r.min.y + font.height; + img.draw(r, disp.white, nil, (0, 0)); + img.text(r.min, disp.black, (0, 0), font, s); +} + +penmenu(t: ref Tk->Toplevel, p: Point) +{ + topy := int tkcmd(t, ".penmenu yposition 0"); + boty := int tkcmd(t, ".penmenu yposition end"); + dy := boty - topy; + p.y -= dy; + tkcmd(t, ".penmenu post " + string p.x + " " + string p.y); +} + +colourcmds := array[] of { + "label .l -height 10", + "frame .c -height 224 -width 224", + "pack .l -fill x -expand 1", + "pack .c -side bottom -fill both -expand 1", + "pack propagate . 0", + "bind .c <Button-1> {send cmd push %x %y}", + "bind .c <ButtonRelease-1> {send cmd release}", +}; + +lastcolour := "255"; +lasttkcolour := "#000000"; + +colourmenu(c: chan of string) +{ + (t, winctl) := tkclient->toplevel(drawctxt, nil, "Whiteboard", Tkclient->OK); + cmd := chan of string; + tk->namechan(t, cmd, "cmd"); + tkcmds(t, colourcmds); + tkcmd(t, ".l configure -bg " + lasttkcolour); + tkcmd(t, "update"); + tkclient->onscreen(t, "onscreen"); + tkclient->startinput(t, "kbd" :: "ptr" :: nil); + + drawcolours(t.image, tk->rect(t, ".c", Tk->Local)); + + for(;;) alt { + p := <-t.ctxt.ptr => + tk->pointer(t, *p); + s := <-t.ctxt.kbd => + tk->keyboard(t, s); + s := <-winctl or + s = <-t.ctxt.ctl or + s = <-t.wreq => + case s{ + "ok" => + c <-= "colour " + lastcolour + " " + lasttkcolour; + return; + "exit" => + c <-= "colour"; + return; + * => + oldimage := t.image; + e := tkclient->wmctl(t, s); + if(s[0] == '!' && e == nil && oldimage != t.image) + drawcolours(t.image, tk->rect(t, ".c", Tk->Local)); + } + + press := <-cmd => + (n, word) := sys->tokenize(press, " "); + case hd word { + "push" => + (lastcolour, lasttkcolour) = color(int hd tl word, int hd tl tl word, tk->rect(t, ".c", 0).size()); + tkcmd(t, ".l configure -bg " + lasttkcolour); + } + } +} + +drawcolours(img: ref Image, cr: Rect) +{ + # use writepixels because it's much faster than allocating all those colors. + tmp := disp.newimage(((0,0),(cr.dx(),cr.dy()/16+1)), Draw->CMAP8, 0, 0); + if(tmp == nil) + return; + buf := array[tmp.r.dx()*tmp.r.dy()] of byte; + dx := cr.dx(); + dy := cr.dy(); + for(y:=0; y<16; y++){ + for(i:=tmp.r.dx()-1; i>=0; --i) + buf[i] = byte (16*y+(16*i)/dx); + for(k:=tmp.r.dy()-1; k>=1; --k) + buf[dx*k:] = buf[0:dx]; + tmp.writepixels(tmp.r, buf); + r: Rect; + r.min.x = cr.min.x; + r.max.x = cr.max.x; + r.min.y = cr.min.y+(dy*y)/16; + r.max.y = cr.min.y+(dy*(y+1))/16; + img.draw(r, tmp, nil, tmp.r.min); + } +} + +color(x, y: int, size: Point): (string, string) +{ + x = (16*x)/size.x; + y = (16*y)/size.y; + col := 16*y+x; + (r, g, b) := disp.cmap2rgb(col); + tks := sys->sprint("#%.2x%.2x%.2x", r, g, b); + return (string disp.cmap2rgba(col), tks); +} + +opensvc(dir: string, svc: string, name: string): (ref Sys->FD, string, string) +{ + ctlfd := sys->open(dir+"/ctl", Sys->ORDWR); + if(ctlfd == nil) + return (nil, nil, sys->sprint("can't open %s/ctl: %r", dir)); + if(sys->fprint(ctlfd, "%s %s", svc, name) <= 0) + return (nil, nil, sys->sprint("can't access %s service %s: %r", svc, name)); + buf := array [32] of byte; + sys->seek(ctlfd, big 0, Sys->SEEKSTART); + n := sys->read(ctlfd, buf, len buf); + if (n <= 0) + return (nil, nil, sys->sprint("%s/ctl: protocol error: %r", dir)); + return (ctlfd, dir+"/"+string buf[0:n], nil); +} + +connect(dir, name: string, res: chan of (string, ref Image, ref Sys->FD, ref Sys->FD)) +{ + (ctlfd, srvdir, emsg) := opensvc(dir, "whiteboard", name); + if(ctlfd == nil) { + res <-= (emsg, nil, nil, nil); + return; + } + + bitpath := srvdir + "/wb.bit"; + strokepath := srvdir + "/strokes"; + + sfd := sys->open(strokepath, Sys->ORDWR); + if (sfd == nil) { + err := sys->sprint("cannot open whiteboard data: %r"); + res <-= (err, nil, nil, nil); + srvfd = nil; + return; + } + + bfd := sys->open(bitpath, Sys->OREAD); + if (bfd == nil) { + err := sys->sprint("cannot open whiteboard image: %r"); + res <-= (err, nil, nil, nil); + srvfd = nil; + return; + } + + img := disp.readimage(bfd); + if (img == nil) { + err := sys->sprint("cannot read whiteboard image: %r"); + res <-= (err, nil, nil, nil); + srvfd = nil; + return; + } +sys->print("read image ok\n"); + + # make sure image is depth 8 (because of image.line() bug) + if (img.depth != 8) { +sys->print("depth is %d, not 8\n", img.depth); + nimg := disp.newimage(img.r, Draw->CMAP8, 0, 0); + if (nimg == nil) { + res <-= ("cannot allocate local image", nil, nil, nil); + srvfd = nil; + return; + } + nimg.draw(nimg.r, img, nil, img.r.min); + img = nimg; + } + + res <-= (nil, img, sfd, ctlfd); +} + +reader(fd: ref Sys->FD, sc: chan of (int, int, array of Point)) +{ + buf := array [Sys->ATOMICIO] of byte; + + for (;;) { + n := sys->read(fd, buf, len buf); + if (n <= 0) { + sc <-= (0, 0, nil); + return; + } + s := string buf[0:n]; + (npts, toks) := sys->tokenize(s, " "); + if (npts & 1) + # something wrong + npts--; + if (npts < 6) + # ignore + continue; + + colour, width: int; + (colour, toks) = (int hd toks, tl toks); + (width, toks) = (int hd toks, tl toks); + pts := array [(npts - 2)/ 2] of Point; + for (i := 0; toks != nil; i++) { + x, y: int; + (x, toks) = (int hd toks, tl toks); + (y, toks) = (int hd toks, tl toks); + pts[i] = Point(x, y); + } + sc <-= (colour, width, pts); + pts = nil; + } +} + +Wmsg: adt { + data: array of byte; + datalen: int; + next: cyclic ref Wmsg; +}; + +writer(fd: ref Sys->FD, sc: chan of (int, int, Point, Point)) +{ + lastcol := -1; + lastw := -1; + lastpt := Point(-1, -1); + curmsg: ref Wmsg; + nextmsg: ref Wmsg; + + eofc := chan of int; + wc := chan of ref Wmsg; + wseof := 0; + spawn wslave(fd, wc, eofc); + + for (;;) { + colour := -1; + width := 0; + p0, p1: Point; + + if (curmsg == nil || wseof) + (colour, width, p0, p1) = <-sc; + else alt { + wseof = <-eofc => + ; + + (colour, width, p0, p1) = <-sc => + ; + + wc <-= curmsg => + curmsg = curmsg.next; + continue; + } + + newseq := 0; + if (curmsg == nil) { + curmsg = ref Wmsg(array [Sys->ATOMICIO] of byte, 0, nil); + nextmsg = curmsg; + newseq = 1; + } + + if (colour != lastcol || width != lastw || p0.x != lastpt.x || p0.y != lastpt.y) + newseq = 1; + + d: array of byte = nil; + if (!newseq) { + d = sys->aprint(" %d %d", p1.x, p1.y); + if (nextmsg.datalen + len d >= Sys->ATOMICIO) { + nextmsg.next = ref Wmsg(array [Sys->ATOMICIO] of byte, 0, nil); + nextmsg = nextmsg.next; + newseq = 1; + } + } + if (newseq) { + d = sys->aprint(" %d %d %d %d %d %d", colour, width, p0.x, p0.y, p1.x, p1.y); + if (nextmsg.datalen != 0) { + nextmsg.next = ref Wmsg(array [Sys->ATOMICIO] of byte, 0, nil); + nextmsg = nextmsg.next; + } + } + nextmsg.data[nextmsg.datalen:] = d; + nextmsg.datalen += len d; + lastcol = colour; + lastw = width; + lastpt = p1; + } +} + +wslave(fd: ref Sys->FD, wc: chan of ref Wmsg, eof: chan of int) +{ + for (;;) { + wm := <-wc; + n := sys->write(fd, wm.data, wm.datalen); + if (n != wm.datalen) + break; + } + eof <-= 1; +} + +drawstroke(img: ref Image, offset, p0, p1: Point, pen: ref Image, width: int) +{ + p0 = p0.add(offset); + p1 = p1.add(offset); + img.line(p0, p1, Draw->Endsquare, Draw->Endsquare, width, pen, p0); +} + +drawstrokes(img: ref Image, offset: Point, pen: ref Image, width: int, pts: array of Point) +{ + if (len pts < 2) + return; + p0, p1: Point; + p0 = pts[0].add(offset); + for (i := 1; i < len pts; i++) { + p1 = pts[i].add(offset); + img.line(p0, p1, Draw->Endsquare, Draw->Endsquare, width, pen, p0); + p0 = p1; + } +} + +badmod(mod: string) +{ + sys->fprint(stderr, "cannot load %s: %r\n", mod); + raise "fail:bad module"; +} + +tkcmd(t: ref Tk->Toplevel, cmd: string): string +{ + s := tk->cmd(t, cmd); + if (s != nil && s[0] == '!') { + sys->fprint(stderr, "%s\n", cmd); + sys->fprint(stderr, "tk error: %s\n", s); + } + return s; +} + +tkcmds(t: ref Tk->Toplevel, cmds: array of string) +{ + for (i := 0; i < len cmds; i++) + tkcmd(t, cmds[i]); +} |
