cmvbt/src/HoverVBT.m3


 Copyright 1996-2000, Critical Mass, Inc. All Rights Reserved. 
 See file COPYRIGHT-CMASS for details. 

MODULE HoverVBT;

IMPORT Filter, Point, Rect, Thread, Time, Trestle, VBT, Word;

REVEAL
  T = Filter.T BRANDED OBJECT
        state     : State    := State.Looking;
        timer     : Timer    := NIL;
      OVERRIDES
        init     := Init;
        position := Position;
      END;

TYPE
  State = {
    Looking,  (* => haven't noticed mouse over a registered vbt *)
    Hovering, (* => we have noticed one, waiting for the timer to expire *)
    Delivered (* => made the callback, now waiting for the mouse to leave *)
  };

TYPE
  Child = REF RECORD
    v     : VBT.T;
    cb    : CallBack;
    ref   : REFANY;
    delay : Time.T;
    next  : Child;
  END;

VAR
  mu := NEW (MUTEX);
  registered : Child := NIL;

PROCEDURE New (ch: VBT.T): T =
  BEGIN
    RETURN NEW(T).init(ch);
  END New;

PROCEDURE Init (v: T;  ch: VBT.T): Filter.T =
  BEGIN
    EVAL Filter.T.init (v, ch);
    VBT.SetCage (v, VBT.GoneCage);
    RETURN v;
  END Init;

PROCEDURE Register (v: VBT.T;  delay: Time.T;  cb: CallBack;  ref: REFANY) =
  VAR x: Child;
  BEGIN
    LOCK mu DO
      x := registered;
      WHILE (x # NIL) DO
        IF (x.v = v) THEN
          x.delay := delay;
          x.cb := cb;
          x.ref := ref;
          RETURN;
        END;
        x := x.next;
      END;
      registered := NEW (Child, v := v, delay := delay, cb := cb,
                         ref := ref, next := registered);
    END;
  END Register;

PROCEDURE Position (v: T;  READONLY cd: VBT.PositionRec) =
  VAR x: Child;  p, hDelta: Point.T;
  BEGIN
    CASE v.state OF
    | State.Looking =>
        IF cd.cp.gone THEN
          VBT.SetCage (v, VBT.GoneCage);
          RETURN;
        END;

        p := Trestle.ScreenOf (v, cd.cp.pt).q;
        hDelta := Point.Sub (cd.cp.pt, p);

        LOCK mu DO
          x := registered;
          LOOP
            IF (x = NIL) THEN
              (* no matches, still looking *)
              VBT.SetCage (v, VBT.CageFromPosition (cd.cp));
              RETURN;
            END;
            IF HoverParent (x.v) = v THEN
              VAR
                chDom := VBT.Domain (x.v);
                nw    := Rect.NorthWest (chDom);
                qnw   := Trestle.ScreenOf (x.v, nw).q;
                delta := Point.Sub (qnw, nw);
                qDom  := Rect.Move (chDom, delta);
              BEGIN
                IF Rect.Member (p, qDom) THEN
                  v.state  := State.Hovering;
                  StartTimer (v, x);
                  VBT.SetCage (v, VBT.CageFromRect(Rect.Move(qDom, hDelta), cd.cp));
                  RETURN;
                END;
              END;
            END;
            x := x.next;
          END;
        END;

    | State.Hovering =>
        (* oops, the mouse left the target before its timeout... *)
        AbortTimer (v);
        v.state  := State.Looking;
        VBT.SetCage (v, VBT.GoneCage);

    | State.Delivered =>
        (* the mouse finally left the target vbt *)
        v.state  := State.Looking;
        VBT.SetCage (v, VBT.GoneCage);
    END;
  END Position;

PROCEDURE HoverParent (v: VBT.T): T =
  (* Find the nearest ancestor of "v" that is a hover vbt *)
  BEGIN
    LOOP
      TYPECASE v OF
      | T(t) => RETURN t;
      ELSE v := VBT.Parent (v);
      END;
    END;
  END HoverParent;
------------------------------------------------------- timeout thread ---

TYPE
  Timer = Thread.Closure OBJECT
    self      : Thread.T := NIL;
    hover     : T        := NIL;
    target    : Child    := NIL;    (* the current hover target *)
    start     : Time.T   := 0.0d0;  (* when we started the delay timer *)
    start_uid : INTEGER  := 0;      (* the "target"'s UID *)
    uid       : INTEGER  := 0;      (* UID for each timer activation *)
    mu        : MUTEX    := NIL;
    burp      : Thread.Condition := NIL;
  OVERRIDES
    apply := TimerRoot;
  END;

PROCEDURE StartTimer (v: T;  x: Child) =
  VAR t := v.timer;
  BEGIN
    IF (t = NIL) THEN
      t := NEW (Timer);  v.timer := t;
      t.hover := v;
      t.mu    := NEW (MUTEX);
      t.burp  := NEW (Thread.Condition);
      t.self  := Thread.Fork (t);
    END;

    LOCK t.mu DO
      t.uid       := Word.Plus (t.uid, 1);
      t.target    := x;
      t.start     := Time.Now ();
      t.start_uid := t.uid;
    END;
    Thread.Signal (t.burp);
  END StartTimer;

PROCEDURE AbortTimer (v: T) =
  VAR t := v.timer;
  BEGIN
    LOCK t.mu DO
      t.uid    := Word.Plus (t.uid, 1);
      t.target := NIL;
    END;
    Thread.Alert (t.self);
  END AbortTimer;

PROCEDURE TimerRoot (t: Timer): REFANY =
  VAR
    x     : Child;
    seq   : INTEGER;
    pause : Time.T;
    now   : Time.T;
  BEGIN
    LOOP
      (* wait for a timer to be scheduled *)
      LOCK t.mu DO
        WHILE (t.target = NIL) OR (t.start_uid # t.uid) DO
          Thread.Wait (t.mu, t.burp);
        END;
        x   := t.target;
        seq := t.start_uid;
        now := Time.Now ();
        pause := (t.start + x.delay) - now;
      END;

      IF (pause > 0.0d0) THEN
        (* wait... *)
        TRY Thread.AlertPause (pause);
        EXCEPT Thread.Alerted => x := NIL; (* aborted *)
        END;
      END;

      IF (x # NIL) THEN
        LOCK t.mu DO
          IF (t.uid = seq)
            THEN t.target := NIL;    (* this timeout is still current. *)
            ELSE x := NIL;           (* darn, this timeout was canceled *)
          END;
        END;

        IF x # NIL THEN MakeCallback (t, x); END;
      END;

    END;
  END TimerRoot;

PROCEDURE MakeCallback (t: Timer;  x: Child) =
  CONST Here = VBT.CursorPosition { Point.Origin, 0, FALSE, FALSE };
  VAR
    dom, cDom : Rect.T;
    nw, qnw   : Point.T;
    cnw, cqnw : Point.T;
    d0, d1    : Point.T;
  BEGIN
    (* let the hover vbt know that this event was delivered *)
    LOCK VBT.mu DO
      dom   := VBT.Domain (t.hover);
      nw    := Rect.NorthWest (dom);
      qnw   := Trestle.ScreenOf (t.hover, nw).q;
      d0    := Point.Sub (nw, qnw);

      cDom  := VBT.Domain (x.v);
      cnw   := Rect.NorthWest (cDom);
      cqnw  := Trestle.ScreenOf (x.v, cnw).q;
      d1    := Point.Sub (cqnw, cnw);

      (* map the child domain to screen coordinates *)
      cDom  := Rect.Move (cDom, d1);

      (* and then map it back to the hover vbt's coordinates *)
      cDom  := Rect.Move (cDom, d0);

      (* finally, reset the hover VBT's cage *)
      t.hover.state := State.Delivered;
      VBT.SetCage (t.hover, VBT.CageFromRect (cDom, Here));
    END;

    (* and deliver it... *)
    x.cb (x.v, x.ref);
  END MakeCallback;

BEGIN
END HoverVBT.