-- 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: May 2, 1984 10:03:39 am PDT
DIRECTORY
Atom USING [GetPropFromList],
Buttons USING [ButtonProc, ReLabel, SetDisplayStyle],
DB USING [GetF, GetName, GetP],
Menus USING [AppendMenuEntry, CreateMenu, Menu, MenuProc],
Rope,
RuntimeError USING [BoundsFault],
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, RuntimeError, 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[q: walnutQueue, name: "Print",
   proc: WalnutPrintOps.MsgSetPrintProc, guarded: TRUE]];
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[q: walnutQueue, name: "Print",
   proc: WalnutPrintOps.MsgSetPrintProc, guarded: TRUE]];
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[q: walnutQueue, name: "Print",
   proc: WalnutPrintOps.MsgSetPrintProc, guarded: TRUE]];
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[q: walnutQueue, name: "Print",
   proc: WalnutPrintOps.MsgSetPrintProc, guarded: TRUE]];
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 =
BEGIN
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;
END;
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;
BEGIN ENABLE RuntimeError.BoundsFault => GOTO doItTheHardWay;
width← 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;
EXITS
doItTheHardWay => [width, s]← DoItTheHardWay[s, colWidth];
END;  -- of enable
-- 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;
DoItTheHardWay: PROC[s: ROPE, colWidth: INT] RETURNS[width: INT, s1: ROPE] =
BEGIN
thisWidth: INTEGER;
dots: ROPE = "...";
nullWidth: INTEGER = VFonts.CharWidth['\000];
width← VFonts.StringWidth[dots];
FOR i: INT IN [0 .. s.Length[]) DO
thisWidth← VFonts.CharWidth[s.Fetch[i] ! RuntimeError.BoundsFault =>
thisWidth← nullWidth ];
width← width + thisWidth;
IF width > colWidth THEN
{ width← width - thisWidth;
s1← Rope.Concat[s.Substr[0, MAX[0, i-1]], dots];
RETURN
};
ENDLOOP;
s1← s.Concat[dots];
END;
-- * * * * * * * * * * * * * * * * * * * * * * * * *
-- start code
BuildDisplayerMenu[];
END.