MODULE Windows;
(**

   project   = "BlackBox"
   organization   = "www.oberon.ch"
   contributors   = "Oberon microsystems"
   version   = "System/Rsrc/About"
   copyright   = "System/Rsrc/About"
   license   = "Docu/BB-License"
   changes   = ""
   issues   = ""

**)

   IMPORT

      Kernel, Ports, Files, Services,
      Stores, Sequencers, Models, Views, Controllers, Properties,
      Dialog, Converters, Containers, Documents;
   CONST

      (** Window.flags **)
      isTool* = 0; isAux* = 1;
      noHScroll* = 2; noVScroll* = 3; noResize* = 4;
      allowDuplicates* = 5; neverDirty* = 6;
      (** Directory.Select lazy **)

      eager* = FALSE; lazy* = TRUE;
      notRecorded = 3;

   TYPE

      Window* = POINTER TO ABSTRACT RECORD
         port-: Ports.Port;
         frame-: Views.RootFrame;
         doc-: Documents.Document;
         seq-: Sequencers.Sequencer;
         link-: Window;   (* ring of windows with same sequencer *)
         sub-: BOOLEAN;
         flags-: SET;
         loc-: Files.Locator;
         name-: Files.Name;
         conv-: Converters.Converter
      END;
      Directory* = POINTER TO ABSTRACT RECORD

         l*, t*, r*, b*: INTEGER;
         minimized*, maximized*: BOOLEAN
      END;
      OpElem = POINTER TO RECORD


         next: OpElem;
         st: Stores.Store;
         op: Stores.Operation;
         name: Stores.OpName;
         invisible, transparent: BOOLEAN
      END;
      Script = POINTER TO RECORD (Stores.Operation)

         up: Script;
         list: OpElem;
         level: INTEGER;   (* nestLevel at creation time *)
         name: Stores.OpName
      END;
      StdSequencer = POINTER TO RECORD (Sequencers.Sequencer)

         home: Window;
         trapEra: INTEGER;   (* last observed TrapCount value *)
         modLevel: INTEGER;   (* dirty if modLevel > 0 *)
         entryLevel: INTEGER;   (* active = (entryLevel > 0) *)
         nestLevel: INTEGER;   (* nesting level of BeginScript/Modification *)
         modStack: ARRAY 64 OF RECORD store: Stores.Store; type: INTEGER END;
         lastSt: Stores.Store;
         lastOp: Stores.Operation;
         script: Script;
         undo, redo: OpElem;   (* undo/redo stacks *)
         noUndo: BOOLEAN;   (* script # NIL and BeginModification called *)
         invisibleLevel, transparentLevel, notRecordedLevel: INTEGER
      END;
      
      SequencerDirectory = POINTER TO RECORD (Sequencers.Directory) END;
      Forwarder = POINTER TO RECORD (Controllers.Forwarder) END;

      
      RootContext = POINTER TO RECORD (Models.Context)
         win: Window
      END;
      
      Reducer = POINTER TO RECORD (Kernel.Reducer) END;
      Hook = POINTER TO RECORD (Views.MsgHook) END;

      
      CheckAction = POINTER TO RECORD (Services.Action)
         wait: WaitAction
      END;
      WaitAction = POINTER TO RECORD (Services.Action)

         check: CheckAction
      END;
      
      LangNotifier = POINTER TO RECORD (Dialog.LangNotifier) END;
   VAR dir-, stdDir-: Directory;

   PROCEDURE ^ Reset (s: StdSequencer);

   PROCEDURE CharError;


   BEGIN
      Dialog.Beep
   END CharError;
   

   
   (** Window **)
   PROCEDURE (w: Window) Init* (port: Ports.Port), NEW;

   BEGIN
      ASSERT(w.port = NIL, 20); ASSERT(port # NIL, 21);
      w.port := port
   END Init;
   PROCEDURE (w: Window) SetTitle* (title: Views.Title), NEW, ABSTRACT;

   PROCEDURE (w: Window) GetTitle* (OUT title: Views.Title), NEW, ABSTRACT;
   PROCEDURE (w: Window) RefreshTitle* (), NEW, ABSTRACT;
   PROCEDURE (w: Window) SetSpec* (loc: Files.Locator; name: Files.Name; conv: Converters.Converter), NEW, EXTENSIBLE;

      VAR u: Window;
   BEGIN
      u := w;
      REPEAT
         u := u.link;
         u.loc := loc; u.name := name$; u.conv := conv
      UNTIL u = w
   END SetSpec;
   PROCEDURE (w: Window) Restore* (l, t, r, b: INTEGER), NEW;

      VAR f: Views.Frame; u, pw, ph: INTEGER;
   BEGIN
      f := w.frame;
      IF f # NIL THEN
         w.port.GetSize(pw, ph); u := w.port.unit;
         IF r > pw THEN r := pw END;
         IF b > ph THEN b := ph END;
         l := l * u - f.gx; t := t * u - f.gy; r := r * u - f.gx; b := b * u - f.gy;
         (* only adds to the BlackBox region, but doesn't draw: *)
         Views.UpdateRoot(w.frame, l, t, r, b, Views.keepFrames)   
      END
   END Restore;
   PROCEDURE (w: Window) Update*, NEW;

   BEGIN
      ASSERT(w.frame # NIL, 20);
      (* redraws the whole accumulated BlackBox region: *)
      Views.ValidateRoot(w.frame)
   END Update;
   PROCEDURE (w: Window) GetSize*(OUT width, height: INTEGER), NEW, EXTENSIBLE;

   BEGIN
      w.port.GetSize(width, height)
   END GetSize;
   
   PROCEDURE (w: Window) SetSize* (width, height: INTEGER), NEW, EXTENSIBLE;
      VAR c: Containers.Controller; w0, h0: INTEGER;
   BEGIN
      w.port.GetSize(w0, h0);
      w.port.SetSize(width, height);
      IF w.frame # NIL THEN Views.AdaptRoot(w.frame) END;
      c := w.doc.ThisController();
      IF c.opts * {Documents.winWidth, Documents.winHeight} # {} THEN
         w.Restore(0, 0, width, height)
      END
   END SetSize;
   PROCEDURE (w: Window) BroadcastModelMsg* (VAR msg: Models.Message), NEW, EXTENSIBLE;

   BEGIN
      IF w.frame # NIL THEN
         Views.BroadcastModelMsg(w.frame, msg)
      END
   END BroadcastModelMsg;
   PROCEDURE (w: Window) BroadcastViewMsg* (VAR msg: Views.Message), NEW, EXTENSIBLE;

   BEGIN
      IF w.frame # NIL THEN
         Views.BroadcastViewMsg(w.frame, msg)
      END
   END BroadcastViewMsg;
   PROCEDURE (w: Window) ForwardCtrlMsg* (VAR msg: Controllers.Message), NEW, EXTENSIBLE;

   BEGIN
      IF w.frame # NIL THEN
         WITH msg: Controllers.CursorMessage DO
            DEC(msg.x, w.frame.gx); DEC(msg.y, w.frame.gy)
         ELSE
         END;
         Views.ForwardCtrlMsg(w.frame, msg)
      END
   END ForwardCtrlMsg;
   PROCEDURE (w: Window) MouseDown* (x, y, time: INTEGER; modifiers: SET), NEW, ABSTRACT;

   PROCEDURE (w: Window) KeyDown* (ch: CHAR; modifiers: SET), NEW, EXTENSIBLE;

      VAR key: Controllers.EditMsg;
   BEGIN
      IF ch = 0X THEN
         CharError
      ELSE
         key.op := Controllers.pasteChar; key.char := ch;
         key.modifiers:= modifiers;
         w.ForwardCtrlMsg(key)
      END
   END KeyDown;
   PROCEDURE (w: Window) Close*, NEW, EXTENSIBLE;

      VAR u: Window; f: Views.Frame; s: Sequencers.Sequencer; msg: Sequencers.RemoveMsg;
   BEGIN
      u := w.link; WHILE u.link # w DO u := u.link END;
      u.link := w.link;
      f := w.frame; s := w.seq;
      IF ~w.sub THEN s.Notify(msg) END;
      WITH s: StdSequencer DO
         IF s.home = w THEN s.home := NIL END
      ELSE
      END;
      w.port.SetSize(0, 0); Views.AdaptRoot(w.frame);
      w.port := NIL; w.frame := NIL; w.doc := NIL; w.seq := NIL; w.link := NIL; w.loc := NIL;
      f.Close
   END Close;
   (** Directory **)


   PROCEDURE (d: Directory) NewSequencer* (): Sequencers.Sequencer, NEW;

      VAR s: StdSequencer;
   BEGIN
      NEW(s); Reset(s); RETURN s
   END NewSequencer;
   PROCEDURE (d: Directory) First* (): Window, NEW, ABSTRACT;


   PROCEDURE (d: Directory) Next* (w: Window): Window, NEW, ABSTRACT;
   PROCEDURE (d: Directory) New* (): Window, NEW, ABSTRACT;

   
   PROCEDURE (d: Directory) Open* (w: Window; doc: Documents.Document;
                                                      flags: SET; name: Views.Title;
                                                      loc: Files.Locator; fname: Files.Name;
                                                      conv: Converters.Converter),
                                                      NEW, EXTENSIBLE;
      VAR v: Views.View; c: RootContext; s: Sequencers.Sequencer; f: Views.Frame; any: ANYPTR;
   BEGIN
      ASSERT(w # NIL, 20); ASSERT(doc # NIL, 21); ASSERT(doc.context = NIL, 22);
      v := doc.ThisView(); ASSERT(v # NIL, 23);
      ASSERT(w.doc = NIL, 24); ASSERT(w.port # NIL, 25);
      IF w.link = NIL THEN w.link := w END;   (* create new window ring *)
      w.doc := doc; w.flags := flags;
      IF w.seq = NIL THEN
         ASSERT(doc.Domain() # NIL, 27);
         any := doc.Domain().GetSequencer();
         IF any # NIL THEN
            ASSERT(any IS Sequencers.Sequencer, 26);
            w.seq := any(Sequencers.Sequencer)
         ELSE
            w.seq := d.NewSequencer();
            doc.Domain().SetSequencer(w.seq)
         END         
      END;
      s := w.seq;
      WITH s: StdSequencer DO
         IF s.home = NIL THEN s.home := w END
      ELSE
      END;
      NEW(c); c.win := w; doc.InitContext(c);
      doc.GetNewFrame(f); w.frame := f(Views.RootFrame);
      w.frame.ConnectTo(w.port);
      Views.SetRoot(w.frame, w.doc, FALSE, w.flags);
      w.SetSpec(loc, fname, conv)
   END Open;
   PROCEDURE (d: Directory) OpenSubWindow* (w: Window; doc: Documents.Document; flags: SET; name: Views.Title), NEW, EXTENSIBLE;

      VAR u: Window; title: Views.Title;
   BEGIN
      ASSERT(w # NIL, 20); ASSERT(doc # NIL, 21);
      u := d.First(); WHILE (u # NIL) & (u.seq # doc.Domain().GetSequencer()) DO u := d.Next(u) END;
      IF u # NIL THEN
         w.sub := TRUE;
         w.link := u.link; u.link := w;
         w.seq := u.seq; w.loc := u.loc; w.name := u.name; w.conv := u.conv;
         u.GetTitle(title);
         d.Open(w, doc, flags, title, u.loc, u.name, u.conv)
      ELSE
         d.Open(w, doc, flags, name, NIL, "", NIL)
      END
   END OpenSubWindow;
   PROCEDURE ^ RestoreSequencer(seq: Sequencers.Sequencer);

   PROCEDURE (d: Directory) Focus* (target: BOOLEAN): Window, NEW, ABSTRACT;

   PROCEDURE (d: Directory) GetThisWindow* (p: Ports.Port; px, py: INTEGER; OUT x, y: INTEGER; OUT w: Window), NEW, ABSTRACT;
   PROCEDURE (d: Directory) Select* (w: Window; lazy: BOOLEAN), NEW, ABSTRACT;
   PROCEDURE (d: Directory) Close* (w: Window), NEW, ABSTRACT;
   PROCEDURE (d: Directory) Update* (w: Window), NEW;

      VAR u: Window;
   BEGIN
      (* redraws the BlackBox region of a given window, or of all windows *)
      u := d.First();
      WHILE u # NIL DO
         ASSERT(u.frame # NIL, 101);
         IF (u = w) OR (w = NIL) THEN RestoreSequencer(u.seq) END;
         u := d.Next(u)
      END
   END Update;
   
   PROCEDURE (d: Directory) GetBounds* (OUT w, h: INTEGER), NEW, ABSTRACT;
   (* RootContext *)


   PROCEDURE (c: RootContext) GetSize (OUT w, h: INTEGER);

   BEGIN
      c.win.port.GetSize(w, h);
      w := w * c.win.port.unit; h := h * c.win.port.unit
   END GetSize;
   PROCEDURE (c: RootContext) SetSize (w, h: INTEGER);

   END SetSize;
   
   PROCEDURE (c: RootContext) Normalize (): BOOLEAN;
   BEGIN
      RETURN TRUE
   END Normalize;
   
   PROCEDURE (c: RootContext) ThisModel (): Models.Model;
   BEGIN
      RETURN NIL
   END ThisModel;
   (* sequencing utilities *)


   PROCEDURE Prepend (s: Script; st: Stores.Store; IN name: Stores.OpName; op: Stores.Operation);

      VAR e: OpElem;
   BEGIN
      ASSERT(op # NIL, 20);
      NEW(e); e.st := st; e.op := op; e.name := name;
      e.next := s.list; s.list := e
   END Prepend;
   PROCEDURE Push (VAR list, e: OpElem);

   BEGIN
      e.next := list; list := e
   END Push;
   PROCEDURE Pop (VAR list, e: OpElem);

   BEGIN
      e := list; list := list.next
   END Pop;
   PROCEDURE Reduce (VAR list: OpElem; max: INTEGER);

      VAR e: OpElem;
   BEGIN
      e := list; WHILE (max > 1) & (e # NIL) DO DEC(max); e := e.next END;
      IF e # NIL THEN e.next := NIL END
   END Reduce;
   
   PROCEDURE (r: Reducer) Reduce (full: BOOLEAN);
      VAR e: OpElem; n: INTEGER; w: Window;
   BEGIN
      IF dir # NIL THEN
         w := dir.First();
         WHILE w # NIL DO
            IF w.seq IS StdSequencer THEN
               IF full THEN
                  n := 1
               ELSE
                  n := 0; e := w.seq(StdSequencer).undo;
                  WHILE e # NIL DO INC(n); e := e.next END;
                  IF n > 20 THEN n := n DIV 2 ELSE n := 10 END
               END;
               Reduce(w.seq(StdSequencer).undo, n)
            END;
            w := dir.Next(w)
         END
      END;
      Kernel.InstallReducer(r)
   END Reduce;
   PROCEDURE Reset (s: StdSequencer);

   BEGIN
      s.trapEra := Kernel.trapCount;
      IF (s.entryLevel # 0) OR (s.nestLevel # 0) THEN
         s.modLevel := 0;
         s.entryLevel := 0;
         s.nestLevel := 0;
         s.lastSt := NIL;
         s.lastOp := NIL;
         s.script := NIL;
         s.noUndo := FALSE;
         s.undo := NIL; s.redo := NIL;
         s.invisibleLevel := 0;
         s.transparentLevel := 0;
         s.notRecordedLevel := 0
      END
   END Reset;
   PROCEDURE Neutralize (st: Stores.Store);

      VAR neutralize: Models.NeutralizeMsg;
   BEGIN
      IF st # NIL THEN   (* st = NIL for scripts *)
         WITH st: Models.Model DO
            Models.Broadcast(st, neutralize)
         | st: Views.View DO
            st.Neutralize
         ELSE
         END
      END
   END Neutralize;
   PROCEDURE Do (s: StdSequencer; st: Stores.Store; op: Stores.Operation);

   BEGIN
      INC(s.entryLevel); s.lastSt := NIL; s.lastOp := NIL;
      Neutralize(st); op.Do;
      DEC(s.entryLevel)
   END Do;
   PROCEDURE AffectsDoc (s: StdSequencer; st: Stores.Store): BOOLEAN;

      VAR v, w: Window;
   BEGIN
      w := s.home;
      IF (w = NIL) OR (st = w.doc) OR (st = w.doc.ThisView()) THEN
         RETURN TRUE
      ELSE
         v := w.link;
         WHILE (v # w) & (st # v.doc) & (st # v.doc.ThisView()) DO v := v.link END;
         RETURN v = w
      END
   END AffectsDoc;
   (* Script *)


   PROCEDURE (s: Script) Do;

      VAR e, f, g: OpElem;
   BEGIN
      e := s.list; f := NIL;
      REPEAT
         Neutralize(e.st); e.op.Do;
         g := e.next; e.next := f; f := e; e := g
      UNTIL e = NIL;
      s.list := f
   END Do;
   (* StdSequencer *)


   PROCEDURE (s: StdSequencer) Handle (VAR msg: ANYREC);

   (* send message to all windows attached to s *)
      VAR w: Window;
   BEGIN
      IF s.trapEra # Kernel.trapCount THEN Reset(s) END;
      WITH msg: Models.Message DO
         IF msg IS Models.UpdateMsg THEN
            Properties.IncEra;
            IF s.entryLevel = 0 THEN
               (* updates in dominated model bypassed the sequencer *)
               Reset(s);   (* panic reset: clear sequencer *)
               INC(s.modLevel)   (* but leave dirty *)
            END
         END;
         w := dir.First();
         WHILE w # NIL DO
            IF w.seq = s THEN w.BroadcastModelMsg(msg) END;
            w := dir.Next(w)
         END
      | msg: Views.Message DO
         w := dir.First();
         WHILE w # NIL DO
            IF w.seq = s THEN w.BroadcastViewMsg(msg) END;
            w := dir.Next(w)
         END
      ELSE
      END
   END Handle;
   PROCEDURE (s: StdSequencer) Dirty (): BOOLEAN;


   BEGIN
      RETURN s.modLevel > 0
   END Dirty;
   PROCEDURE (s: StdSequencer) SetDirty (dirty: BOOLEAN);

   BEGIN
      IF dirty THEN INC(s.modLevel) ELSE s.modLevel := 0 END
   END SetDirty;
   PROCEDURE (s: StdSequencer) LastOp (st: Stores.Store): Stores.Operation;

   BEGIN
      ASSERT(st # NIL, 20);
      IF s.lastSt = st THEN RETURN s.lastOp ELSE RETURN NIL END
   END LastOp;
   PROCEDURE (s: StdSequencer) BeginScript (IN name: Stores.OpName; VAR script: Stores.Operation);


      VAR sop: Script;
   BEGIN
      IF s.trapEra # Kernel.trapCount THEN Reset(s) END;
      INC(s.nestLevel);
      IF (s.nestLevel = 1) & (s.invisibleLevel = 0) & (s.transparentLevel = 0) & (s.notRecordedLevel = 0) THEN
         INC(s.modLevel)
      END;
      s.lastSt := NIL; s.lastOp := NIL;
      NEW(sop); sop.up := s.script; sop.list := NIL; sop.level := s.nestLevel; sop.name := name;
      s.script := sop;
      script := sop
   END BeginScript;
   PROCEDURE (s: StdSequencer) Do (st: Stores.Store; IN name: Stores.OpName; op: Stores.Operation);

      VAR e: OpElem;
   BEGIN
      ASSERT(st # NIL, 20); ASSERT(op # NIL, 21);
      IF s.trapEra # Kernel.trapCount THEN Reset(s) END;
      Do(s, st, op);
      IF s.noUndo THEN   (* cannot undo: unbalanced BeginModification pending *)
         s.lastSt := NIL; s.lastOp := NIL
      ELSIF (s.entryLevel = 0)   (* don't record when called from within op.Do *)
      & AffectsDoc(s, st) THEN   (* don't record when Do affected child window only *)
         s.lastSt := st; s.lastOp := op;
         s.redo := NIL;   (* clear redo stack *)
         IF s.script # NIL THEN
            Prepend(s.script, st, name, op)
         ELSE
            IF (s.invisibleLevel = 0) & (s.transparentLevel = 0) & (s.notRecordedLevel = 0) THEN INC(s.modLevel) END;
            NEW(e); e.st := st; e.op := op; e.name := name;
            e.invisible := s.invisibleLevel > 0; e.transparent := s.transparentLevel > 0;
            IF (s.notRecordedLevel=0) THEN Push(s.undo, e) END
         END
      END
   END Do;
   PROCEDURE (s: StdSequencer) Bunch (st: Stores.Store);

      VAR lastOp: Stores.Operation;
   BEGIN
      IF s.trapEra # Kernel.trapCount THEN Reset(s) END;
      ASSERT(st # NIL, 20); ASSERT(st = s.lastSt, 21);
      lastOp := s.lastOp;
      Do(s, st, lastOp);
      IF s.noUndo THEN
         s.lastSt := NIL; s.lastOp := NIL
      ELSIF (s.entryLevel = 0)   (* don't record when called from within op.Do *)
      & AffectsDoc(s, st) THEN   (* don't record when Do affected child window only *)
         s.lastSt := st; s.lastOp := lastOp
      END
   END Bunch;
   PROCEDURE (s: StdSequencer) EndScript (script: Stores.Operation);

      VAR e: OpElem;
   BEGIN
      IF s.trapEra # Kernel.trapCount THEN Reset(s) END;
      ASSERT(script # NIL, 20); ASSERT(s.script = script, 21);
      WITH script: Script DO
         ASSERT(s.nestLevel = script.level, 22);
         s.script := script.up;
         IF s.entryLevel = 0 THEN   (* don't record when called from within op.Do *)
            IF script.list # NIL THEN
               IF s.script # NIL THEN
                  Prepend(s.script, NIL, script.name, script)
               ELSE   (* outermost scripting level *)
                  s.redo := NIL;   (* clear redo stack *)
                  IF ~s.noUndo THEN
                     NEW(e); e.st := NIL; e.op := script; e.name := script.name;
                     e.invisible := s.invisibleLevel > 0; e.transparent := s.transparentLevel > 0;
                     IF s.notRecordedLevel=0 THEN Push(s.undo, e) END
                  END;
                  s.lastSt := NIL; s.lastOp := NIL
               END
            ELSE
               IF (s.script = NIL) & (s.modLevel > 0) & (s.invisibleLevel = 0) & (s.transparentLevel = 0) THEN
                  DEC(s.modLevel)
               END
            END
         END
      END;
      DEC(s.nestLevel);
      IF s.nestLevel = 0 THEN ASSERT(s.script = NIL, 22); s.noUndo := FALSE END
   END EndScript;
   PROCEDURE (s: StdSequencer) StopBunching;

   BEGIN
      s.lastSt := NIL; s.lastOp := NIL
   END StopBunching;
   PROCEDURE (s: StdSequencer) BeginModification (type: INTEGER; st: Stores.Store);

   BEGIN
      IF s.trapEra # Kernel.trapCount THEN Reset(s) END;
      IF s.nestLevel < LEN(s.modStack) THEN s.modStack[s.nestLevel].store := st; s.modStack[s.nestLevel].type := type END;
      INC(s.nestLevel);
      IF type = Sequencers.notUndoable THEN
         INC(s.modLevel);   (* unbalanced! *)
         s.noUndo := TRUE; s.undo := NIL; s.redo := NIL;
         s.lastSt := NIL; s.lastOp := NIL;
         INC(s.entryLevel)   (* virtual entry of modification "operation" *)
      ELSIF type = Sequencers.invisible THEN
         INC(s.invisibleLevel)
      ELSIF type = Sequencers.clean THEN
         INC(s.transparentLevel)
      ELSIF type = notRecorded THEN
         INC(s.notRecordedLevel)
      END
   END BeginModification;
   PROCEDURE (s: StdSequencer) EndModification (type: INTEGER; st: Stores.Store);

   BEGIN
      IF s.trapEra # Kernel.trapCount THEN Reset(s) END;
      ASSERT(s.nestLevel > 0, 20);
      IF s.nestLevel <= LEN(s.modStack) THEN
         ASSERT((s.modStack[s.nestLevel - 1].store = st) & (s.modStack[s.nestLevel - 1].type = type), 21)
      END;
      DEC(s.nestLevel);
      IF type = Sequencers.notUndoable THEN
         DEC(s.entryLevel)
      ELSIF type = Sequencers.invisible THEN
         DEC(s.invisibleLevel)
      ELSIF type = Sequencers.clean THEN
         DEC(s.transparentLevel)
      ELSIF type = notRecorded THEN
         DEC(s.notRecordedLevel)
      END;
      IF s.nestLevel = 0 THEN ASSERT(s.script = NIL, 22); s.noUndo := FALSE END
   END EndModification;
   PROCEDURE (s: StdSequencer) CanUndo (): BOOLEAN;

      VAR op: OpElem;
   BEGIN
      IF s.trapEra # Kernel.trapCount THEN Reset(s) END;
      op := s.undo;
      WHILE (op # NIL) & op.invisible DO op := op.next END;
      RETURN op # NIL
   END CanUndo;
   PROCEDURE (s: StdSequencer) CanRedo (): BOOLEAN;

      VAR op: OpElem;
   BEGIN
      IF s.trapEra # Kernel.trapCount THEN Reset(s) END;
      op := s.redo;
      WHILE (op # NIL) & op.invisible DO op := op.next END;
      RETURN op # NIL
   END CanRedo;
   PROCEDURE (s: StdSequencer) GetUndoName (VAR name: Stores.OpName);

      VAR op: OpElem;
   BEGIN
      IF s.trapEra # Kernel.trapCount THEN Reset(s) END;
      op := s.undo;
      WHILE (op # NIL) & op.invisible DO op := op.next END;
      IF op # NIL THEN name := op.name$ ELSE name[0] := 0X END
   END GetUndoName;
   PROCEDURE (s: StdSequencer) GetRedoName (VAR name: Stores.OpName);

      VAR op: OpElem;
   BEGIN
      IF s.trapEra # Kernel.trapCount THEN Reset(s) END;
      op := s.redo;
      WHILE (op # NIL) & op.invisible DO op := op.next END;
      IF op # NIL THEN name := op.name$ ELSE name[0] := 0X END
   END GetRedoName;
   PROCEDURE (s: StdSequencer) Undo;

      VAR e: OpElem;
   BEGIN
      IF s.trapEra # Kernel.trapCount THEN Reset(s) END;
      IF s.undo # NIL THEN
         REPEAT
            Pop(s.undo, e); Do(s, e.st, e.op); Push(s.redo, e)
         UNTIL ~e.invisible OR (s.undo = NIL);
         IF ~e.transparent THEN
            IF s.modLevel > 0 THEN DEC(s.modLevel) END
         END
      END
   END Undo;
   PROCEDURE (s: StdSequencer) Redo;

      VAR e: OpElem;
   BEGIN
      IF s.trapEra # Kernel.trapCount THEN Reset(s) END;
      IF s.redo # NIL THEN
         Pop(s.redo, e); Do(s, e.st, e.op); Push(s.undo, e);
         WHILE (s.redo # NIL) & s.redo.invisible DO
            Pop(s.redo, e); Do(s, e.st, e.op); Push(s.undo, e)
         END;
         IF ~e.transparent THEN
            INC(s.modLevel)
         END
      END
   END Redo;
   (* Forwarder *)


   PROCEDURE (f: Forwarder) Forward (target: BOOLEAN; VAR msg: Controllers.Message);

      VAR w: Window;
   BEGIN
      w := dir.Focus(target);
      IF w # NIL THEN w.ForwardCtrlMsg(msg) END
   END Forward;
   PROCEDURE (f: Forwarder) Transfer (VAR msg: Controllers.TransferMessage);

      VAR w: Window; h: Views.Frame; p: Ports.Port; sx, sy, tx, ty, pw, ph: INTEGER;
   BEGIN
      h := msg.source; p := h.rider.Base();
      (* (msg.x, msg.y) is point in local coordinates of source frame *)
      sx := (msg.x + h.gx) DIV h.unit;
      sy := (msg.y + h.gy) DIV h.unit;
      (* (sx, sy) is point in global coordinates of source port *)
      dir.GetThisWindow(p, sx, sy, tx, ty, w);
      IF w # NIL THEN
         (* (tx, ty) is point in global coordinates of target port *)
         w.port.GetSize(pw, ph);
         msg.x := tx * w.port.unit;
         msg.y := ty * w.port.unit;
         (* (msg.x, msg.y) is point in coordinates of target window *)
         w.ForwardCtrlMsg(msg)
      END
   END Transfer;
   (** miscellaneous **)


   PROCEDURE SetDir* (d: Directory);

   BEGIN
      ASSERT(d # NIL, 20);
      IF stdDir = NIL THEN stdDir := d END;
      dir := d
   END SetDir;
   PROCEDURE SelectBySpec* (loc: Files.Locator; name: Files.Name; conv: Converters.Converter; VAR done: BOOLEAN);

      VAR w: Window;
   BEGIN
      Kernel.MakeFileName(name, "");
      w := dir.First();
      WHILE (w # NIL) & ((loc = NIL) OR (w.loc = NIL) OR (loc.res = 77) OR(w.loc.res = 77) OR
                               (name = "") OR (w.name = "") OR
                              ~Files.dir.SameFile(loc, name, w.loc, w.name) OR (w.conv # conv)) DO
         w := dir.Next(w)
      END;
      IF w # NIL THEN dir.Select(w, lazy); done := TRUE ELSE done := FALSE END
   END SelectBySpec;
   PROCEDURE SelectByTitle* (v: Views.View; flags: SET; title: Views.Title; VAR done: BOOLEAN);

      VAR w: Window; t: Views.Title; n1, n2: ARRAY 64 OF CHAR;
   BEGIN
      done := FALSE;
      IF v # NIL THEN
         IF v IS Documents.Document THEN v := v(Documents.Document).ThisView() END;
         Services.GetTypeName(v, n1)
      ELSE n1 := ""
      END;
      w := dir.First();
      WHILE w # NIL DO
         IF ((w.flags / flags) * {isAux, isTool} = {}) & ~(allowDuplicates IN w.flags) THEN
            w.GetTitle(t);
            IF t = title THEN
               Services.GetTypeName(w.doc.ThisView(), n2);
               IF (n1 = "") OR (n1 = n2) THEN dir.Select(w, lazy); done := TRUE; RETURN END
            END
         END;
         w := dir.Next(w)
      END
   END SelectByTitle;
   PROCEDURE (h: Hook) Omnicast (VAR msg: ANYREC);


      VAR w: Window;
   BEGIN
      w := dir.First();
      WHILE w # NIL DO
         IF ~w.sub THEN w.seq.Handle(msg) END;
         w := dir.Next(w)
      END
   END Omnicast;
   PROCEDURE RestoreSequencer (seq: Sequencers.Sequencer);

      VAR w: Window;
   BEGIN
      w := dir.First();
      WHILE w # NIL DO
         ASSERT(w.frame # NIL, 100);
         IF (seq = NIL) OR (w.seq = seq) THEN
            w.Update   (* causes redrawing of BlackBox region *)
         END;
         w := dir.Next(w)
      END
   END RestoreSequencer;
   PROCEDURE (h: Hook) RestoreDomain (d: Stores.Domain);

      VAR seq: ANYPTR;
   BEGIN
      IF d = NIL THEN
         RestoreSequencer(NIL)
      ELSE
         seq := d.GetSequencer();
         IF seq # NIL THEN
            RestoreSequencer (seq(Sequencers.Sequencer))
         END
      END
   END RestoreDomain;
   (* SequencerDirectory *)


   
   PROCEDURE (d: SequencerDirectory) New (): Sequencers.Sequencer;
   BEGIN
      RETURN dir.NewSequencer()
   END New;
   (** CheckAction **)

   
   PROCEDURE (a: CheckAction) Do;
      VAR w: Window; s: StdSequencer;
   BEGIN
      Services.DoLater(a.wait, Services.resolution);
      w := dir.First();
      WHILE w # NIL DO
         s := w.seq(StdSequencer);
         IF s.trapEra # Kernel.trapCount THEN Reset(s) END;
         ASSERT(s.nestLevel = 0, 100);
         (* unbalanced calls of Views.BeginModification/EndModification or Views.BeginScript/EndScript *)
         w := dir.Next(w)
      END
   END Do;
   
   PROCEDURE (a: WaitAction) Do;
   BEGIN
      Services.DoLater(a.check, Services.immediately)
   END Do;
   PROCEDURE (n: LangNotifier) Notify;


      VAR w: Window; pw, ph: INTEGER;
   BEGIN
      w := dir.First();
      WHILE w # NIL DO
         w.port.GetSize(pw, ph);
         w.Restore(0, 0, pw, ph);
         w.RefreshTitle;
         w := dir.Next(w)
      END
   END Notify;
   
   PROCEDURE Init;
      VAR f: Forwarder; r: Reducer; sdir: SequencerDirectory;
         a: CheckAction; w: WaitAction; h: Hook;ln: LangNotifier;
   BEGIN
      NEW(sdir); Sequencers.SetDir(sdir);
      NEW(h); Views.SetMsgHook(h);
      NEW(f); Controllers.Register(f);
      NEW(r); Kernel.InstallReducer(r);
      NEW(a); NEW(w); a.wait := w; w.check := a; Services.DoLater(a, Services.immediately);
      NEW(ln); Dialog.RegisterLangNotifier(ln)
   END Init;
BEGIN

   Init
END Windows.