summaryrefslogtreecommitdiff
path: root/appl/lib/selectfile.b
diff options
context:
space:
mode:
authorCharles.Forsyth <devnull@localhost>2006-12-22 17:07:39 +0000
committerCharles.Forsyth <devnull@localhost>2006-12-22 17:07:39 +0000
commit37da2899f40661e3e9631e497da8dc59b971cbd0 (patch)
treecbc6d4680e347d906f5fa7fca73214418741df72 /appl/lib/selectfile.b
parent54bc8ff236ac10b3eaa928fd6bcfc0cdb2ba46ae (diff)
20060303a
Diffstat (limited to 'appl/lib/selectfile.b')
-rw-r--r--appl/lib/selectfile.b624
1 files changed, 624 insertions, 0 deletions
diff --git a/appl/lib/selectfile.b b/appl/lib/selectfile.b
new file mode 100644
index 00000000..15c55766
--- /dev/null
+++ b/appl/lib/selectfile.b
@@ -0,0 +1,624 @@
+implement Selectfile;
+
+include "sys.m";
+ sys: Sys;
+ Dir: import sys;
+
+include "draw.m";
+ draw: Draw;
+ Screen, Rect, Point: import draw;
+
+include "tk.m";
+ tk: Tk;
+
+include "string.m";
+ str: String;
+
+include "tkclient.m";
+ tkclient: Tkclient;
+
+include "workdir.m";
+
+include "readdir.m";
+ readdir: Readdir;
+
+include "filepat.m";
+ filepat: Filepat;
+
+include "selectfile.m";
+
+Browser: adt {
+ top: ref Tk->Toplevel;
+ ncols: int;
+ colwidth: int;
+ w: string;
+ init: fn(top: ref Tk->Toplevel, w: string, colwidth: string): (ref Browser, chan of string);
+
+ addcol: fn(c: self ref Browser, t: string, d: array of string);
+ delete: fn(c: self ref Browser, colno: int);
+ selection: fn(c: self ref Browser, cno: int): string;
+ select: fn(b: self ref Browser, cno: int, e: string);
+ entries: fn(b: self ref Browser, cno: int): array of string;
+ resize: fn(c: self ref Browser);
+};
+
+BState: adt {
+ b: ref Browser;
+ bpath: string; # path currently displayed in browser
+ epath: string; # path entered by user
+ dirfetchpid: int;
+ dirfetchpath: string;
+};
+
+filename_config := array[] of {
+ "entry .e -bg white",
+ "frame .pf",
+ "entry .pf.e",
+ "label .pf.t -text {Filter:}",
+ "entry .pats",
+ "bind .e <Key> +{send ech key}",
+ "bind .e <Key-\n> {send ech enter}",
+ "bind .e {<Key-\t>} {send ech expand}",
+ "bind .pf.e <Key-\n> {send ech setpat}",
+ "bind . <Configure> {send ech config}",
+ "pack .b -side top -fill both -expand 1",
+ "pack .pf.t -side left",
+ "pack .pf.e -side top -fill x",
+ "pack .pf -side top -fill x",
+ "pack .e -side top -fill x",
+ "pack propagate . 0",
+};
+
+debugging := 0;
+STEP: con 20;
+
+init(): string
+{
+ sys = load Sys Sys->PATH;
+ draw = load Draw Draw->PATH;
+ tk = load Tk Tk->PATH;
+ tkclient = load Tkclient Tkclient->PATH;
+ tkclient->init();
+ str = load String String->PATH;
+ readdir = load Readdir Readdir->PATH;
+ filepat = load Filepat Filepat->PATH;
+ return nil;
+}
+
+filename(ctxt: ref Draw->Context, parent: ref Draw->Image,
+ title: string,
+ pats: list of string,
+ dir: string): string
+{
+ patstr: string;
+
+ if (dir == nil || dir == ".") {
+ wd := load Workdir Workdir->PATH;
+ if ((dir = wd->init()) != nil) {
+ (ok, nil) := sys->stat(dir);
+ if (ok == -1)
+ dir = nil;
+ }
+ wd = nil;
+ }
+ if (dir == nil)
+ dir = "/";
+ (pats, patstr) = makepats(pats);
+ where := localgeom(parent);
+ if (title == nil)
+ title = "Open";
+ (top, wch) := tkclient->toplevel(ctxt, where+" -bd 1", # -font /fonts/misc/latin1.6x13.font",
+ title, Tkclient->Popup|Tkclient->Resize|Tkclient->OK);
+ (b, colch) := Browser.init(top, ".b", "16w");
+ entrych := chan of string;
+ tk->namechan(top, entrych, "ech");
+ tkcmds(top, filename_config);
+ cmd(top, ". configure -width " + string (b.colwidth * 3) + " -height 20h");
+ cmd(top, ".e insert 0 '" + dir);
+ cmd(top, ".pf.e insert 0 '" + patstr);
+ s := ref BState(b, nil, dir, -1, nil);
+ s.b.resize();
+ dfch := chan of (string, array of ref Sys->Dir);
+ if (parent == nil)
+ centre(top);
+ tkclient->onscreen(top, nil);
+ tkclient->startinput(top, "kbd" :: "ptr" :: nil);
+loop: for (;;) {
+ if (debugging) {
+ sys->print("filename: before sync, bpath: '%s'; epath: '%s'\n",
+ s.bpath, s.epath);
+ }
+ bsync(s, dfch, pats);
+ if (debugging) {
+ sys->print("filename: after sync, bpath: '%s'; epath: '%s'", s.bpath, s.epath);
+ if (s.dirfetchpid == -1)
+ sys->print("\n");
+ else
+ sys->print("; fetching '%s' (pid %d)\n", s.dirfetchpath, s.dirfetchpid);
+ }
+ cmd(top, "focus .e");
+ cmd(top, "update");
+ alt {
+ c := <-top.ctxt.kbd =>
+ tk->keyboard(top, c);
+ p := <-top.ctxt.ptr =>
+ tk->pointer(top, *p);
+ c := <-top.ctxt.ctl or
+ c = <-top.wreq =>
+ tkclient->wmctl(top, c);
+ c := <-colch =>
+ double := c[0] == 'd';
+ c = c[1:];
+ (bpath, nbpath, elem) := (s.bpath, "", "");
+ for (cno := 0; cno <= int c; cno++) {
+ (elem, bpath) = nextelem(bpath);
+ nbpath = pathcat(nbpath, elem);
+ }
+ nsel := s.b.selection(int c);
+ if (nsel != nil)
+ nbpath = pathcat(nbpath, nsel);
+ s.epath = nbpath;
+ cmd(top, ".e delete 0 end");
+ cmd(top, ".e insert 0 '" + s.epath);
+ if (double)
+ break loop;
+ c := <-entrych =>
+ case c {
+ "enter" =>
+ break loop;
+ "config" =>
+ s.b.resize();
+ "key" =>
+ s.epath = cmdget(top, ".e get");
+ "expand" =>
+ cmd(top, ".e delete 0 end");
+ cmd(top, ".e insert 0 '" + s.bpath);
+ s.epath = s.bpath;
+ "setpat" =>
+ patstr = cmdget(top, ".pf.e get");
+ if (patstr == " debug ")
+ debugging = !debugging;
+ else {
+ (nil, pats) = sys->tokenize(patstr, " ");
+ s.b.delete(0);
+ s.bpath = nil;
+ }
+ }
+ c := <-wch =>
+ if (c == "ok")
+ break loop;
+ if (c == "exit") {
+ s.epath = nil;
+ break loop;
+ }
+ tkclient->wmctl(top, c);
+ (t, d) := <-dfch =>
+ ds := array[len d] of string;
+ for (i := 0; i < len d; i++) {
+ n := d[i].name;
+ if ((d[i].mode & Sys->DMDIR) != 0)
+ n[len n] = '/';
+ ds[i] = n;
+ }
+ s.b.addcol(t, ds);
+ ds = nil;
+ d = nil;
+ s.bpath = s.dirfetchpath;
+ s.dirfetchpid = -1;
+ }
+ }
+ if (s.dirfetchpid != -1)
+ kill(s.dirfetchpid);
+ return s.epath;
+}
+
+bsync(s: ref BState, dfch: chan of (string, array of ref Sys->Dir), pats: list of string)
+{
+ (epath, bpath) := (s.epath, s.bpath);
+ cno := 0;
+ prefix, e1, e2: string = "";
+
+ # find maximal prefix of epath and bpath.
+ for (;;) {
+ p1, p2: string;
+ (e1, p1) = nextelem(epath);
+ (e2, p2) = nextelem(bpath);
+ if (e1 == nil || e1 != e2)
+ break;
+ prefix = pathcat(prefix, e1);
+ (epath, bpath) = (p1, p2);
+ cno++;
+ }
+
+ if (epath == nil) {
+ if (bpath != nil) {
+ s.b.delete(cno);
+ s.b.select(cno - 1, nil);
+ s.bpath = prefix;
+ }
+ return;
+ }
+
+ # if the paths have no prefix in common then we're starting
+ # at a different root - don't do anything until
+ # we know we have at least one full element.
+ # even then, if it's not a directory, we have to ignore it.
+ if (cno == 0 && islastelem(epath))
+ return;
+
+ if (e1 != nil && islastelem(epath)) {
+ # find first prefix-matching entry.
+ match := "";
+ for ((i, ents) := (0, s.b.entries(cno - 1)); i < len ents; i++) {
+ m := ents[i];
+ if (len m >= len e1 && m[0:len e1] == e1) {
+ match = deslash(m);
+ break;
+ }
+ }
+ if (match != nil) {
+ if (match == e2 && islastelem(bpath))
+ return;
+
+ epath = pathcat(match, epath[len e1:]);
+ e1 = match;
+ if (e1 == e2)
+ cno++;
+ } else {
+ s.b.delete(cno);
+ s.bpath = prefix;
+ return;
+ }
+ }
+
+ s.b.delete(cno);
+ s.b.select(cno - 1, e1);
+ np := pathcat(prefix, e1);
+ if (s.dirfetchpid != -1) {
+ if (np == s.dirfetchpath)
+ return;
+ kill(s.dirfetchpid);
+ s.dirfetchpid = -1;
+ }
+ (ok, dir) := sys->stat(np);
+ if (ok != -1 && (dir.mode & Sys->DMDIR) != 0) {
+ sync := chan of int;
+ spawn dirfetch(np, e1, sync, dfch, pats);
+ s.dirfetchpid = <-sync;
+ s.dirfetchpath = np;
+ } else if (ok != -1)
+ s.bpath = np;
+ else
+ s.bpath = prefix;
+}
+
+dirfetch(p: string, t: string, sync: chan of int,
+ dfch: chan of (string, array of ref Sys->Dir),
+ pats: list of string)
+{
+ sync <-= sys->pctl(0, nil);
+ (a, e) := readdir->init(p, Readdir->NAME|Readdir->COMPACT);
+ if (e != -1) {
+ j := 0;
+ for (i := 0; i < len a; i++) {
+ pl := pats;
+ if ((a[i].mode & Sys->DMDIR) == 0) {
+ for (; pl != nil; pl = tl pl)
+ if (filepat->match(hd pl, a[i].name))
+ break;
+ }
+ if (pl != nil || pats == nil)
+ a[j++] = a[i];
+ }
+ a = a[0:j];
+ }
+ dfch <-= (t, a);
+}
+
+dist(top: ref Tk->Toplevel, s: string): int
+{
+ cmd(top, "frame .xxxx -width " + s);
+ d := int cmd(top, ".xxxx cget -width");
+ cmd(top, "destroy .xxxx");
+ return d;
+}
+
+Browser.init(top: ref Tk->Toplevel, w: string, colwidth: string): (ref Browser, chan of string)
+{
+ b := ref Browser;
+ b.top = top;
+ b.ncols = 0;
+ b.colwidth = dist(top, colwidth);
+ b.w = w;
+ cmd(b.top, "frame " + b.w);
+ cmd(b.top, "canvas " + b.w + ".c -width 0 -height 0 -xscrollcommand {" + b.w + ".s set}");
+ cmd(b.top, "frame " + b.w + ".c.f -bd 0");
+ cmd(b.top, "pack propagate " + b.w + ".c.f 0");
+ cmd(b.top, b.w + ".c create window 0 0 -tags win -window " + b.w + ".c.f -anchor nw");
+ cmd(b.top, "scrollbar "+b.w+".s -command {"+b.w+".c xview} -orient horizontal");
+ cmd(b.top, "bind "+b.w+".c <Configure> {"+b.w+".c itemconfigure win -height ["+b.w+".c cget -actheight]}");
+ cmd(b.top, "pack "+b.w+".c -side top -fill both -expand 1");
+ cmd(b.top, "pack "+b.w+".s -side top -fill x");
+ ch := chan of string;
+ tk->namechan(b.top, ch, "colch");
+ return (b, ch);
+}
+
+xview(top: ref Tk->Toplevel, w: string): (real, real)
+{
+ s := tk->cmd(top, w + " xview");
+ if (s != nil && s[0] != '!') {
+ (n, v) := sys->tokenize(s, " ");
+ if (n == 2)
+ return (real hd v, real hd tl v);
+ }
+ return (0.0, 0.0);
+}
+
+setscrollregion(b: ref Browser)
+{
+ (w, h) := (b.colwidth * (b.ncols + 1), int cmd(b.top, b.w + ".c cget -actheight"));
+ cmd(b.top, b.w+".c.f configure -width " + string w + " -height " + string h);
+# w := int cmd(b.top, b.w+".c.f cget -actwidth");
+# w += int cmd(b.top, b.w+".c cget -actwidth") - b.colwidth;
+# h := int cmd(b.top, b.w+".c.f cget -actheight");
+ if (w > 0 && h > 0)
+ cmd(b.top, b.w + ".c configure -scrollregion {0 0 " + string w + " " + string h + "}");
+ (start, end) := xview(b.top, b.w+".c");
+ if (end > 1.0)
+ cmd(b.top, b.w+".c xview scroll left 0 units");
+}
+
+Browser.addcol(b: self ref Browser, title: string, d: array of string)
+{
+ ncol := string b.ncols++;
+
+ f := b.w + ".c.f.d" + ncol;
+ cmd(b.top, "frame " + f + " -bg green -width " + string b.colwidth);
+
+ t := f + ".t";
+ cmd(b.top, "label " + t + " -text " + tk->quote(title) + " -bg black -fg white");
+
+ sb := f + ".s";
+ lb := f + ".l";
+ cmd(b.top, "scrollbar " + sb +
+ " -command {" + lb + " yview}");
+
+ cmd(b.top, "listbox " + lb +
+ " -selectmode browse" +
+ " -yscrollcommand {" + sb + " set}" +
+ " -bd 2");
+
+ cmd(b.top, "bind " + lb + " <ButtonRelease-1> +{send colch s " + ncol + "}");
+ cmd(b.top, "bind " + lb + " <Double-Button-1> +{send colch d " + ncol + "}");
+ cmd(b.top, "pack propagate " + f + " 0");
+ cmd(b.top, "pack " + t + " -side top -fill x");
+ cmd(b.top, "pack " + sb + " -side left -fill y");
+ cmd(b.top, "pack " + lb + " -side left -fill both -expand 1");
+ cmd(b.top, "pack " + f + " -side left -fill y");
+ for (i := 0; i < len d; i++)
+ cmd(b.top, lb + " insert end '" + d[i]);
+ setscrollregion(b);
+ seecol(b, b.ncols - 1);
+}
+
+Browser.resize(b: self ref Browser)
+{
+ if (b.ncols == 0)
+ return;
+ setscrollregion(b);
+}
+
+seecol(b: ref Browser, cno: int)
+{
+ w := b.w + ".c.f.d" + string cno;
+ min := int cmd(b.top, w + " cget -actx");
+ max := min + int cmd(b.top, w + " cget -actwidth") +
+ 2 * int cmd(b.top, w + " cget -bd");
+ min = int cmd(b.top, b.w+".c canvasx " + string min);
+ max = int cmd(b.top, b.w +".c canvasx " + string max);
+
+ # see first the right edge; then the left edge, to ensure
+ # that the start of a column is visible, even if the window
+ # is narrower than one column.
+ cmd(b.top, b.w + ".c see " + string max + " 0");
+ cmd(b.top, b.w + ".c see " + string min + " 0");
+}
+
+Browser.delete(b: self ref Browser, colno: int)
+{
+ while (b.ncols > colno)
+ cmd(b.top, "destroy " + b.w+".c.f.d" + string --b.ncols);
+ setscrollregion(b);
+}
+
+Browser.selection(b: self ref Browser, cno: int): string
+{
+ if (cno >= b.ncols || cno < 0)
+ return nil;
+ l := b.w+".c.f.d" + string cno + ".l";
+ sel := cmd(b.top, l + " curselection");
+ if (sel == nil)
+ return nil;
+ return cmdget(b.top, l + " get " + sel);
+}
+
+Browser.select(b: self ref Browser, cno: int, e: string)
+{
+ if (cno < 0 || cno >= b.ncols)
+ return;
+ l := b.w+".c.f.d" + string cno + ".l";
+ cmd(b.top, l + " selection clear 0 end");
+ if (e == nil)
+ return;
+ ents := b.entries(cno);
+ for (i := 0; i < len ents; i++) {
+ if (deslash(ents[i]) == e) {
+ cmd(b.top, l + " selection set " + string i);
+ cmd(b.top, l + " see " + string i);
+ return;
+ }
+ }
+}
+
+Browser.entries(b: self ref Browser, cno: int): array of string
+{
+ if (cno < 0 || cno >= b.ncols)
+ return nil;
+ l := b.w+".c.f.d" + string cno + ".l";
+ nent := int cmd(b.top, l + " index end") + 1;
+ ents := array[nent] of string;
+ for (i := 0; i < len ents; i++)
+ ents[i] = cmdget(b.top, l + " get " + string i);
+ return ents;
+}
+
+# turn each pattern of the form "*.b (Limbo files)" into "*.b".
+# ignore '*' as it's a hangover from a past age.
+makepats(pats: list of string): (list of string, string)
+{
+ np: list of string;
+ s := "";
+ for (; pats != nil; pats = tl pats) {
+ p := hd pats;
+ for (i := 0; i < len p; i++)
+ if (p[i] == ' ')
+ break;
+ pat := p[0:i];
+ if (p != "*") {
+ np = p[0:i] :: np;
+ s += hd np;
+ if (tl pats != nil)
+ s[len s] = ' ';
+ }
+ }
+ return (np, s);
+}
+
+widgetwidth(top: ref Tk->Toplevel, w: string): int
+{
+ return int cmd(top, w + " cget -width") + 2 * int cmd(top, w + " cget -bd");
+}
+
+skipslash(path: string): string
+{
+ for (i := 0; i < len path; i++)
+ if (path[i] != '/')
+ return path[i:];
+ return nil;
+}
+
+nextelem(path: string): (string, string)
+{
+ if (path == nil)
+ return (nil, nil);
+ if (path[0] == '/')
+ return ("/", skipslash(path));
+ for (i := 0; i < len path; i++)
+ if (path[i] == '/')
+ break;
+ return (path[0:i], skipslash(path[i:]));
+}
+
+islastelem(path: string): int
+{
+ for (i := 0; i < len path; i++)
+ if (path[i] == '/')
+ return 0;
+ return 1;
+}
+
+pathcat(path, elem: string): string
+{
+ if (path != nil && path[len path - 1] != '/')
+ path[len path] = '/';
+ return path + elem;
+}
+
+# remove a possible trailing slash
+deslash(s: string): string
+{
+ if (len s > 0 && s[len s - 1] == '/')
+ s = s[0:len s - 1];
+ return s;
+}
+
+#
+# find upper left corner for subsidiary child window (always at constant
+# position relative to parent)
+#
+localgeom(im: ref Draw->Image): string
+{
+ if (im == nil)
+ return nil;
+
+ return sys->sprint("-x %d -y %d", im.r.min.x+STEP, im.r.min.y+STEP);
+}
+
+centre(t: ref Tk->Toplevel)
+{
+ org: Point;
+ org.x = t.screenr.dx() / 2 - int cmd(t, ". cget -width") / 2;
+ org.y = t.screenr.dy() / 3 - int cmd(t, ". cget -height") / 2;
+ if (org.y < 0)
+ org.y = 0;
+ cmd(t, ". configure -x " + string org.x + " -y " + string org.y);
+}
+
+tkcmds(top: ref Tk->Toplevel, a: array of string)
+{
+ n := len a;
+ for(i := 0; i < n; i++)
+ tk->cmd(top, a[i]);
+}
+
+topopts := array[] of {
+ "font"
+# , "bd" # Wait for someone to ask for these
+# , "relief" # Note: colors aren't inherited, it seems
+};
+
+opts(top: ref Tk->Toplevel) : string
+{
+ if (top == nil)
+ return nil;
+ opts := "";
+ for ( i := 0; i < len topopts; i++ ) {
+ cfg := tk->cmd(top, ". cget " + topopts[i]);
+ if ( cfg != "" && cfg[0] != '!' )
+ opts += " -" + topopts[i] + " " + tk->quote(cfg);
+ }
+ return opts;
+}
+
+kill(pid: int): int
+{
+ fd := sys->open("/prog/"+string pid+"/ctl", Sys->OWRITE);
+ if (fd == nil)
+ return -1;
+ if (sys->write(fd, array of byte "kill", 4) != 4)
+ return -1;
+ return 0;
+}
+Showtk: con 0;
+
+cmd(top: ref Tk->Toplevel, s: string): string
+{
+ if (Showtk)
+ sys->print("%s\n", s);
+ e := tk->cmd(top, s);
+ if (e != nil && e[0] == '!')
+ sys->fprint(sys->fildes(2), "tkclient: tk error %s on '%s'\n", e, s);
+ return e;
+}
+
+cmdget(top: ref Tk->Toplevel, s: string): string
+{
+ if (Showtk)
+ sys->print("%s\n", s);
+ tk->cmd(top, "variable lasterror");
+ e := tk->cmd(top, s);
+ lerr := tk->cmd(top, "variable lasterror");
+ if (lerr != nil) sys->fprint(sys->fildes(2), "tkclient: tk error %s on '%s'\n", e, s);
+ return e;
+}