-- File: WalnutMsgSetDisplayerImpl.mesa
-- Contents: Implementation of the WalnutMsg Editor windows.
-- Status: mostly here functionally, but not yet completed.
-- Last Edited by: Willie-Sue, December 9, 1982 12:29 pm

-- Last edit by:
-- Rick on: April 29, 1982 7:02 pm
-- Donahue, July 27, 1983 2:22 pm
-- Willie-Sue on: September 21, 1983 4:48 pm

DIRECTORY
Atom USING [GetPropFromList],
Buttons USING [ButtonProc, ReLabel, SetDisplayStyle],
DB USING [GetF, GetName, GetP],
Menus USING [AppendMenuEntry, CreateMenu, Menu, MenuProc],
Rope,
UserProfile USING [Boolean],
VFonts USING [ StringWidth, CharWidth],
ViewerClasses USING [Column, Viewer],
ViewerEvents USING [EventProc, EventRegistration, RegisterEventProc, UnRegisterEventProc],
ViewerLocks USING [CallUnderWriteLock],
ViewerOps USING [AddProp, ComputeColumn, CreateViewer, DestroyViewer, FetchProp,
   GrowViewer, MoveViewer, OpenIcon, PaintViewer, SetMenu],
WalnutDB USING [Entity, Msg, MsgSet, Relship,
     activeMsgSet, deletedMsgSet, mCategory, mCategoryIs, mCategoryOf,
     mHasBeenReadIs, mSubjectIs, mTOCEntryIs,
     AcquireDBLock, AddMsgToMsgSet, DeclareMsg, DeclareMsgSet, EqEntities,
     MGetP, Null, RelationSubsetList, RemoveMsgFromMsgSet,
     SetMsgHasBeenRead, V2B, V2E, V2S],
WalnutLog USING [MsgRec, LogAddMsg, LogMsgHasBeenRead, LogRemoveMsg],
WalnutDisplayerOps,
WalnutMsgOps USING [MsgSetFieldHandle, MsgSetFieldObject,
     DisplayMsgFromMsgSet, MsgCategories],
WalnutPrintOps USING [MsgSetPrintProc, PrintMsgList],
WalnutViewer USING [AnotherButton, CreateMenuEntry, FirstButton],
WalnutWindow USING [MsgSetButton,
     initialActiveIconic, initialActiveOpen, initialActiveRight, personalMailDB,
     readOnlyAccess, msgSetIcon, walnutQueue,
     FindMSBForMsgSet, FindMSViewer, GetSelectedMsgSets, Report, ReportRope,
     RetrieveNewMail];

WalnutMsgSetDisplayerImpl: CEDAR PROGRAM
IMPORTS
Atom, Rope, UserProfile,
WalnutDB, WalnutLog,
WalnutMsgOps, WalnutPrintOps, WalnutViewer, WalnutWindow,
Buttons, Menus, ViewerEvents, ViewerLocks, ViewerOps, VFonts
EXPORTS
WalnutDisplayerOps, WalnutMsgOps =

BEGIN OPEN WalnutDB, WalnutWindow, WalnutMsgOps;

Viewer: TYPE = ViewerClasses.Viewer;
ROPE: TYPE = Rope.ROPE;

displayerMenu: PUBLIC Menus.Menu = Menus.CreateMenu[];
activeMenu: PUBLIC Menus.Menu = Menus.CreateMenu[];
deletedMenu: PUBLIC Menus.Menu = Menus.CreateMenu[];
buildingMenu: PUBLIC Menus.Menu = Menus.CreateMenu[];
readOnlyMenu: PUBLIC Menus.Menu = Menus.CreateMenu[];

msgSetName: PUBLIC ROPE;
destroyedMsg: ROPE = "Selected msg viewer has been destroyed";

blankWidth: INT← VFonts.CharWidth[' ]; -- in default font
blanks: ROPE← " "; -- lotsa blanks

BuildDisplayerMenu: PROC =
BEGIN
Menus.AppendMenuEntry[displayerMenu,
  WalnutViewer.CreateMenuEntry[walnutQueue, "Categories", CategoriesProc]];
Menus.AppendMenuEntry[displayerMenu,
  WalnutViewer.CreateMenuEntry[walnutQueue, "MoveTo", MoveToProc]];
Menus.AppendMenuEntry[displayerMenu,
  WalnutViewer.CreateMenuEntry[walnutQueue, "Display", DisplayProc]];
Menus.AppendMenuEntry[displayerMenu,
  WalnutViewer.CreateMenuEntry[walnutQueue, "Delete", DeleteProc]];
Menus.AppendMenuEntry[displayerMenu,
  WalnutViewer.CreateMenuEntry[walnutQueue, "AddTo", AddProc]];
Menus.AppendMenuEntry[displayerMenu,
  WalnutViewer.CreateMenuEntry[walnutQueue, "Print",
    WalnutPrintOps.MsgSetPrintProc]];
Menus.AppendMenuEntry[displayerMenu,
  WalnutViewer.CreateMenuEntry[walnutQueue, "PrintSelected", PrintSelectedProc]];

Menus.AppendMenuEntry[activeMenu,
  WalnutViewer.CreateMenuEntry[walnutQueue, "Categories", CategoriesProc]];
Menus.AppendMenuEntry[activeMenu,
  WalnutViewer.CreateMenuEntry[walnutQueue, "MoveTo", MoveToProc]];
Menus.AppendMenuEntry[activeMenu,
  WalnutViewer.CreateMenuEntry[walnutQueue, "Display", DisplayProc]];
Menus.AppendMenuEntry[activeMenu,
  WalnutViewer.CreateMenuEntry[walnutQueue, "Delete", DeleteProc]];
Menus.AppendMenuEntry[activeMenu,
  WalnutViewer.CreateMenuEntry[walnutQueue, "AddTo", AddProc]];
Menus.AppendMenuEntry[activeMenu,
  WalnutViewer.CreateMenuEntry[walnutQueue, "NewMail", NewMailProc]];
Menus.AppendMenuEntry[activeMenu,
  WalnutViewer.CreateMenuEntry[walnutQueue, "Print",
    WalnutPrintOps.MsgSetPrintProc]];
Menus.AppendMenuEntry[activeMenu,
  WalnutViewer.CreateMenuEntry[walnutQueue, "PrintSelected", PrintSelectedProc]];

Menus.AppendMenuEntry[deletedMenu,
  WalnutViewer.CreateMenuEntry[walnutQueue, "Categories", CategoriesProc]];
Menus.AppendMenuEntry[deletedMenu,
  WalnutViewer.CreateMenuEntry[walnutQueue, "MoveTo", MoveToProc]];
Menus.AppendMenuEntry[deletedMenu,
  WalnutViewer.CreateMenuEntry[walnutQueue, "Display", DisplayProc]];
Menus.AppendMenuEntry[deletedMenu,
  WalnutViewer.CreateMenuEntry[walnutQueue, "Print",
    WalnutPrintOps.MsgSetPrintProc]];
Menus.AppendMenuEntry[deletedMenu,
  WalnutViewer.CreateMenuEntry[walnutQueue, "PrintSelected", PrintSelectedProc]];
  
Menus.AppendMenuEntry[readOnlyMenu,
  WalnutViewer.CreateMenuEntry[walnutQueue, "Categories", CategoriesProc]];
Menus.AppendMenuEntry[readOnlyMenu,
  WalnutViewer.CreateMenuEntry[walnutQueue, "Display", DisplayProc]];
Menus.AppendMenuEntry[readOnlyMenu,
  WalnutViewer.CreateMenuEntry[walnutQueue, "Print",
    WalnutPrintOps.MsgSetPrintProc]];
Menus.AppendMenuEntry[readOnlyMenu,
  WalnutViewer.CreateMenuEntry[walnutQueue, "PrintSelected", PrintSelectedProc]];
END;

-- * * * * * * * * * * * * * * * * * * * * * *
MoveToProc: Menus.MenuProc =
BEGIN
 viewer: Viewer = NARROW[parent];
 selected: Viewer = NARROW[ViewerOps.FetchProp[viewer, $selectedMsg]];
IF selected = NIL THEN { Report[" No selected msg to be moved"]; RETURN};
IF selected.destroyed THEN Report[destroyedMsg]
ELSE
  { ra: REF ANY = NARROW[ViewerOps.FetchProp[viewer, $WalnutEntity]];
  thisMsgSet: MsgSet = V2E[ra];
  thisName: ROPE← MSNameFromViewer[viewer];
  mfh: MsgSetFieldHandle = NARROW[ViewerOps.FetchProp[selected, $mfH]];
  msgSetList: LIST OF MsgSetButton = GetSelectedMsgSets[];
  first: BOOLTRUE;
  moveToSelf: BOOLFALSE;
IF msgSetList = NIL THEN
  { Report[" No selected MsgSets to Move msg to"]; RETURN};

IF ~mfh.hasBeenRead THEN MarkMsgAsRead[mfh, selected];
FOR msL: LIST OF MsgSetButton ← msgSetList, msL.rest UNTIL msL=NIL DO
   IF thisName.Equal[msL.first.selector.name, FALSE] THEN moveToSelf ← TRUE
ENDLOOP;
  ReportDisposition[mfh.msg]; ReportRope["Moved to:"];
FOR msL: LIST OF MsgSetButton← msgSetList, msL.rest UNTIL msL=NIL DO
IF first THEN first ← FALSE ELSE ReportRope[","];
  ReportRope[" "]; ReportRope[msL.first.selector.name];
ENDLOOP;
  ReportRope["\n"];
IF ~moveToSelf THEN RemoveFromDisplayedMsgSet[viewer, selected, mfh.msg]
   ELSE [] ← AdvanceSelection[mfh];
IF mouseButton#red THEN DisplaySelectedMsg[viewer, shift];
-- do ALL logging first
FOR msL: LIST OF MsgSetButton ← msgSetList, msL.rest UNTIL msL=NIL DO
  msName: ROPE;
   IF NOT thisName.Equal[msName← msL.first.selector.name, FALSE] THEN
    WalnutLog.LogAddMsg[mfh.msgName, msName];
ENDLOOP;
IF NOT moveToSelf THEN
    WalnutLog.LogRemoveMsg[mfh.msgName, thisName];

-- now change the database and the display
FOR msL: LIST OF MsgSetButton ← msgSetList, msL.rest UNTIL msL=NIL DO
   IF NOT EqEntities[msL.first.msgSet, thisMsgSet] THEN
    { rel: Relship; existed: BOOL;
    [rel, existed]← WalnutDB.AddMsgToMsgSet[msg: mfh.msg, msgSet: msL.first.msgSet];
    IF ~existed THEN AddToMsgSetDisplayer[mfh, msL.first.msgSet, rel]
    };
ENDLOOP;
IF NOT moveToSelf THEN
   []← WalnutDB.RemoveMsgFromMsgSet[mfh.msg, thisMsgSet, mfh.relship]
  };
END;

NewMailProc: Menus.MenuProc =
BEGIN
 v: Viewer = NARROW[parent];
 oldM: Menus.Menu = v.menu;
 ViewerOps.SetMenu[v, buildingMenu];
BEGIN ENABLE UNWIND => {v.menu ← oldM; ViewerOps.PaintViewer[v, menu]};
  []← WalnutWindow.RetrieveNewMail[];
  v.menu ← oldM;
  ViewerOps.PaintViewer[v, menu];
END;
END;

CategoriesProc: Menus.MenuProc =
BEGIN
 msViewer: Viewer← NARROW[parent];
 selected: Viewer← NARROW[ViewerOps.FetchProp[msViewer, $selectedMsg]];
 mfh: MsgSetFieldHandle;
IF selected = NIL THEN {Report[" No selected msg"]; RETURN};
IF selected.destroyed THEN {Report[destroyedMsg]; RETURN};
 mfh← NARROW[ViewerOps.FetchProp[selected, $mfH]];
 MsgCategories[mfh.msg];
END;

DeleteProc: Menus.MenuProc =
BEGIN
 msViewer: Viewer = NARROW[parent];
 msName: ROPE← MSNameFromViewer[msViewer];
 selected: Viewer = NARROW[ViewerOps.FetchProp[msViewer, $selectedMsg]];
IF selected = NIL THEN {Report[" No selected msg to be deleted"]; RETURN};
IF selected.destroyed THEN {Report[destroyedMsg]; RETURN}
ELSE
  { ra: REF ANY = NARROW[ViewerOps.FetchProp[msViewer, $WalnutEntity]];
  thisMsgSet: MsgSet = V2E[ra];
  mfh: MsgSetFieldHandle;
  newRel: Relship;
IF msName.Equal["Deleted", FALSE] THEN RETURN;  -- already deleted
  mfh← NARROW[ViewerOps.FetchProp[selected, $mfH]];
IF ~mfh.hasBeenRead THEN MarkMsgAsRead[mfh, selected];
  RemoveFromDisplayedMsgSet[msViewer, selected, mfh.msg];
  ReportDisposition[mfh.msg];
  ReportRope["Deleted from "]; Report[msName];
IF mouseButton#red THEN DisplaySelectedMsg[msViewer, shift];
  WalnutLog.LogRemoveMsg[mfh.msgName, msName];
  newRel← WalnutDB.RemoveMsgFromMsgSet[mfh.msg, thisMsgSet, mfh.relship];
IF newRel # NIL THEN AddToMsgSetDisplayer[mfh, deletedMsgSet, newRel] };
END;

AddProc: Menus.MenuProc = {
viewer: Viewer = NARROW[parent];
 selected: Viewer = NARROW[ViewerOps.FetchProp[viewer, $selectedMsg]];
IF selected = NIL THEN {Report[" No selected msg to be moved"]; RETURN};
IF selected.destroyed THEN {Report[destroyedMsg]; RETURN}
ELSE
  { ra: REF ANY = NARROW[ViewerOps.FetchProp[viewer, $WalnutEntity]];
  thisMsgSet: MsgSet← V2E[ra];
  mfh: MsgSetFieldHandle = NARROW[ViewerOps.FetchProp[selected, $mfH]];
  msgSetList: LIST OF MsgSetButton = GetSelectedMsgSets[];
  msAddedTo: ROPE;
  msgSetAddedTo: MsgSet;
  thisName: ROPE← MSNameFromViewer[viewer];
  first: BOOLTRUE;
IF msgSetList = NIL THEN
  { Report[" No selected MsgSets to Add msg to"]; RETURN};

IF ~mfh.hasBeenRead THEN MarkMsgAsRead[mfh, selected];
  ReportDisposition[mfh.msg]; ReportRope["Added to:"];
IF mouseButton#red THEN
  { [] ← AdvanceSelection[mfh]; DisplaySelectedMsg[viewer, shift] };
-- do all logging first
FOR msL: LIST OF MsgSetButton ← msgSetList, msL.rest UNTIL msL=NIL DO
   IF NOT thisName.Equal[(msAddedTo← msL.first.selector.name), FALSE] THEN
    { WalnutLog.LogAddMsg[mfh.msgName, msAddedTo];
    IF first THEN first ← FALSE ELSE ReportRope[","];
    ReportRope[" "];
    ReportRope[msAddedTo];
   };
ENDLOOP;
  ReportRope["\n"];

-- now update database and finish updating display
FOR msL: LIST OF MsgSetButton ← msgSetList, msL.rest UNTIL msL=NIL DO
   IF NOT EqEntities[(msgSetAddedTo← msL.first.msgSet), thisMsgSet] THEN
    { rel: Relship; existed: BOOL;
    [rel, existed]← AddMsgToMsgSet[msg: mfh.msg, msgSet: msgSetAddedTo];
    IF ~existed THEN AddToMsgSetDisplayer[mfh, msgSetAddedTo, rel] };
ENDLOOP };
};

AdvanceSelection: PROCEDURE [msfH: MsgSetFieldHandle] RETURNS [v: Viewer] =
{ v ← msfH.next;
IF v = NIL THEN { Report[" No Next message"]; RETURN };
[] ← SelectMsgInMsgSet[v]
};

DisplayProc: Menus.MenuProc =
BEGIN
 viewer: Viewer = NARROW[parent];
 selected: Viewer ← NARROW[ViewerOps.FetchProp[viewer, $selectedMsg]];
 msfH: MsgSetFieldHandle = IF (selected = NIL OR selected.destroyed) THEN NIL
            ELSE NARROW[ViewerOps.FetchProp[selected, $mfH]];
IF mouseButton # red AND msfH # NIL THEN selected ← AdvanceSelection[msfH];
IF selected = NIL THEN {Report[" No selected msg to be displayed"]; RETURN};
IF selected.destroyed THEN {Report[destroyedMsg]; RETURN};
 DisplayOneMsg[selected, shift]
END;

PrintSelectedProc
: Menus.MenuProc =
BEGIN
 viewer: Viewer = NARROW[parent];
 selected: Viewer ← NARROW[ViewerOps.FetchProp[viewer, $selectedMsg]];
 msfH: MsgSetFieldHandle = IF (selected = NIL OR selected.destroyed) THEN NIL
            ELSE NARROW[ViewerOps.FetchProp[selected, $mfH]];
IF selected = NIL THEN {Report[" No selected msg to be printed"]; RETURN};
IF selected.destroyed THEN {Report[destroyedMsg]; RETURN};
 []← WalnutPrintOps.PrintMsgList[LIST[msfH.msg], viewer];
END;

ReportDisposition: PROC[msg: Msg] =
BEGIN
 msgSubject: ROPE← V2S[MGetP[msg, mSubjectIs]];
IF msgSubject.Length[] > 24 THEN msgSubject← Rope.Concat[msgSubject.Substr[0, 21], " ..."];
 ReportRope[Rope.Cat["Msg: ", msgSubject, ": has been "]];
END;

DisplaySelectedMsg: PROC[viewer: Viewer, shift: BOOL] =
BEGIN
selected: Viewer← NARROW[ViewerOps.FetchProp[viewer, $selectedMsg]];
IF selected # NIL THEN DisplayOneMsg[selected, shift];
END;

-- * * * * * * * * * * * * * * * * * * * * * *
-- called from "outside", need to check MsgSetButtons first
BuildMsgSetViewer: PUBLIC PROC[msName: ROPE, msgSet: MsgSet, shift, paint: BOOLFALSE]
  RETURNS[v: Viewer] =
-- if msName is already being displayed, returns that Viewer
BEGIN
 msb: MsgSetButton← FindMSBForMsgSet[msgSet];
IF (v← msb.msViewer) # NIL THEN RETURN;
 v← MSDisplayer[msgSet, msName, shift, FALSE, paint];
 msb.msViewer← v;
END;

BuildListOfMsgsViewer: PUBLIC PROC[mL: LIST OF ROPE, name: ROPE, oldV: Viewer]
RETURNS[v: Viewer] =
-- builds a msgset-like displayer for a list of msgs, using oldV if given
BEGIN OPEN ViewerOps;
 prevMfh, mfh: MsgSetFieldHandle;
 lastButton: Viewer;
DoListOfMsgs: PROC =
BEGIN
FOR mlT: LIST OF ROPE← mL, mlT.rest UNTIL mlT=NIL DO
  msg: Msg← DeclareMsg[mlT.first, OldOnly].msg;
  hasBeenRead: BOOL;
  IF Null[msg] THEN LOOP ELSE hasBeenRead← V2B[DB.GetP[msg, mHasBeenReadIs]];
  [mfh, lastButton]← BuildMsgLineViewer[lastButton, MsgButtonName[msg, hasBeenRead]];
  mfh.msg← msg;
  mfh.hasBeenRead← hasBeenRead;
  mfh.msgName← mlT.first;
  IF prevMfh # NIL THEN prevMfh.next← lastButton;
  prevMfh← mfh;
ENDLOOP;
END;

IF oldV = NIL THEN
  v← ViewerOps.CreateViewer[
  flavor: $Container, paint: TRUE,
  info: [name: name, menu: NIL, icon: msgSetIcon, inhibitDestroy: TRUE]]
--  info: [name: name, menu: NIL, icon: msgQueryIcon, inhibitDestroy: TRUE]]
ELSE IF ViewerOps.FetchProp[oldV, $WalnutQuery] = NIL THEN RETURN
ELSE ClearMSViewer[v← oldV];

 ViewerOps.AddProp[v, $WalnutQuery, mL];  -- save guys to display
 v.newVersion← TRUE; PaintViewer[v, caption];
 lastButton← v;
 WalnutDB.AcquireDBLock[DoListOfMsgs];
 v.newVersion← FALSE; PaintViewer[v, caption];
 v.inhibitDestroy← FALSE;
END;

BuildMsgSetDisplayer: PUBLIC PROC
 [msgSet: MsgSet, name: ROPE, shift: BOOLFALSE, initialActive: BOOLFALSE]
  RETURNS[msV: Viewer] =
 { RETURN[MSDisplayer[msgSet, name, shift, initialActive, TRUE]]};

MSDisplayer: PROC[msgSet: MsgSet, name: ROPE, shift, initialActive, paint: BOOL]
  RETURNS[msV: Viewer] =
BEGIN
 iconic: BOOLFALSE;
 whichSide: ViewerClasses.Column← left;

IF EqEntities[msgSet, activeMsgSet] THEN
  { iconic← WalnutWindow.initialActiveIconic AND WalnutWindow.initialActiveOpen;
IF WalnutWindow.initialActiveRight THEN whichSide← right;
  };

 msV← ViewerOps.CreateViewer[
  flavor: $Container, paint: paint,
  info: [name: Rope.Concat[name, " Messages"], column: whichSide, menu: NIL,
    iconic: iconic, icon: msgSetIcon, inhibitDestroy: TRUE]];

 ViewerOps.AddProp[msV, $Entity, msgSetName.Concat[name]];
 ViewerOps.AddProp[msV, $WalnutEntity, msgSet];
 ViewerOps.AddProp[msV, $IconLabel, name];
IF shift AND paint THEN
  { IF iconic THEN ViewerOps.OpenIcon[msV, shift] ELSE ViewerOps.GrowViewer[msV]};
END;

AutoScrollRef: TYPE = REF AutoScrollObject;
AutoScrollObject: TYPE =
  RECORD[eventReg: ViewerEvents.EventRegistration, firstUnread, last: Viewer];

MsgSetInViewer:
  PUBLIC PROC[msName: ROPE, msgSet: MsgSet, v: Viewer, shift: BOOLFALSE] =
BEGIN OPEN ViewerOps;
firstUnread, last: Viewer;
menu: Menus.Menu←
IF WalnutWindow.readOnlyAccess THEN readOnlyMenu ELSE
IF EqEntities[msgSet, activeMsgSet] THEN
IF WalnutWindow.personalMailDB THEN activeMenu ELSE displayerMenu ELSE
IF EqEntities[msgSet, deletedMsgSet] THEN deletedMenu ELSE displayerMenu;
autoScroll: BOOL← UserProfile.Boolean[key: "Walnut.AutoScrollMsgSets", default: TRUE];
ScrollMsgSet: PROC =
BEGIN
IF autoScroll AND last#NIL THEN
  { IF v.iconic THEN
  { scroll: ViewerEvents.EventRegistration←
    ViewerEvents.RegisterEventProc[AutoScroll, open, v, FALSE]; --after
  ViewerOps.AddProp[v, $autoScroll,
    NEW[AutoScrollObject← [scroll, firstUnread, last]]];
  }
ELSE DoScroll[v, firstUnread, last];
  };
  SetMenu[v, menu];
END;

-- for calls from "outside" - might be changing the msgset being displayed
IF msName # NIL THEN
  { curName: ROPE← MSNameFromViewer[v];
IF msName.Equal[curName, FALSE] THEN
  { IF ViewerOps.FetchProp[v, $lastButton] # NIL THEN RETURN}
ELSE
  { ra: REF ANY← ViewerOps.FetchProp[v, $WalnutEntity];
   oldMsgSet: MsgSet← V2E[ra];
   msb: MsgSetButton← FindMSBForMsgSet[oldMsgSet];
   msb.msViewer← NIL;
  ClearMSViewer[v];
   v.name← Rope.Concat[msName, " Messages"];
   ViewerOps.AddProp[v, $Entity, msgSetName.Concat[msName]];
   ViewerOps.AddProp[v, $WalnutEntity, msgSet];
   ViewerOps.AddProp[v, $IconLabel, msName];
   msb← FindMSBForMsgSet[msgSet];
   msb.msViewer← v;
   };
  };

 []← v.class.scroll[v, thumb, 0];  -- position at beginning
IF ~v.iconic THEN ComputeColumn[v.column, TRUE];
 v.newVersion← TRUE; PaintViewer[v, caption];
 SetMenu[v, buildingMenu];
 [firstUnread, last]← BuildTupleWindowButtons[v, msgSet];

 ViewerLocks.CallUnderWriteLock[ScrollMsgSet, v];
 v.newVersion← FALSE; PaintViewer[v, caption];
 v.inhibitDestroy← FALSE;
END;

DoScroll: PROC[viewer, firstUnread, last: Viewer] =
BEGIN
IF viewer.ch < (last.cy+last.ch) THEN
IF firstUnread = NIL THEN
  { []← viewer.class.scroll[viewer, thumb, 100];
  []← viewer.class.scroll[viewer, down, viewer.ch - 54]
  }
ELSE
  { []← viewer.class.scroll[viewer, thumb, 0];
  []← viewer.class.scroll[viewer, up, firstUnread.cy - 16]
  };
END;

AutoScroll: ViewerEvents.EventProc =
BEGIN
 autoScroll: AutoScrollRef← NARROW[ViewerOps.FetchProp[viewer, $autoScroll]];
 firstUnread: Viewer← autoScroll.firstUnread;
 last: Viewer← autoScroll.last;
 ViewerEvents.UnRegisterEventProc[autoScroll.eventReg, open];  -- once only
 DoScroll[viewer, firstUnread, last];
END;

FixUpMsgSetViewer: PUBLIC PROC[msName: ROPE, v: Viewer] =
BEGIN
 entityName: ROPE;
 msgSet: MsgSet;
 msb: MsgSetButton;

IF (msName.Length[] = 0) OR v.destroyed THEN RETURN;

 entityName← msName.Substr[msgSetName.Length[]];
 msgSet← DeclareMsgSet[entityName, OldOnly].msgSet;
IF msgSet = NIL THEN
  { Report["MsgSet: ", entityName, " doesn't exist; destroying viewer"];
  ViewerOps.DestroyViewer[v];
RETURN
  };
 ClearMSViewer[v];
 ViewerOps.AddProp[v, $WalnutEntity, msgSet];
 MsgSetInViewer[NIL, msgSet, v];
 msb← FindMSBForMsgSet[msgSet];
 msb.msViewer← v;
END;

ClearMSViewer: PROC[v: Viewer] =
BEGIN
-- delete buttons & start again
 child: Viewer;
UNTIL (child← v.child) = NIL DO ViewerOps.DestroyViewer[child, FALSE] ENDLOOP;
 ViewerOps.PaintViewer[v, client];  -- paint cleared viewer
 ViewerOps.AddProp[v, $selectedMsg, NIL];  -- no selected Msg
 ViewerOps.AddProp[v, $lastButton, NIL];
END;

-- * * * * * * * * * * * * * * * * * * * * * *
-- for adding new messages to displayed MsgSet

AddParsedMsgToMSViewer: PUBLIC PROC
  [msg: Msg, msgR: WalnutLog.MsgRec, msViewer: Viewer, rel: Relship] =
BEGIN
 prevMfh, mfhN: MsgSetFieldHandle;
 lastButton: Viewer← NARROW[ViewerOps.FetchProp[msViewer, $lastButton]];
IF lastButton#NIL THEN prevMfh← NARROW[ViewerOps.FetchProp[lastButton, $mfH]]
  ELSE lastButton← msViewer;
 [mfhN, lastButton]←
   BuildMsgLineViewer[lastButton, MsgButtonName[msg, msgR.hasBeenRead]];
 mfhN.posOK← TRUE;
 mfhN.headersPos← msgR.headersPos;
 mfhN.msgLength← msgR.msgLength;
 mfhN.relship← rel;
 mfhN.msg← msg;
 mfhN.msgName← msgR.gvID;
 mfhN.hasBeenRead← msgR.hasBeenRead;
IF prevMfh # NIL THEN prevMfh.next← lastButton;
 ViewerOps.AddProp[msViewer, $lastButton, lastButton];
END;

AddToMsgSetDisplayer:PROC[mfh: MsgSetFieldHandle, msAddedTo: MsgSet, rel: Relship] =
BEGIN
 prevMfh, mfhN: MsgSetFieldHandle;
 lastButton: Viewer;

 msgSetViewer: Viewer← FindMSViewer[msAddedTo];
IF msgSetViewer = NIL THEN RETURN;  -- msgSet not displayed
 lastButton← NARROW[ViewerOps.FetchProp[msgSetViewer, $lastButton]];

IF lastButton#NIL THEN prevMfh← NARROW[ViewerOps.FetchProp[lastButton, $mfH]]
  ELSE lastButton ← msgSetViewer;
 [mfhN, lastButton] ← BuildMsgLineViewer[lastButton, MsgButtonName[mfh.msg, TRUE]];
 mfhN.relship ← rel;
 mfhN.msg ← mfh.msg;

IF mfh.posOK THEN
  { mfhN.posOK ← TRUE;
  mfhN.headersPos ← mfh.headersPos;
  mfhN.msgLength ← mfh.msgLength;
  };

IF prevMfh # NIL THEN prevMfh.next← lastButton;
 ViewerOps.AddProp[msgSetViewer, $lastButton, lastButton];
END;

AddMsgToMsgSetDisplayer: PUBLIC PROC[msg: Msg, msgSet: MsgSet, rel: Relship] =
BEGIN
 prevMfh, mfhN: MsgSetFieldHandle;
 lastButton: Viewer;
 msgSetViewer: Viewer← FindMSViewer[msgSet];
IF msgSetViewer = NIL THEN RETURN;  -- msgSet not displayed
 lastButton← NARROW[ViewerOps.FetchProp[msgSetViewer, $lastButton]];

IF lastButton#NIL THEN prevMfh← NARROW[ViewerOps.FetchProp[lastButton, $mfH]]
  ELSE lastButton← msgSetViewer;
 [mfhN, lastButton]← BuildMsgLineViewer[lastButton, MsgButtonName[msg, TRUE]];
 mfhN.relship← rel;
 mfhN.msg← msg;
IF prevMfh # NIL THEN prevMfh.next← lastButton;
 ViewerOps.AddProp[msgSetViewer, $lastButton, lastButton];
END;

-- called with parent & viewer 'displaying' msg to be deleted
RemoveFromDisplayedMsgSet: PROC[parent, which: Viewer, msg: Msg] =
BEGIN
delta: INTEGER;
mfhT, x: MsgSetFieldHandle;
bt, prev, next: Viewer;
oldName: ROPE;
selected: Viewer← NARROW[ViewerOps.FetchProp[parent, $selectedMsg]];

RemoveFrom: PROC =
BEGIN
  delta← mfhT.next.wy - which.wy;
  ViewerOps.DestroyViewer[which, FALSE];   -- don't paint
  FOR bt← next, mfhT.next UNTIL bt = NIL DO
   mfhT← NARROW[ViewerOps.FetchProp[bt, $mfH]];
   IF mfhT.next = NIL THEN-- last line, erase in current position
   { oldName← bt.name;
   bt.name← NIL;
   ViewerOps.PaintViewer[bt, all];
   bt.name← oldName;
   };
   ViewerOps.MoveViewer[bt, bt.wx, bt.wy-delta, bt.ww, bt.wh, FALSE];
   ViewerOps.PaintViewer[bt, all];
  ENDLOOP;
END;

IF which = NIL THEN which← FindMsgViewer[parent, msg];
IF selected = which THEN Buttons.SetDisplayStyle[which, $BlackOnWhite];
 mfhT← NARROW[ViewerOps.FetchProp[which, $mfH]];
 prev← mfhT.prev;
IF (next← mfhT.next) = NIL
THEN
{ ViewerOps.AddProp[parent, $lastButton, mfhT.prev]; -- last line
   which.name← NIL;
   ViewerOps.PaintViewer[which, all];  -- erase line
   ViewerOps.DestroyViewer[which, FALSE];   -- don't paint
  }
ELSE
IF parent.iconic THEN RemoveFrom[]
ELSE ViewerLocks.CallUnderWriteLock[RemoveFrom, parent];

-- take which out of chain of mfh's
IF prev # NIL THEN
 { x← NARROW[ViewerOps.FetchProp[prev, $mfH]];
IF x # NIL THEN x.next← next};
IF next # NIL THEN
  { x← NARROW[ViewerOps.FetchProp[next, $mfH]];
  x.prev← prev};

IF selected = which THEN
  { IF next = NIL THEN ViewerOps.AddProp[parent, $selectedMsg, NIL]  -- no selected Msg
  ELSE
   { ViewerOps.AddProp[parent, $selectedMsg, next];
   Buttons.SetDisplayStyle[next, $BlackOnGrey]; -- show it is selected
   ViewerOps.PaintViewer[next, all];
   };
  };
END;

RemoveFromMsgSetDisplayer: PUBLIC PROC[msgSet: MsgSet, msg: Msg] =
BEGIN
 msV: Viewer← FindMSViewer[msgSet];
IF msV = NIL THEN RETURN;
 RemoveFromDisplayedMsgSet[msV, FindMsgViewer[msV, msg], msg];
END;

FindMFH: PROC[msViewer: Viewer, rel: Relship] RETURNS[mfh: MsgSetFieldHandle] =
BEGIN
 lb: Viewer← NARROW[ViewerOps.FetchProp[msViewer, $lastButton]];
DO
IF lb = NIL THEN RETURN;
  mfh← NARROW[ViewerOps.FetchProp[lb, $mfH]];
IF EqEntities[mfh.relship, rel] THEN RETURN;
  lb← mfh.prev;
ENDLOOP;
END;

FindMsgViewer: PROC[msViewer: Viewer, msg: Msg] RETURNS[mViewer: Viewer] =
BEGIN
 mfh: MsgSetFieldHandle;
 mViewer← NARROW[ViewerOps.FetchProp[msViewer, $lastButton]];
DO
IF mViewer = NIL THEN RETURN;
  mfh← NARROW[ViewerOps.FetchProp[mViewer, $mfH]];
IF EqEntities[mfh.msg, msg] THEN RETURN;
  mViewer← mfh.prev;
ENDLOOP;
END;

BuildTupleWindowButtons: PROC[v: Viewer, e: Entity]
  RETURNS[firstUnread, lastButton: Viewer] =
BEGIN
tuplesList: LIST OF Relship;
prevMfh, mfh: MsgSetFieldHandle← NIL;
DoTupleWindowButtons: PROC =
{ FOR tL: LIST OF Relship← tuplesList, tL.rest UNTIL tL = NIL DO
  m: Msg← V2E[DB.GetF[tL.first, mCategoryOf]]; -- follow relship to the Msg
  hasBeenRead: BOOL← V2B[DB.GetP[m, mHasBeenReadIs]];

  [mfh, lastButton]← BuildMsgLineViewer[lastButton, MsgButtonName[m, hasBeenRead]];
IF ~hasBeenRead AND firstUnread = NIL THEN firstUnread← lastButton;
  mfh.relship← tL.first;
  mfh.msg← m;
  mfh.hasBeenRead← hasBeenRead;
  mfh.msgName← DB.GetName[m];
IF prevMfh # NIL THEN prevMfh.next← lastButton;
  prevMfh← mfh;
ENDLOOP;
  };

lastButton← v;
tuplesList← RelationSubsetList[mCategory, LIST[[mCategoryIs, e]]];
WalnutDB.AcquireDBLock[DoTupleWindowButtons];
ViewerOps.AddProp[v, $lastButton, lastButton]; -- for updating
END;

MsgButtonName: PROC[msg: Msg, hasBeenRead: BOOL] RETURNS [ROPE] =
{ RETURN[Rope.Cat[
   IF hasBeenRead THEN " " ELSE "? ",
   SquashRopeIntoWidth[V2S[DB.GetP[msg, mTOCEntryIs]], 165],
   V2S[DB.GetP[msg, mSubjectIs]]]];
};

BuildMsgLineViewer: PROC[lastButton: Viewer, name: ROPE]
  RETURNS[mfh: MsgSetFieldHandle, lb: Viewer] =
BEGIN
isIconic: BOOL;
BuildLine: PROC =
BEGIN
  IF lastButton.parent = NIL THEN
   lb← WalnutViewer.FirstButton[
 q: walnutQueue,
 name: name,
 proc: MsgSetSelectionProc,
 width: 1024,   -- hack for now = screenW
 parent: lastButton]
ELSE lb← WalnutViewer.AnotherButton[
 q: walnutQueue,
 name: name,
 proc: MsgSetSelectionProc,
 sib: lastButton,
 width: 1024,   -- hack for now = screenW
 newLine: TRUE];
END;

 isIconic← IF lastButton.parent = NIL THEN lastButton.iconic ELSE lastButton.parent.iconic;
IF isIconic THEN BuildLine[]
ELSE ViewerLocks.CallUnderWriteLock
   [BuildLine, IF lastButton.parent = NIL THEN lastButton ELSE lastButton.parent];

-- Containers.ChildXBound[lb.parent, lb];
ViewerOps.AddProp[lb, $mfH, mfh← NEW[MsgSetFieldObject←
 [msgViewer: lb, msgTOC: name, prev: lastButton, posOK: FALSE]]];
END;

noMsgRope: ROPE← "Msg is no longer in the MsgSet";

MsgSetSelectionProc: Buttons.ButtonProc =
{ viewer: Viewer = NARROW[parent];
IF viewer.destroyed THEN Report[noMsgRope]
ELSE
{ []← SelectMsgInMsgSet[viewer];
IF control THEN DeleteProc[parent: viewer.parent, mouseButton: mouseButton]
ELSE IF mouseButton#red THEN DisplayOneMsg[viewer, shift]
};
};

DisplayOneMsg: PROC[viewer: Viewer, shift: BOOL] =
BEGIN
 mfh: MsgSetFieldHandle = NARROW[ViewerOps.FetchProp[viewer, $mfH]];
 v: Viewer = DisplayMsgFromMsgSet[mfh, viewer.parent, shift];
IF ~mfh.hasBeenRead THEN MarkMsgAsRead[mfh, viewer];
END;

SelectMsgInMsgSet: PROC[msgButton: Viewer] RETURNS[sameAsBefore: BOOL] =
BEGIN
IF msgButton.destroyed THEN Report[noMsgRope]
ELSE
  { prevSelected: Viewer = NARROW[ViewerOps.FetchProp[msgButton.parent, $selectedMsg]];
  IF prevSelected = msgButton THEN RETURN[TRUE];
-- turn off previous selection
  IF prevSelected # NIL AND ~prevSelected.destroyed THEN
   { Buttons.SetDisplayStyle[prevSelected, $BlackOnWhite];
   ViewerOps.PaintViewer[prevSelected, all]};

   Buttons.SetDisplayStyle[msgButton, $BlackOnGrey]; -- show it is selected
  ViewerOps.PaintViewer[msgButton, all];
   ViewerOps.AddProp[msgButton.parent, $selectedMsg, msgButton];
  };
RETURN[FALSE];
END;

MarkMsgAsRead: PROC[mfh: MsgSetFieldHandle, msgButton: Viewer] =
-- used by selection, MoveTo & Delete
BEGIN
 newName: ROPE = msgButton.name.Replace[0, 1, " "]; -- change ? to SP
 WalnutLog.LogMsgHasBeenRead[mfh.msgName];
 WalnutDB.SetMsgHasBeenRead[mfh.msg];
 mfh.hasBeenRead← TRUE;
 mfh.msgTOC← newName;
 Buttons.ReLabel[msgButton, newName];
END;

MSNameFromViewer: PROC[v: Viewer] RETURNS[msName: ROPE] =
{ fullName: ROPENARROW[ViewerOps.FetchProp[v, $Entity]];
RETURN[fullName.Substr[msgSetName.Length[]]];  -- less of a hack now
};

SquashRopeIntoWidth: PROC[s: ROPE, colWidth: INT] RETURNS[ROPE] =
-- Truncates s with "..." or expands it with blanks, so that it is about
-- colWidth characters wide. Not exact, uses a few heuristics here...
BEGIN
blankCount: INT;
width: INT← VFonts.StringWidth[s];
DO
IF width<= colWidth THEN EXIT;
-- truncate
BEGIN guessLength: INT← s.Length[] * colWidth / width;
s← Rope.Cat[s.Substr[0, MAX[0, guessLength-4]], "..."];
width← VFonts.StringWidth[s];
END;
ENDLOOP;
-- At this point s is shorter than colWidth and we want to extend it with blanks
blankCount← ((colWidth - width) / blankWidth) + 1;  -- force at least one blank
s← Rope.Cat[s, Rope.Substr[blanks, 0, MIN[blankCount, blanks.Length[]]]];
RETURN[s]
END;

----------------------------

-- start code
BuildDisplayerMenu[];

END.