(* Copyright (C) 1992, Digital Equipment Corporation *) (* All rights reserved. *) (* See the file COPYRIGHT for a full description. *) (* *) (* Last modified on Tue Jun 16 13:08:52 PDT 1992 by muller *) (* modified on Fri May 8 19:15:11 PDT 1992 by meehan *) (* modified on Fri Mar 20 22:42:45 1992 by steveg *) (* modified on Fri Jan 17 16:06:38 PST 1992 by mhb *) (* modified on Thu Apr 4 18:16:18 PST 1991 by brooks *) (* modified on Mon Sep 24 12:10:58 PDT 1990 by mcjones *) MODULE FileBrowserVBT; IMPORT AnchorSplit, AnyEvent, Axis, BorderedVBT, Char, Filename, Filter, Font, HVSplit, Key, Lex, List, MenuSwitchVBT, Multi, Rd, Shadow, ShadowedFeedbackVBT, Split, Text, TextBrowserVBT, TextList, TextPort, TextRd, TextVBT, TextWr, Thread, UnixUtils, VBT, Wr; <* PRAGMA LL *> REVEAL T = Public BRANDED "FileBrowserVBT 1.0" OBJECT fmu: MUTEX; (* guarding our local parameters and state *) helper : Helper; dirmenu: DirMenu; (* parameters *) suffixes: TextList.T; readOnly: BOOLEAN; prefs : SET OF Preference; (* state *) dir: TEXT; (* Current directory, without final '/' *) inError: BOOLEAN; (* helper contains incorrect pathname *) (* inner workings *) truthInHelper: BOOLEAN; (* where to look for the value *) time: CARDINAL; (* last time we looked at this directory *) bkg : Thread.T; (* background thread *) OVERRIDES (* of TextBrowserVBT.T *) callback := Callback; (* of FileBrowser.Public *) init := Init; selectItems := IgnoreItemSelection; activateFile := IgnoreFileActivation; activateDir := DefaultActivateDir; error := IgnoreError; END; Helper = PublicHelper BRANDED OBJECT parent: T; OVERRIDES init := InitHelper; filter := HelperFilter; modified := HelperModified END; TYPE BgClosure = Thread.Closure OBJECT v : T; dir : TEXT; satisfied: TextList.T; others : TextList.T; OVERRIDES apply := InBackground END; REVEAL DirMenu = PublicDirMenu BRANDED OBJECT font := Font.BuiltIn; shadow : Shadow.T := NIL; (* Shadow.None *) filebrowser: T; top: TextVBT.T; vbox: HVSplit.T; OVERRIDES init := InitDirMenu END; TYPE DirMenuTop = ShadowedFeedbackVBT.T OBJECT dm: DirMenu END; DirMenuButton = MenuSwitchVBT.T OBJECT dm: DirMenu METHODS init (text: TEXT): DirMenuButton := InitDirMenuButton; put (text: TEXT) := DirMenuButtonPut; get (): TEXT := DirMenuButtonGet; OVERRIDES callback := DirMenuButtonCallback END; (**************************** Creation ***************************) PROCEDURE Init (v : T; multiColumn: BOOLEAN; font : Font.T; shadow : Shadow.T := NIL; style : Shadow.Style := Shadow.Style.Flat): T = BEGIN EVAL TextBrowserVBT.Multi.init (v, multiColumn, font, shadow, style); v.helper := NIL; v.dirmenu := NIL; v.fmu := NEW (MUTEX); v.suffixes := NIL; v.readOnly := FALSE; v.prefs := SET OF Preference {Preference.caseSensitive}; v.dir := ""; v.truthInHelper := FALSE; RETURN v END Init; PROCEDURE InitHelper (helper : Helper; hMargin: REAL := 1.5; vMargin: REAL := 1.5; font : Font.T := Font.BuiltIn; shadow : Shadow.T := NIL ): Helper = BEGIN EVAL TextPort.T.init (helper, TRUE, hMargin, vMargin, font, shadow); RETURN helper END InitHelper; PROCEDURE SetHelper (v: T; helper: Helper) RAISES {Error} = BEGIN LOCK v.fmu DO v.helper := helper; IF helper # NIL THEN helper.parent := v; EVAL Normalize(v, path := "", time := 0, mayChangeDir := FALSE, force := FALSE) END END END SetHelper; PROCEDURE InitDirMenu (dm : DirMenu; font : Font.T := Font.BuiltIn; shadow: Shadow.T := NIL; (* Shadow.None *) n : CARDINAL := 0 ): DirMenu = BEGIN IF shadow = NIL THEN shadow := Shadow.None END; dm.shadow := shadow; dm.font := font; dm.top := NEW (TextVBT.T).init ("", fnt := font, bgFg := shadow); dm.vbox := NEW (HVSplit.T).init (Axis.T.Ver); WITH feedback = NEW (DirMenuTop, dm := dm).init (NIL, shadow), menuFrame = NEW (Filter.T) DO EVAL AnchorSplit.T.init (dm, feedback, menuFrame, n); Multi.AddChild (dm, dm.top); Multi.AddChild ( dm, BorderedVBT.New (dm.vbox, 2.0 * 25.4 / 72.0, shadow.fg)); RETURN dm END END InitDirMenu; PROCEDURE InitDirMenuButton (dmb: DirMenuButton; text: TEXT): DirMenuButton = VAR textvbt := TextVBT.New (text, fnt := dmb.dm.font, bgFg := dmb.dm.shadow); menubutton := ShadowedFeedbackVBT.NewMenu (textvbt, dmb.dm.shadow); BEGIN EVAL MenuSwitchVBT.T.init (dmb, menubutton); RETURN dmb END InitDirMenuButton; PROCEDURE SetDirMenu (v: T; dm: DirMenu) = BEGIN LOCK v.fmu DO v.dirmenu := dm; IF dm # NIL THEN dm.filebrowser := v; EVAL Normalize (v, path := "", time := 0, mayChangeDir := FALSE, force := TRUE) END END END SetDirMenu; (************************* Client interface ***********************) PROCEDURE SetReadOnly (v: T; readOnly: BOOLEAN) = BEGIN LOCK v.fmu DO v.readOnly := readOnly END END SetReadOnly; PROCEDURE SetSuffixes (v: T; suffixes: TEXT) = BEGIN WITH list = ParseSuffixes (suffixes) DO LOCK v.fmu DO v.suffixes := list; v.time := 0; (* force true redisplay next chance *) END END END SetSuffixes; PROCEDURE ParseSuffixes (suffixes: TEXT): TextList.T = VAR list : TextList.T := NIL; rd := TextRd.New (suffixes); suffix: TEXT; <* FATAL Thread.Alerted *> BEGIN TRY TRY LOOP Lex.Skip (rd, Char.All - Char.AlphaNumerics); suffix := Lex.Scan (rd, Char.AlphaNumerics); IF Text.Empty (suffix) THEN EXIT END; TextList.Push (list, suffix) END FINALLY Rd.Close (rd) END EXCEPT Rd.Failure => END; RETURN list END ParseSuffixes; PROCEDURE SetPreference (v: T; p: Preference; value: BOOLEAN) = BEGIN LOCK v.fmu DO IF value THEN v.prefs := v.prefs + SET OF Preference{p} ELSE v.prefs := v.prefs - SET OF Preference{p} END; v.time := 0; (* force true redisplay next chance *) END END SetPreference; PROCEDURE Set (v: T; path: TEXT; time: VBT.TimeStamp := 0) RAISES {Error} = BEGIN LOCK v.fmu DO EVAL Normalize (v, path, time, mayChangeDir := TRUE, force := TRUE) END END Set; PROCEDURE Normalize (v : T; path : TEXT; time : VBT.TimeStamp; mayChangeDir: BOOLEAN := TRUE; force : BOOLEAN := FALSE ): BOOLEAN (* whether selected item is a directory *) RAISES {Error} = <* LL = v.fmu *> (* Main internal routine to take a pathname and get it displayed in normal form: absolute pathname, tildes expanded, file selected in browser if it exists. Errors are detected here. *) BEGIN IF Text.Empty (path) THEN path := "./" ELSE TRY path := Filename.ExpandTilde (path) EXCEPT | Filename.Error => RaiseError (v, text := "Couldn't expand tilde", path := path) END END; IF Text.GetChar (path, 0) = '/' (* absolute *) THEN (* skip *) ELSIF Text.Empty (v.dir) THEN TRY path := UnixUtils.GetWD () & "/" & path EXCEPT | UnixUtils.Error => RaiseError (v, text := "getwd failed", path := path) END ELSE path := v.dir & "/" & path END; VAR dir, file : Text.T; dirExists, fileExists, fileIsDir: BOOLEAN; BEGIN dir := Filename.Head (path); file := Filename.Tail (path); TRY dirExists := UnixUtils.ProbeFile (dir, error := TRUE); EXCEPT | UnixUtils.Error => RaiseError (v, text := "probefile failed", path := dir) END; TRY fileExists := NOT Text.Empty (file) AND UnixUtils.ProbeFile (path, error := v.readOnly) EXCEPT | UnixUtils.Error => RaiseError (v, text := "probefile failed", path := path) END; fileIsDir := fileExists AND UnixUtils.IsDirectory (path); IF fileIsDir THEN dir := path; file := ""; END; DisplayDir (v, CompressPath (dir), redisplaying := FALSE, force := force OR mayChangeDir AND fileIsDir); IF NOT Text.Empty (file) THEN TextBrowserVBT.SelectText (v, file) END; IF v.helper # NIL THEN TextPort.SetModified (v.helper, FALSE); IF fileExists OR NOT v.readOnly THEN ShowFileInHelper (v, file); v.truthInHelper := TRUE (* Redundant. See HelperModified. *) ELSE ShowFileInHelper (v, ""); v.truthInHelper := FALSE END; IF time # 0 THEN TextPort.Select ( v.helper, time := time, begin := 0, end := LAST (CARDINAL), sel := TextPort.SelectionType.Primary, replaceMode := TRUE) END END; RETURN fileIsDir END END Normalize; PROCEDURE CompressPath (path: Text.T): Text.T = PROCEDURE chop (path: Text.T): Text.T = VAR k := Text.FindCharR(path, '/'); BEGIN IF k < 1 THEN RETURN path & "/.." END; WITH field = Text.Sub(path, k + 1, LAST(CARDINAL)) DO IF Text.Equal(field, "..") THEN RETURN path & "/.." END; END; RETURN Text.Sub(path, 0, k) END chop; VAR prev, pos : INTEGER; newpath, field: Text.T; BEGIN IF Text.Empty(path) THEN RETURN path END; IF Text.GetChar(path, 0) = '/' THEN pos := Text.FindChar(path, '/', 1) ELSE pos := Text.FindChar(path, '/', 0) END; IF pos = -1 THEN RETURN path END; newpath := Text.Sub(path, 0, pos); WHILE pos # LAST(CARDINAL) DO prev := pos + 1; pos := Text.FindChar(path, '/', prev); IF pos = -1 THEN pos := LAST(CARDINAL) END; field := Text.Sub(path, prev, pos - prev); IF Text.Empty(field) OR Text.Equal(field, ".") THEN (* ignore *) ELSIF Text.Equal(field, "..") THEN newpath := chop(newpath); ELSE newpath := newpath & "/" & field END; END; RETURN newpath; END CompressPath; PROCEDURE Unselect (v: T) = BEGIN TextBrowserVBT.Select (v, -1) END Unselect; PROCEDURE Refresh (v: T) RAISES {Error} = BEGIN LOCK v.fmu DO IF Text.Empty (v.dir) THEN RETURN END; TRY EVAL UnixUtils.ProbeFile (v.dir, error := TRUE) EXCEPT | UnixUtils.Error => TextBrowserVBT.SetText (v, "", new := TRUE); RaiseError (v, "No such file: ", v.dir) END; IF UnixUtils.FileModifyTime (v.dir) > v.time THEN WITH selection = TextBrowserVBT.GetMultiSelection (v) DO DisplayDir (v, v.dir, redisplaying := TRUE, force := FALSE); TextBrowserVBT.SelectMulti (v, selection^) END END END END Refresh; PROCEDURE GetDir (v: T): TEXT = BEGIN LOCK v.fmu DO RETURN v.dir END END GetDir; PROCEDURE GetFile (v : T; shortName: BOOLEAN := FALSE; normalize: BOOLEAN := TRUE ): Text.T RAISES {Error} = BEGIN WITH files = GetFiles (v, shortName, normalize) DO IF files = NIL THEN RETURN "" ELSE RETURN TextList.First (files) END END END GetFile; PROCEDURE GetFiles (v : T; shortName: BOOLEAN := FALSE; normalize: BOOLEAN := TRUE ): TextList.T RAISES {Error} = PROCEDURE full (file: TEXT): TEXT = BEGIN RETURN v.dir & "/" & file END full; VAR file: TEXT; BEGIN LOCK v.fmu DO IF v.truthInHelper THEN file := TextPort.GetText (v.helper); IF NOT normalize THEN (* can't shorten, don't know enough *) RETURN TextList.List1 (file) ELSE EVAL Normalize (v, path := file, time := 0, mayChangeDir := FALSE, force := FALSE); IF v.truthInHelper THEN file := TextPort.GetText (v.helper); IF v.dirmenu # NIL THEN file := full (file) END; IF shortName THEN file := Filename.Tail (file) END; RETURN TextList.List1 (file) END END END; (* truth in browser, perhaps after normalizing above *) IF Text.Empty (v.dir) THEN RETURN NIL ELSE WITH files = TextBrowserVBT.GetMultiTextList (v) DO IF shortName OR files = NIL THEN RETURN files ELSE RETURN TextList.Map (files, full) END END END END END GetFiles; PROCEDURE IsDir (filename: TEXT): BOOLEAN = BEGIN WITH length = Text.Length (filename) DO RETURN length > 0 AND Text.GetChar (filename, length - 1) = '/' END END IsDir; (********************** Displaying a directory ***********************) PROCEDURE DisplayDir (v : T; dirname : TEXT; redisplaying: BOOLEAN := FALSE; force : BOOLEAN := FALSE ) RAISES {Error} = (* DisplayDir: display a new directory. Set may call DisplayDir with a directory which might or might not really be accessible. If it isn't accessible, Error will be raised. *) <* LL = v.fmu *> VAR satFiles, others, allfiles: TextList.T := NIL; PROCEDURE partition (file: TEXT) = BEGIN IF Satisfies (file, v.suffixes) THEN TextList.Push (satFiles, file) ELSE TextList.Push (others, file) END END partition; BEGIN IF NOT force AND Text.Equal (dirname, v.dir) THEN RETURN END; IF v.bkg # NIL THEN Thread.Alert (v.bkg) END; TRY allfiles := UnixUtils.Directory (dirname) EXCEPT | UnixUtils.Error => RaiseError (v, text := "opendir failed", path := dirname) END; IF v.suffixes = NIL THEN satFiles := allfiles ELSE TextList.Walk (allfiles, partition) END; TRY satFiles := TextList.SortD (satFiles, Text.Compare) EXCEPT | Thread.Alerted => RETURN END; TextBrowserVBT.SetItems (v, satFiles, NOT redisplaying); v.dir := dirname; ShowDirInMenu (v); v.bkg := Thread.Fork (NEW (BgClosure, v := v, dir := v.dir, satisfied := satFiles, others := others)) END DisplayDir; PROCEDURE DirMenuButtonPut (dmb: DirMenuButton; text: TEXT) = VAR menubutton: ShadowedFeedbackVBT.T := Filter.Child (dmb); textvbt : TextVBT.T := Filter.Child (menubutton); BEGIN TextVBT.Put (textvbt, text) END DirMenuButtonPut; PROCEDURE DirMenuButtonGet (dmb: DirMenuButton): TEXT = VAR menubutton: ShadowedFeedbackVBT.T := Filter.Child (dmb); textvbt : TextVBT.T := Filter.Child (menubutton); BEGIN RETURN TextVBT.Get (textvbt) END DirMenuButtonGet; PROCEDURE DirMenuButtonCallback ( dmb: DirMenuButton; READONLY cd : VBT.MouseRec ) = <* LL = VBT.mu *> VAR path := dmb.get () & "/"; vbox := dmb.dm.vbox; next := dmb; BEGIN TRY LOOP next := Split.Succ (vbox, next); IF next = NIL THEN EXIT END; path := next.get () & "/" & path END; Set (dmb.dm.filebrowser, "/" & path, cd.time) EXCEPT | Split.NotAChild => dmb.dm.filebrowser.error ( NEW (E, v := dmb.dm.filebrowser, path := path, text := "Split.NotAChild (internal error)")) | Error (e) => dmb.dm.filebrowser.error (e) END END DirMenuButtonCallback; PROCEDURE InBackground (arg: BgClosure): REFANY RAISES {} = VAR items, others: TextList.T; pos : CARDINAL := LAST (CARDINAL); satFiles := arg.satisfied; BEGIN TRY others := TextList.SortD (arg.others, Text.Compare) EXCEPT Thread.Alerted => RETURN NIL END; WITH v = arg.v DO (* find non-satisfying directories and add them in order *) WHILE others # NIL DO WITH name = TextList.Pop (others) DO IF Thread.TestAlert () THEN RETURN NIL ELSIF UnixUtils.IsDirectory (arg.dir & "/" & name) THEN LOCK v.fmu DO items := TextBrowserVBT.GetItems (v); FOR i := 0 TO TextBrowserVBT.ItemCount (v) - 1 DO IF Text.Compare (name, TextList.Pop (items)) = -1 THEN pos := i; EXIT END END; TextBrowserVBT.Insert (v, name & "/", pos) END END END END; (* find (low probability) satisfying directories and mark them with a '/' *) WHILE satFiles # NIL DO WITH name = TextList.Pop (satFiles) DO IF Thread.TestAlert () THEN RETURN NIL ELSIF UnixUtils.IsDirectory (arg.dir & "/" & name) THEN LOCK v.fmu DO TextBrowserVBT.ReplaceItem (v, name, name & "/") END END END END END; RETURN NIL END InBackground; (* Explanation of the InBackground synchronization algorithm: Synchronization is a challenge because the user might double-click a directory while a background thread is still working. If so, we don't want to hold up the user, we want to abort the background thread, which has just ceased to be relevant. A mainstream thread aborts a bkg thread by alerting it. The bkg thread has its own private data in the closure, so it can keep on working harmlessly for a while. The one thing it must not do is modify the TextBrowserVBT after it has been alerted. So before touching the TextBrowserVBT, it tests Thread.Alerted(). Since a mainstream thread calling Display holds v.mu, this guarantees that once a new call to Display is begun, a background thread cannot touch TextBrowserVBT. v.bkg is initially NIL; DisplayDir tests this before Alerting. Even if the thread has died, it does no harm to alert it. *) PROCEDURE Satisfies (file: TEXT; suffixes: TextList.T): BOOLEAN = VAR ext := Filename.Extension (file); BEGIN IF Text.Equal (ext, "") THEN ext := "$" END; WHILE suffixes # NIL DO IF Text.Equal (ext, TextList.Pop (suffixes)) THEN RETURN TRUE END END; RETURN FALSE END Satisfies; (*************************** User interface **************************) PROCEDURE Callback ( v : T; <* UNUSED *> value: CARDINAL; READONLY event: VBT.MouseRec) = BEGIN IF event.clickCount = 2 THEN BrowserActivate (v, event) ELSE BrowserSelect (v, event) END END Callback; PROCEDURE BrowserSelect (v: T; READONLY event: VBT.MouseRec) = (* the upclick *) <* LL = VBT.mu *> BEGIN v.truthInHelper := FALSE; IF v.helper # NIL THEN TextPort.SetModified (v.helper, TRUE); (* That prevents TextPort from calling the "modified" method, which is HelperModified, which sets v.truthInHelper to TRUE. *) TextPort.SetText (v.helper, ""); TextPort.SetModified (v.helper, FALSE) END; v.selectItems (AnyEvent.MouseToCode (event)) END BrowserSelect; PROCEDURE BrowserActivate (v: T; READONLY cd: VBT.MouseRec) = (* the upclick *) <* LL = VBT.mu *> BEGIN (* bug? -- if clicking on an directory item that the background thread hasn't modified with a trailing slash, IsDir thinks of the item as a file, not a directory *) WITH value = TextBrowserVBT.GetMultiSelection (v) [0], item = TextBrowserVBT.NthItem (v, value), event = AnyEvent.MouseToCode (cd) DO IF IsDir (item) THEN v.activateDir (item, event) ELSE IF v.helper # NIL THEN TextPort.SetModified (v.helper, FALSE); ShowFileInHelper (v, item); v.truthInHelper := TRUE; (* Redundant. *) TextPort.Select ( v.helper, time := cd.time, begin := 0, end := LAST (CARDINAL), sel := TextPort.SelectionType.Primary, replaceMode := TRUE) END; v.activateFile (item, event) END END END BrowserActivate; PROCEDURE IgnoreItemSelection (<* UNUSED *> v : T; <* UNUSED *> event: AnyEvent.Code) = BEGIN END IgnoreItemSelection; PROCEDURE IgnoreFileActivation (<* UNUSED *> v : T; <* UNUSED *> filename: TEXT; <* UNUSED *> event : AnyEvent.Code) = BEGIN END IgnoreFileActivation; PROCEDURE DefaultActivateDir (v: T; dirname: TEXT; event: AnyEvent.Code) = BEGIN WITH time = AnyEvent.TimeStamp (event) DO TRY Set (v, dirname, time) EXCEPT Error (x) => v.error (x) END END END DefaultActivateDir; PROCEDURE IgnoreError (<* UNUSED *> v: T; <* UNUSED *> err: E) = BEGIN END IgnoreError; PROCEDURE ShowFileInHelper (v: T; file: TEXT) = <* LL = v.fmu *> BEGIN IF v.helper = NIL THEN (* skip *) ELSIF v.dirmenu = NIL THEN (* Show the whole name. *) TextPort.SetText (v.helper, v.dir & "/" & file) ELSE (* Show just the file name. *) TextPort.SetText (v.helper, file) END END ShowFileInHelper; PROCEDURE ShowDirInMenu (v: T) = <* LL = v.fmu > VBT.mu *> VAR dm := v.dirmenu; top : TextVBT.T; vbox : HVSplit.T; prevChild: VBT.T := NIL; thisChild: DirMenuButton; dlist : TextList.T; BEGIN IF dm = NIL THEN RETURN END; top := dm.top; vbox := dm.vbox; dlist := Decompose (dm.filebrowser.dir); TextVBT.Put (top, TextList.Pop (dlist)); LOOP thisChild := Split.Succ (vbox, prevChild); IF thisChild = NIL THEN IF dlist = NIL THEN EXIT ELSE thisChild := NEW (DirMenuButton, dm := dm).init (TextList.Pop (dlist)); Split.Insert (vbox, prevChild, thisChild); prevChild := thisChild END ELSIF dlist = NIL THEN (* delete remaining children *) Split.Delete (vbox, Split.Succ (vbox, prevChild)) ELSE thisChild.put (TextList.Pop (dlist)); prevChild := thisChild END END END ShowDirInMenu; PROCEDURE Decompose (dir: TEXT): TextList.T = <* FATAL Rd.Failure, Thread.Alerted, Wr.Failure *>(* Can't happen *) VAR res: TextList.T := NIL; rd := TextRd.New (dir); wr := TextWr.New (); s : TEXT; c : CHAR; PROCEDURE addComponent () = BEGIN s := TextWr.ToText (wr); IF NOT Text.Empty (s) THEN TextList.Push (res, s) END END addComponent; BEGIN TRY LOOP c := Rd.GetChar (rd); IF c = '/' THEN addComponent () ELSE Wr.PutChar (wr, c) END END EXCEPT Rd.EndOfFile => END; addComponent (); Rd.Close (rd); Wr.Close (wr); IF res = NIL THEN RETURN TextList.New (".", NIL) ELSE RETURN res END END Decompose; PROCEDURE HelperModified (hp: Helper) = <* LL = v.fmu *> (* That's the locking level because this is the "modified" method of the Helper, which is invoked by TextPort.ReplaceInVText, which is called by TextPort.SetText, which is called by ShowFileInHelper and others. *) BEGIN WITH v = hp.parent DO Unselect (v); v.truthInHelper := TRUE END END HelperModified; PROCEDURE HelperFilter (hp: Helper; VAR (* inOut*) event: VBT.KeyRec) = BEGIN IF event.wentDown AND event.whatChanged = Key.Return THEN event.whatChanged := VBT.NoKey; HelperReturn (hp, event) END END HelperFilter; PROCEDURE HelperReturn (hp: Helper; READONLY event: VBT.KeyRec) = VAR isDir: BOOLEAN; BEGIN WITH v = hp.parent DO LOCK v.fmu DO TRY isDir := Normalize (v, TextPort.GetText (hp), time := event.time, mayChangeDir := TRUE, force := FALSE) EXCEPT | Error (x) => v.error (x); RETURN END END; WITH event = AnyEvent.KeyToCode (event) DO IF isDir THEN v.activateDir ("./", event) ELSE WITH path = TextPort.GetText (hp), tail = Filename.Tail (path) DO IF NOT Text.Empty (tail) THEN v.activateFile (tail, event) END END END END END END HelperReturn; PROCEDURE RaiseError (v: T; text, path: TEXT := "") RAISES {Error} = BEGIN RAISE Error (NEW (E, v := v, text := text, path := path)) END RaiseError; BEGIN END FileBrowserVBT.