<> <> <> <> DIRECTORY Atom USING [GetPName, MakeAtom], Basics, Buttons, ChoiceButtons, Commander USING [CommandProc, Register], Containers, Icons, IndexProps, IndexTree, IndexToolViewer, IO, Labels, List, Menus, MessageWindow, NumberLabels USING [CreateNumber, NumberLabel, NumberLabelUpdate], RedBlackTree, Rope, Rules, RuntimeError USING [UNCAUGHT], TEditScrolling, TextEdit, TextNode, TiogaOps, TiogaOpsDefs, TIPUser, ViewerClasses USING [InitProc, ViewerClass, ViewerClassRec, Viewer], ViewerOps USING [AddProp, CreateViewer, FetchProp, FetchViewerClass, FindViewer, PaintViewer, RegisterViewerClass, SetMenu, SetNewVersion], ViewerSpecs, ViewerTools; IndexToolViewerImpl: CEDAR PROGRAM IMPORTS Atom, Buttons, ChoiceButtons, Commander, Containers, IndexProps, IndexTree, IO, Labels, List, Menus, MessageWindow, NumberLabels, RedBlackTree, Rope, Rules, RuntimeError, TEditScrolling, TextEdit, TextNode, TiogaOps, ViewerOps, ViewerSpecs, ViewerTools EXPORTS IndexToolViewer = BEGIN OPEN IndexToolViewer; ROPE: TYPE = Rope.ROPE; IndexEntry: TYPE ~ IndexProps.IndexEntry; Phrases: TYPE ~ IndexProps.Phrases; TextNodeRef: PROC [n: TiogaOps.Ref] RETURNS [TextNode.Ref] ~ TRUSTED { RETURN [LOOPHOLE[n]]; }; Span: PROC [s, e: TiogaOps.Location] RETURNS [TextNode.Span] ~ TRUSTED { RETURN [[LOOPHOLE[s], LOOPHOLE[e]]]; }; <> NewTool: PUBLIC PROCEDURE [documentName: ROPE _ NIL, kindOfIndex: ROPE _ NIL] RETURNS [indexToolHandle: IndexToolHandle] = { v: ViewerClasses.Viewer; caption: ROPE _ IF documentName.IsEmpty THEN "IndexTool" ELSE Rope.Concat["Index for ", documentName]; IF NOT kindOfIndex.IsEmpty THEN caption _ Rope.Cat[kindOfIndex, " ", caption]; v _ ViewerOps.CreateViewer[ flavor: $IndexTool, info: [name: caption, column: right, iconic: FALSE, scrollable: FALSE], paint: TRUE]; indexToolHandle _ NARROW[ViewerOps.FetchProp[v, $IndexToolHandle]]; indexToolHandle.index _ IndexTree.CreateIndex[TextNodeRef[TiogaOps.ViewerDoc[indexToolHandle.indexViewer]]]; indexToolHandle.kindOfIndex _ IF kindOfIndex.IsEmpty THEN $Index ELSE Atom.MakeAtom[Rope.Concat[kindOfIndex, "Index"]]; }; CreateIndexFromDocument: PUBLIC PROCEDURE [documentName: ROPE _ NIL, kindOfIndex: ROPE _ NIL] RETURNS [indexToolHandle: IndexToolHandle] = { IF documentName.IsEmpty THEN RETURN[NIL] ELSE { documentViewer: ViewerClasses.Viewer _ ViewerOps.FindViewer[documentName]; IF documentViewer = NIL THEN documentViewer _ ViewerOps.CreateViewer[flavor: $Text, info: [name: documentName, file: documentName] ]; indexToolHandle _ CreateIndexFromViewer[documentViewer, kindOfIndex]; }; }; CreateIndexFromViewer: PUBLIC PROCEDURE [documentViewer: ViewerClasses.Viewer, kindOfIndex: ROPE _ NIL] RETURNS [indexToolHandle: IndexToolHandle] = { indexToolHandle _ NewTool[documentViewer.name, kindOfIndex]; indexToolHandle.documentViewer _ documentViewer; ScanIndexProperties[documentViewer, indexToolHandle]; }; ScanIndexProperties: PUBLIC PROCEDURE [viewer: ViewerClasses.Viewer, indexToolHandle: IndexToolHandle] = { root: TiogaOps.Ref _ TiogaOps.ViewerDoc[viewer]; node: TiogaOps.Ref _ TiogaOps.FirstChild[root]; ScanIndexPropsOnNode: PROC [node: TextNode.Ref] ~ { SetIndexProps: TextEdit.ModifyPropsAction ~ { <> ixList: IndexProps.IndexEntryList _ NARROW[value]; SetIndexEntryProc: IndexProps.IndexEntryProc ~ { <> [] _ IndexTree.InsertNewIndexEntry[ix: ix, range: [[node, index], [node, index+nChars-1]], index: indexToolHandle.index]; }; IndexProps.MapIndexEntryList[ixList, SetIndexEntryProc]; }; [] _ TextEdit.ModifyCharProps[node, $IndexEntries, 0, INT.LAST, SetIndexProps, NIL, TextNodeRef[root]]; }; ForEachNodeWithCharProps: PROC [root: TextNode.Ref, nodeProc: PROC[node: TextNode.Ref]] ~ { node: TextNode.Ref _ root; WHILE (node _ TextNode.Next[node]) # NIL DO IF node.hascharprops THEN nodeProc[node]; ENDLOOP; }; disaster: BOOL _ FALSE; TiogaOps.Lock[root]; ForEachNodeWithCharProps[TextNodeRef[root], ScanIndexPropsOnNode ! RuntimeError.UNCAUGHT => {disaster _ TRUE; TiogaOps.Unlock[root]}]; IF NOT disaster THEN TiogaOps.Unlock[root]; TEditScrolling.ScrollToPosition[indexToolHandle.indexViewer, [indexToolHandle.index.root, 0]]; }; <<>> <> IndexToolViewerInit: ViewerClasses.InitProc ~ { indexToolHandle: IndexToolHandle _ NEW[IndexToolHandleRec]; firstColumn: INTEGER ~ 0; secondColumn: INTEGER ~ ViewerSpecs.openRightWidth/2; firstThird: INTEGER ~ 0; secondThird: INTEGER ~ ViewerSpecs.openRightWidth/3; thirdThird: INTEGER ~ secondThird+secondThird; columnWidth: INTEGER ~ ViewerSpecs.openRightWidth/2; numberOfPhrases: INTEGER ~ 10; phraseSize: INTEGER ~ secondColumn - 100; defaultPhraseRows: INTEGER ~ 3; h: INTEGER ~ ViewerSpecs.captionHeight; rowH: INTEGER ~ ViewerSpecs.messageWindowHeight; thisY: INTEGER _ 0; NextRow: PROCEDURE [rows: INTEGER _ 1] ~ { thisY _ thisY + rows*rowH; }; menu: Menus.Menu _ Menus.CreateMenu[2]; InsertMenuEntry: PROCEDURE [name: Rope.ROPE, proc: Menus.MenuProc, line: Menus.MenuLine] ~ { menuEntry: Menus.MenuEntry _ Menus.CreateEntry[ name: name, proc: proc, clientData: indexToolHandle, documentation: NIL, fork: TRUE, guarded: FALSE]; Menus.InsertMenuEntry[menu, menuEntry, line]; }; CreatePhraseButtons: PROCEDURE [label: ROPE, x, y: INTEGER] RETURNS [phraseContainer: ViewerClasses.Viewer] ~ { <> thisY _ y; NextRow[]; phraseContainer _ Containers.Create[ info: [ scrollable: TRUE, border: FALSE, parent: self, wx: x, wy: thisY, ww: columnWidth, wh: defaultPhraseRows*rowH], paint: FALSE]; <> FOR i: INTEGER DECREASING IN [1..numberOfPhrases] DO phraseData: PhraseButtonData _ NEW[PhraseButtonDataRec _ [indexToolHandle, i]]; button: Buttons.Button _ Buttons.Create[ info: [ name: " ", wx: 0, wy: (i-1)*rowH, ww: 0, wh: h, parent: phraseContainer, scrollable: FALSE, border: TRUE], clientData: phraseData, proc: PhraseButtonProc, paint: FALSE]; phraseData.viewer _ ViewerOps.CreateViewer[flavor: $Text, info: [ wx: button.cw + 6*i, wy: (i-1)*rowH, ww: phraseContainer.cw - (button.cw + 6*i) - 5, wh: rowH, parent: phraseContainer, scrollable: TRUE, border: TRUE], paint: FALSE]; ViewerOps.AddProp[phraseData.viewer, $IndexPhrase, $TRUE]; ENDLOOP; [] _ Buttons.Create[ info: [ name: label, wx: x, wy: y, wh: rowH, parent: self, scrollable: FALSE, border: FALSE], proc: ClearPhrasesButtonProc, clientData: phraseContainer, paint: FALSE]; thisY _ y; }; FeedbackLabel: PROCEDURE [label: ROPE, x, y: INTEGER, charsInNumber: NAT _ 4] RETURNS[v: NumberLabels.NumberLabel] ~ { v _ Labels.Create[ info: [name: label, parent: self, wx: x, wy: y, wh: rowH, border: FALSE], paint: FALSE ]; v _ NumberLabels.CreateNumber[ info: [ parent: self, wx: x + v.ww, wy: y, ww: columnWidth - v.ww - 40, wh: rowH, border: FALSE], chars: charsInNumber, initialValue: 0, paint: FALSE ]; }; Rule: PROCEDURE [h: CARDINAL] ~ { rule: Rules.Rule _ Rules.Create[info: [parent: self, wx: 0, wy: thisY, ww: 0, wh: h]]; Containers.ChildXBound[self, rule]; }; Choices: PROCEDURE [choices: Phrases, x, y: INTEGER] RETURNS [choiceRef: ChoiceButtons.EnumTypeRef] ~ { choiceRef _ ChoiceButtons.BuildEnumTypeSelection[viewer: self, x: x, y: y, buttonNames: choices, clientdata: indexToolHandle, style: menuSelection]; }; <> containerInitProc[self]; indexToolHandle.toolViewer _ self; InsertMenuEntry[name: "CopyIndexTo", proc: CopyIndexToButtonProc, line: 0]; InsertMenuEntry[name: "NewIndexTool", proc: NewIndexToolButtonProc, line: 0]; InsertMenuEntry[name: "DeleteEntry", proc: DeleteEntryButtonProc, line: 0]; InsertMenuEntry[name: "InsertEntry!", proc: InsertEntryButtonProc, line: 0]; InsertMenuEntry[name: "AddNewKind", proc: AddNewKindButtonProc, line: 1]; InsertMenuEntry[name: "PermutePhrases", proc: PermutePhrasesButtonProc, line: 1]; ViewerOps.SetMenu[self, menu]; indexToolHandle.kindOfEntryChoices _ Choices[LIST["Ordinary", "See", "SeeAlso"], firstColumn, thisY]; NextRow[]; indexToolHandle.indexPhrasesContainer _ CreatePhraseButtons["Index Entry Phrases:", firstColumn, thisY]; indexToolHandle.sortAsPhrasesContainer _ CreatePhraseButtons["Sort As:", secondColumn, thisY]; NextRow[4]; indexToolHandle.seePhrasesContainer _ CreatePhraseButtons["See phrases:", firstColumn, thisY]; NextRow[4]; Rule[1]; -- ============================================================== indexToolHandle.entriesLabel _ FeedbackLabel["Entries:", firstThird, thisY]; indexToolHandle.seeCountLabel _ FeedbackLabel["See refs:", secondThird, thisY]; indexToolHandle.nestingCountLabel _ FeedbackLabel["Max nesting:", thirdThird, thisY]; NextRow[]; indexToolHandle.startPositionLabel _ FeedbackLabel["Start position:", firstThird, thisY]; indexToolHandle.endPositionLabel _ FeedbackLabel["End position:", secondThird, thisY]; NextRow[]; Rule[1]; -- ============================================================== indexToolHandle.indexViewer _ ViewerOps.CreateViewer[ flavor: $TiogaButtons, info: [ wx: firstColumn, wy: thisY, parent: self, scrollable: TRUE], paint: FALSE ]; Containers.ChildXBound[self, indexToolHandle.indexViewer]; Containers.ChildYBound[self, indexToolHandle.indexViewer]; ViewerOps.AddProp[self, $IndexToolHandle, indexToolHandle]; }; CopyIndexToButtonProc: Menus.ClickProc = { indexToolHandle: IndexToolHandle _ NARROW[clientData, IndexToolHandle]; IF TiogaOps.GetSelection[primary].viewer = NIL THEN { Sorry["Make a primary selection for the destination of the index."]; RETURN; } ELSE { root: TiogaOps.Ref ~ TiogaOps.ViewerDoc[indexToolHandle.indexViewer]; TiogaOps.SelectNodes[viewer: indexToolHandle.indexViewer, start: TiogaOps.FirstChild[root], end: TiogaOps.LastLocWithin[root].node, which: secondary]; TiogaOps.ToPrimary[]; }; }; NewIndexToolButtonProc: Menus.ClickProc = { selectedViewer: ViewerClasses.Viewer _ ViewerTools.GetSelectedViewer[]; documentName: ROPE _ ViewerTools.GetSelectionContents[]; IF selectedViewer = NIL THEN { Sorry["Make a text selection of the document name (longer than 1 character)"]; documentName _ NIL; } ELSE IF documentName.IsEmpty THEN documentName _ selectedViewer.name; [] _ CreateIndexFromDocument[documentName]; }; DeleteEntryButtonProc: Menus.ClickProc = { <> Sorry["Not implemented"]; }; InsertEntryButtonProc: Menus.ClickProc = { indexToolHandle: IndexToolHandle _ NARROW[clientData, IndexToolHandle]; selectedViewer: ViewerClasses.Viewer _ ViewerTools.GetSelectedViewer[]; <<>> <> IF selectedViewer = NIL THEN { Sorry["Make a text selection in the document for this index entry"]; RETURN; } ELSE IF ViewerTools.GetContents[indexToolHandle.indexPhrasesContainer.child].IsEmpty THEN { Sorry["Supply an index phrase in the IndexTool before inserting it!"]; RETURN; } ELSE IF indexToolHandle.documentViewer = NIL THEN { <> indexToolHandle.documentViewer _ selectedViewer; ScanIndexProperties[selectedViewer, indexToolHandle]; <> indexToolHandle.toolViewer.name _ Rope.Cat["IndexTool for ", selectedViewer.name]; ViewerOps.PaintViewer[indexToolHandle.toolViewer, caption, FALSE]; } ELSE IF selectedViewer # indexToolHandle.documentViewer THEN { Sorry["Can't index that document from this index tool"]; RETURN; }; { start, end: TiogaOps.Location; [start~start, end~end] _ TiogaOps.GetSelection[primary]; IF start.node # end.node THEN { Sorry["Index entries must be within a single node"]; RETURN; } ELSE { ixList: IndexProps.IndexEntryList _ NARROW[TextEdit.GetCharProp[ node~TextNodeRef[start.node], index~start.where, name~$IndexEntries]]; selectionRoot: TiogaOps.Ref _ TiogaOps.SelectionRoot[primary]; ix: IndexEntry _ IndexEntryFromTool[indexToolHandle]; ixItem: IndexTree.IxItem _ IndexTree.InsertNewIndexEntry[ix, Span[start, end], indexToolHandle.index ! IndexTree.DuplicateKey => GOTO Duplicate]; TiogaOps.Lock[selectionRoot]; TextEdit.PutCharProp[node~TextNodeRef[start.node], index~start.where, name~$IndexEntries, value~IndexProps.AddEntryToList[ix, ixList], nChars~end.where-start.where, root~TextNodeRef[selectionRoot]]; TiogaOps.Unlock[selectionRoot]; ViewerOps.SetNewVersion[indexToolHandle.documentViewer]; <> NormalizeIndexViewer[indexToolHandle, ixItem]; UpdateFeedback[indexToolHandle.entriesLabel, indexToolHandle.entries _ indexToolHandle.entries + 1]; IF indexToolHandle.kindOfEntry = $See OR indexToolHandle.kindOfEntry = $SeeAlso THEN UpdateFeedback[indexToolHandle.seeCountLabel, indexToolHandle.seeCount _ indexToolHandle.seeCount + 1]; UpdateFeedback[indexToolHandle.nestingCountLabel, indexToolHandle.nestingCount _ MAX[NumberOfPhrases[ix.phrases], indexToolHandle.nestingCount]]; UpdateFeedback[indexToolHandle.startPositionLabel, TiogaOps.LocOffset[[selectionRoot, 0], start]]; UpdateFeedback[indexToolHandle.endPositionLabel, TiogaOps.LocOffset[[selectionRoot, 0], end]]; }; }; EXITS Duplicate => Sorry["Index entry duplicates an entry already in table; NOT inserted."]; }; AddNewKindButtonProc: Menus.ClickProc = { <> <> << _ ChoiceButtons.GetSelectedButton[indexToolHandle.kindOfEntryChoices];>> Sorry["Not implemented"]; }; PermutePhrasesButtonProc: Menus.ClickProc = { <> Sorry["Not implemented"]; }; atTheBeginning: ViewerTools.SelPos = NEW[ViewerTools.SelPosRec _ [0, 0, FALSE, before]]; PhraseButtonData: TYPE = REF PhraseButtonDataRec; PhraseButtonDataRec: TYPE = RECORD [ indexToolHandle: IndexToolHandle, phraseNumber: CARDINAL _ 0, viewer: ViewerClasses.Viewer _ NIL ]; PhraseButtonProc: Menus.ClickProc = { phraseData: PhraseButtonData _ NARROW[clientData, PhraseButtonData]; indexToolHandle: IndexToolHandle _ phraseData.indexToolHandle; SELECT mouseButton FROM red => { selectedViewer: ViewerClasses.Viewer _ ViewerTools.GetSelectedViewer[]; IF selectedViewer = NIL THEN Sorry["Please make a text selection of the phrase to index"] ELSE { start, end: TiogaOps.Location; pendingDelete: BOOL; level: TiogaOps.SelectionGrain; caretBefore: BOOL; root: TiogaOps.Ref _ TiogaOps.SelectionRoot[primary]; [selectedViewer, start, end, level, caretBefore, pendingDelete] _ TiogaOps.GetSelection[primary]; IF pendingDelete THEN TiogaOps.SetSelection[selectedViewer, start, end, level, caretBefore, FALSE, primary]; <> IF start.node # end.node THEN Sorry["Index phrases must be within a single node"] ELSE { ViewerTools.SetContents[phraseData.viewer, NIL]; TiogaOps.SelectDocument[viewer~phraseData.viewer, level~char, which~secondary]; TiogaOps.ToSecondary[]; -- copy the selected phrase TiogaOps.SetSelection[selectedViewer, start, end, level, caretBefore, FALSE, primary]; }; }; }; yellow => ViewerTools.SetSelection[phraseData.viewer, atTheBeginning]; blue => { ViewerTools.SetContents[phraseData.viewer, NIL]; ViewerTools.SetSelection[phraseData.viewer, atTheBeginning]; }; ENDCASE; }; ClearPhrasesButtonProc: Buttons.ButtonProc = { phraseContainer: ViewerClasses.Viewer _ NARROW[clientData, ViewerClasses.Viewer]; v: ViewerClasses.Viewer _ phraseContainer.child; WHILE v # NIL DO ViewerTools.SetContents[v, NIL]; v _ v.sibling; ENDLOOP; ViewerTools.SetSelection[phraseContainer.child, atTheBeginning]; }; <> Sorry: PROCEDURE [rope: ROPE] = { MessageWindow.Append[message: rope, clearFirst: TRUE]; MessageWindow.Blink[]; }; UpdatePhrases: PROC [phraseContainer: ViewerClasses.Viewer, phrases: Phrases] ~ { v: ViewerClasses.Viewer _ phraseContainer.child; WHILE v # NIL DO IF ViewerOps.FetchProp[v, $IndexPhrase] # NIL THEN { IF phrases # NIL AND phrases.rest # NIL THEN { contents: ViewerTools.TiogaContents _ NEW[ViewerTools.TiogaContentsRec _ [NARROW[phrases.first, ROPE], NARROW[phrases.rest.first, ROPE]]]; ViewerTools.SetTiogaContents[v, contents]; phrases _ phrases.rest.rest; } ELSE ViewerTools.SetContents[v, NIL]; }; v _ v.sibling; ENDLOOP; }; <<>> CollectPhrases: PROCEDURE [phraseContainer: ViewerClasses.Viewer] RETURNS [phrases: Phrases _ NIL] = { v: ViewerClasses.Viewer _ phraseContainer.child; phrase: ViewerTools.TiogaContents; WHILE v # NIL DO IF ViewerOps.FetchProp[v, $IndexPhrase] # NIL THEN { phrase _ ViewerTools.GetTiogaContents[v]; IF phrase = NIL OR phrase.contents.IsEmpty THEN EXIT; TRUSTED {phrases _ LOOPHOLE[List.Nconc1[LOOPHOLE[phrases], phrase.contents]]}; TRUSTED {phrases _ LOOPHOLE[List.Nconc1[LOOPHOLE[phrases], phrase.formatting]]}; }; v _ v.sibling; ENDLOOP; }; NumberOfPhrases: PROC [phrases: Phrases] RETURNS [NAT] ~ TRUSTED { RETURN [List.Length[LOOPHOLE[phrases]]/2]; }; <<>> UpdateFeedback: PROC [numberLabel: NumberLabels.NumberLabel, value: INT] ~ { IF numberLabel.destroyed THEN RETURN; NumberLabels.NumberLabelUpdate[numberLabel, value]; }; IndexEntryToTool: PROC [ix: IndexEntry, indexToolHandle: IndexToolHandle] ~ { IF ix.kindOfIndex # indexToolHandle.kindOfIndex THEN ERROR WrongIndexTool; UpdatePhrases[indexToolHandle.indexPhrasesContainer, ix.phrases]; UpdatePhrases[indexToolHandle.sortAsPhrasesContainer, ix.sortAsPhrases]; UpdatePhrases[indexToolHandle.seePhrasesContainer, ix.seePhrases]; UpdateKindOfEntry[indexToolHandle, ix]; }; WrongIndexTool: ERROR = CODE; -- an internal logic error <<>> IndexEntryFromTool: PROC [indexToolHandle: IndexToolHandle] RETURNS [ix: IndexEntry _ NEW[IndexProps.IndexEntryRec]] ~ { ix.kindOfIndex _ indexToolHandle.kindOfIndex; <> ix.phrases _ CollectPhrases[indexToolHandle.indexPhrasesContainer]; ix.seePhrases _ CollectPhrases[indexToolHandle.seePhrasesContainer]; IF ix.seePhrases # NIL AND ix.kindOfEntry # $See AND ix.kindOfEntry # $SeeAlso THEN { ix.kindOfEntry _ $See; UpdateKindOfEntry[indexToolHandle, ix]; }; ix.sortAsPhrases _ CollectPhrases[indexToolHandle.sortAsPhrasesContainer]; }; <<>> UpdateKindOfEntry: PROC [indexToolHandle: IndexToolHandle, ix: IndexEntry] ~ { ChoiceButtons.UpdateChoiceButtons[indexToolHandle.toolViewer, indexToolHandle.kindOfEntryChoices, Atom.GetPName[ix.kindOfEntry] ! ChoiceButtons.ChoiceDoesntExist => GOTO AddKindToChoices]; EXITS AddKindToChoices => { NULL <> }; }; NormalizeIndexViewer: PROCEDURE [indexToolHandle: IndexToolHandle, ixItem: IndexTree.IxItem] = { nearestEntry: IndexEntry; item: IndexTree.IxItem; leftItem, equalItem, rightItem: RedBlackTree.UserData; [leftItem, equalItem, rightItem] _ RedBlackTree.Lookup3[indexToolHandle.index.table, ixItem]; item _ NARROW[IF equalItem # NIL THEN equalItem ELSE IF leftItem # NIL THEN leftItem ELSE rightItem, IndexTree.IxItem]; nearestEntry _ item.ix; TiogaOps.SelectNodes[viewer: indexToolHandle.indexViewer, start: item.button.startLoc.node, end: item.button.endLoc.node, level: word, which: feedback]; TEditScrolling.AutoScroll[indexToolHandle.indexViewer, TRUE, FALSE, feedback]; ViewerOps.PaintViewer[indexToolHandle.indexViewer, client]; }; <> IndexToolCommand: Commander.CommandProc = { documentName: ROPE _ NIL; documentName _ IO.GetTokenRope[IO.RIS[cmd.commandLine], IO.IDProc ! IO.EndOfStream => CONTINUE].token; <> IF documentName.IsEmpty THEN [] _ NewTool[] ELSE [] _ CreateIndexFromDocument[documentName]; }; <> indexToolViewerClass: ViewerClasses.ViewerClass ~ NEW[ViewerClasses.ViewerClassRec _ ViewerOps.FetchViewerClass[$Container]^]; containerInitProc: ViewerClasses.InitProc ~ indexToolViewerClass.init; <> indexToolViewerClass.init _ IndexToolViewerInit; indexToolViewerClass.icon _ tool; <> <> <> ViewerOps.RegisterViewerClass[$IndexTool, indexToolViewerClass]; Commander.Register[ key: "IndexTool", proc: IndexToolCommand, doc: "Create an index tool." ]; END.