GraphBrowsersImpl.Mesa
Mike Spreitzer September 27, 1986 3:16:53 pm PDT
DIRECTORY Atom, GraphBrowsers, Graphs, Icons, Imager, ImagerBackdoor, ImagerBox, ImagerFont, Menus, MessageWindow, MJSContainers, Process, Real, Rope, TIPUser, VFonts, ViewerClasses, ViewerOps;
GraphBrowsersImpl: CEDAR MONITOR
IMPORTS Atom, Graphs, Icons, Imager, ImagerBackdoor, ImagerBox, ImagerFont, MessageWindow, MJSContainers, Process, Real, Rope, TIPUser, VFonts, ViewerOps
EXPORTS GraphBrowsers =
BEGIN OPEN GraphBrowsers;
Edge: TYPE = Graphs.Edge;
Direction: TYPE = Graphs.Direction;
nullVertex: Vertex = Graphs.nullVertex;
Pane: TYPE = REF PaneRep;
PaneRep: PUBLIC TYPE = RECORD [
asViewer: Viewer,
vertex: Vertex,
browser: Browser,
parent: Pane,
myFilter: Graphs.DirectionFilter,
myIndex: CARDINAL,
height: CARDINAL,
columnWidth: REAL,
rows, columns: CARDINAL,
expanded, expandedV: CARDINAL,
length: CARDINAL,
sel, vSel: CardBox,
maxNameWidth: REAL,
entries: SEQUENCE size: CARDINAL OF PaneEntry];
CardBox: TYPE = RECORD [
startRow, startCol, finishRow, finishCol: CARDINAL];
badBox: CardBox = [badRowCol, badRowCol, badRowCol, badRowCol];
badRowCol: CARDINAL = LAST[CARDINAL] - 1;
PaneEntry: TYPE = RECORD [
paneText, edgeLabel, vertexLabel: ROPENIL,
vertex: Vertex ← nullVertex,
drawX, drawY: REAL ← 0.0];
Browser: TYPE = REF BrowserRep;
BrowserRep: PUBLIC TYPE = RECORD [
container: Viewer ← NIL,
width: INTEGER ← 0,
panes: LIST OF Pane ← NIL,
class: BrowserClass,
browserData: REF ANY,
serial: INTEGER ← 1,
newSerial: CONDITION];
dy: INT ← VFonts.FontHeight[] + 2;
hSep: REAL ← 10;
font: ImagerFont.Font ← VFonts.defaultFont;
paneFlavor: ATOM ← Atom.MakeAtom["Spreitzer January 5, 1984 6:40 pm"];
icon: Icons.IconFlavor ← Icons.NewIconFromFile["GraphBrowsers.icons", 0];
basicBrowserClass: PUBLIC BrowserClass ← NEW [BrowserClassRep ← [
cmds: [
ALL[[[Expand, $Outgoing], [Contract]]],
ALL[[[Expand, $Both], [Contract]]],
ALL[[[Expand, $Incoming], [Contract]]]
]
]];
paneClass: ViewerClasses.ViewerClass ← NEW [ViewerClasses.ViewerClassRec ← [
flavor: paneFlavor,
notify: NotifyPane,
paint: PaintPane,
tipTable: TIPUser.InstantiateNewTIPTable[file: "GraphBrowserPane.TIP"],
topDownCoordSys: FALSE
]];
Browse: PUBLIC PROC [vertex: Vertex, browserClass: BrowserClass, browserData: REF ANYNIL, viewerInit: ViewerClasses.ViewerRec ← [], directionFilter: Graphs.DirectionFilter ← Graphs.allDirections, paint: BOOLEANTRUE] RETURNS [browser: Browser] =
BEGIN
browser ← NEW [BrowserRep ← [
class: browserClass,
browserData: browserData]];
viewerInit.data ← browser;
IF viewerInit.icon = unInit THEN viewerInit.icon ← icon;
browser.container ← MJSContainers.Create[viewerFlavor: $GraphBrowser, info: viewerInit, paint: paint];
TRUSTED {Process.InitializeCondition[@browser.newSerial, Process.SecondsToTicks[3]]};
browser.panes ← LIST[MakePane[
browser: browser,
vertex: vertex,
parent: NIL,
myIndex: 0,
directionFilter: directionFilter,
paint: paint
]];
END;
BrowserQuaViewer: PUBLIC PROC [browser: Browser] RETURNS [Viewer] =
{RETURN [browser.container]};
MakePane: PROC [browser: Browser, vertex: Vertex, parent: Pane, myIndex: CARDINAL, directionFilter: Graphs.DirectionFilter, paint: BOOLEAN] RETURNS [pane: Pane] =
BEGIN
SurveyNameWidths: PROC [direction: Direction, edgeLabel, vertexLabel: ROPE] = {
paneText: ROPE = MakePaneText[direction, edgeLabel, vertexLabel];
xmin,ymin,xmax,ymax: REAL;
[[xmin,ymin,xmax,ymax]] ← ImagerBox.BoxFromExtents[font.RopeBoundingBox[paneText]];
pane.entries[index] ← [paneText: paneText, edgeLabel: edgeLabel, vertexLabel: vertexLabel, drawX: 0, drawY: 0, vertex: nullVertex];
pane.maxNameWidth ← MAX[pane.maxNameWidth, hSep + xmax - xmin];
index ← index + 1;
};
SurveyVertexWidths: PROC [edge: Edge, direction: Direction, otherSide: Vertex] = {
edgeLabel: ROPE = edge.GetEdgeLabel[];
vertexLabel: ROPE = otherSide.GetVertexLabel[];
paneText: ROPE = MakePaneText[direction, edgeLabel, vertexLabel];
xmin,ymin,xmax,ymax: REAL;
[[xmin,ymin,xmax,ymax]] ← ImagerBox.BoxFromExtents[font.RopeBoundingBox[paneText]];
pane.entries[index] ← [paneText: paneText, edgeLabel: edgeLabel, vertexLabel: vertexLabel, drawX: 0, drawY: 0, vertex: otherSide];
pane.maxNameWidth ← MAX[pane.maxNameWidth, hSep + xmax - xmin];
index ← index + 1;
};
Elt: TYPE = RECORD [edge: Edge, direction: Direction, otherSide: Vertex];
GetList: PROC [edge: Edge, direction: Direction, otherSide: Vertex] =
BEGIN
rest: LIST OF Elt ← LIST[[edge, direction, otherSide]];
IF last = NIL THEN list ← rest ELSE last.rest ← rest;
last ← rest;
size ← size + 1;
END;
list, last: LIST OF Elt ← NIL;
index, size: CARDINAL ← 0;
IF vertex.class.GetNeighborCount = NIL
THEN TRUSTED {vertex.class.Expand[vertex, GetList, directionFilter]}
ELSE size ← vertex.class.GetNeighborCount[vertex, directionFilter];
pane ← NEW [PaneRep[size]];
pane.browser ← browser;
pane.parent ← parent;
pane.myFilter ← directionFilter;
pane.myIndex ← myIndex;
pane.vertex ← vertex;
pane.height ← 10;
pane.expanded ← pane.expandedV ← pane.size;
pane.sel ← pane.vSel ← badBox;
pane.columnWidth ← 0;
pane.maxNameWidth ← 0;
IF (size = 0) OR list # NIL THEN
BEGIN
FOR list ← list, list.rest WHILE list # NIL DO
SurveyVertexWidths[list.first.edge, list.first.direction, list.first.otherSide];
ENDLOOP;
END
ELSE vertex.EnumerateForLabels[SurveyNameWidths, directionFilter];
pane.length ← index;
pane.asViewer ← ViewerOps.CreateViewer[
flavor: paneFlavor,
info: [
parent: browser.container,
data: pane,
ww: browser.container.cw,
scrollable: FALSE],
paint: FALSE];
LayoutPane[pane, paint];
END;
MakePaneText: PROC [direction: Direction, edgeLabel, vertexLabel: ROPE] RETURNS [en: ROPE] = {
en ← SELECT direction FROM Incoming => "←", Outgoing => "->", Undirected => "-", ENDCASE => ERROR;
IF vertexLabel.Length[] # 0 THEN en ← en.Cat[" ", vertexLabel];
IF edgeLabel.Length[] # 0 THEN en ← edgeLabel.Cat[" ", en];
};
LayoutPane: PROC [pane: Pane, paint: BOOLEAN] =
BEGIN
index: CARDINAL ← 0;
paneWidth: REAL ← 0;
pane.columns ← MAX[1, Real.FixC[(paneWidth ← MAX[pane.asViewer.cw, 100]) / MAX[pane.maxNameWidth, 1]]];
pane.columnWidth ← paneWidth / pane.columns;
pane.rows ← ((pane.length + pane.columns - 1) / pane.columns);
pane.height ← pane.rows * dy;
FOR index IN [0 .. pane.length) DO
leftX, bottomY: REAL;
xmin,ymin,xmax,ymax: REAL;
leftX ← pane.columnWidth * (index MOD pane.columns);
bottomY ← pane.height + 1 - (1 + index / pane.columns) * dy;
[[xmin,ymin,xmax,ymax]] ← ImagerBox.BoxFromExtents[font.RopeBoundingBox[pane.entries[index].paneText]];
pane.entries[index].drawX ← leftX - xmin;
pane.entries[index].drawY ← bottomY - ymin;
ENDLOOP;
ViewerOps.MoveViewer[viewer: pane.asViewer, paint: paint,
x: pane.asViewer.wx,
y: IF pane.parent # NIL THEN pane.parent.asViewer.wy + pane.parent.asViewer.wh ELSE 0,
w: pane.asViewer.ww,
h: pane.height + pane.asViewer.wh - pane.asViewer.ch];
END;
LayoutBrowser: PROC [browser: Browser] =
BEGIN
topDown: LIST OF Pane ← NIL;
FOR rest: LIST OF Pane ← browser.panes, rest.rest WHILE rest # NIL DO
topDown ← CONS[rest.first, topDown];
ENDLOOP;
FOR topDown ← topDown, topDown.rest WHILE topDown # NIL DO
ViewerOps.MoveViewer[viewer: topDown.first.asViewer, paint: FALSE,
x: topDown.first.asViewer.wx,
y: topDown.first.asViewer.wy,
w: browser.container.cw,
h: topDown.first.asViewer.wh];
LayoutPane[topDown.first, FALSE];
ENDLOOP;
END;
SizeChange: PROC [self: Viewer] RETURNS [adjusted: BOOLFALSE] --ViewerClasses.AdjustProc-- =
BEGIN
browser: Browser ← NARROW[MJSContainers.GetClientData[self]];
IF browser.container = NIL THEN --we hope it's only because we're in initialization code-- RETURN;
IF (browser.width # browser.container.cw) THEN
BEGIN
browser.width ← browser.container.cw;
LayoutBrowser[browser];
END;
END;
SortBox: PROC [cb: CardBox] RETURNS [sorted: CardBox] =
BEGIN
[sorted.startCol, sorted.finishCol] ← MinMax[cb.startCol, cb.finishCol];
[sorted.startRow, sorted.finishRow] ← MinMax[cb.startRow, cb.finishRow];
END;
MinMax: PROC [a, b: CARDINAL] RETURNS [min, max: CARDINAL] =
{IF a <= b THEN RETURN [a, b] ELSE RETURN [b, a]};
PaintPane: PROC [self: Viewer, context: Imager.Context, whatChanged: REF, clear: BOOL] RETURNS [quit: BOOLFALSE] --ViewerClasses.PaintProc-- =
BEGIN
InvertEntry: PROC [index: CARDINAL] =
BEGIN
IF index IN [firstIndex .. afterLastIndex) THEN
BEGIN
xmin,ymin,xmax,ymax: REAL;
expanded: PaneEntry ← pane.entries[index];
[[xmin,ymin,xmax,ymax]] ← ImagerBox.BoxFromExtents[font.RopeBoundingBox[expanded.paneText]];
context.MaskRectangle[[
x: xmin + expanded.drawX, w: xmax - xmin,
y: ymin + expanded.drawY, h: ymax - ymin]];
END;
END;
InvertBox: PROC [cb: CardBox] =
BEGIN
context.MaskRectangle[[
x: cb.startCol * pane.columnWidth,
w: (cb.finishCol+1 - cb.startCol) * pane.columnWidth,
y: pane.height - (cb.finishRow+1) * dy,
h: (cb.finishRow+1 - cb.startRow) * dy]];
END;
pane: Pane ← NARROW[self.data];
box: Box ← [xmin: 0, ymin: 0, xmax: self.cw, ymax: self.ch];
firstRow: CARDINAL ← Real.FixC[MAX[pane.height - box.ymax, 0] / dy];
lastRow: CARDINAL ← Real.FixC[MAX[pane.height - box.ymin, 0] / dy];
firstIndex: CARDINAL ← firstRow * pane.columns;
afterLastIndex: CARDINALMIN[pane.length, (lastRow+1) * pane.columns];
IF clear THEN
BEGIN
context.SetFont[font];
FOR index: CARDINAL IN [firstIndex .. afterLastIndex) DO
entry: PaneEntry ← pane.entries[index];
context.SetXY[[x: entry.drawX, y: entry.drawY]];
context.ShowRope[entry.paneText];
ENDLOOP;
context.SetColor[ImagerBackdoor.invert];
InvertEntry[pane.expandedV ← pane.expanded];
InvertBox[SortBox[pane.vSel ← pane.sel]];
END
ELSE BEGIN
context.SetColor[ImagerBackdoor.invert];
IF pane.expandedV # pane.expanded THEN
{InvertEntry[pane.expandedV]; InvertEntry[pane.expandedV ← pane.expanded]};
IF pane.vSel # pane.sel THEN
{InvertBox[SortBox[pane.vSel]]; InvertBox[SortBox[pane.vSel ← pane.sel]]};
END;
END;
Box: TYPE = RECORD [xmin, ymin, xmax, ymax: REAL];
NotifyPane: PROC [self: Viewer, input: LIST OF REF ANY] --ViewerClasses.NotifyProc-- =
BEGIN
op: ATOMNARROW[input.first];
coords: TIPUser.TIPScreenCoords;
ctled, shifted, paint: BOOLEANFALSE;
button: Menus.MouseButton;
pane: Pane ← NARROW[self.data];
row, col: CARDINAL;
sel: CardBox;
IF op = $Abort THEN
BEGIN
pane.sel ← badBox;
ViewerOps.PaintViewer[viewer: pane.asViewer, hint: client, clearClient: FALSE];
RETURN;
END;
coords ← NARROW[input.rest.first];
row ← Real.FixC[(pane.height - coords.mouseY) / dy];
row ← MIN[row, MAX[1, pane.rows]-1];
col ← Real.FixC[coords.mouseX / pane.columnWidth];
col ← MIN[col, MAX[1, pane.columns]-1];
IF op = $Start THEN
{pane.sel.startRow ← row; pane.sel.startCol ← col; paint ← TRUE};
IF pane.sel.finishRow # row OR pane.sel.finishCol # col THEN
{pane.sel.finishRow ← row; pane.sel.finishCol ← col; paint ← TRUE};
IF paint THEN ViewerOps.PaintViewer[viewer: pane.asViewer, hint: client, clearClient: FALSE];
IF op # $Finish THEN RETURN;
sel ← SortBox[pane.sel];
pane.sel ← badBox;
ctled ← SELECT input.rest.rest.first FROM $Ctl => TRUE, $NotCtl => FALSE, ENDCASE => ERROR;
shifted ← SELECT input.rest.rest.rest.first FROM $Shift => TRUE, $NotShift => FALSE, ENDCASE => ERROR;
button ← SELECT input.rest.rest.rest.rest.first FROM $Red => red, $Yellow => yellow, $Blue => blue, ENDCASE => ERROR;
IF sel.startRow = badRowCol OR sel.startCol = badRowCol OR sel.finishRow = badRowCol OR sel.finishCol = badRowCol THEN MessageWindow.Append[message: "Bad selection", clearFirst: TRUE]
ELSE IF pane.browser.class.cmds[button][ctled][shifted].proc # NIL THEN
BEGIN
MessageWindow.Append[message: NIL, clearFirst: TRUE];
IF pane.browser.class.fork THEN TRUSTED {Process.Detach[FORK Invoke[pane, sel, button, ctled, shifted]]} ELSE Invoke[pane, sel, button, ctled, shifted];
END
ELSE MessageWindow.Append[message: "Undefined click", clearFirst: TRUE];
END;
Invoke: PROC [pane: Pane, sel: CardBox, button: Menus.MouseButton, ctled, shifted: BOOLEAN] =
BEGIN
browser: Browser ← pane.browser;
bc: BrowserCmd ← pane.browser.class.cmds[button][ctled][shifted];
single: BOOLEAN ← sel.startRow = sel.finishRow AND sel.startCol = sel.finishCol;
IF sel = badBox THEN RETURN;
IF browser.class.serialize THEN GetSerial[browser];
BEGIN
ENABLE UNWIND => IF browser.class.serialize THEN ReleaseSerial[browser];
FOR col: CARDINAL IN [sel.startCol .. sel.finishCol] DO
IF col >= pane.columns THEN EXIT;
FOR row: CARDINAL IN [sel.startRow .. sel.finishRow] DO
index: CARDINAL;
IF row >= pane.rows THEN EXIT;
index ← row * pane.columns + col;
IF NOT index IN [0 .. pane.length) THEN
{IF single THEN MessageWindow.Append[message: "Missed", clearFirst: FALSE]}
ELSE BEGIN
bc.proc[pane: pane, index: index, cmdData: bc.cmdData, browserData: browser.browserData, button: button, ctl: ctled, shift: shifted, paint: TRUE];
END
ENDLOOP;
ENDLOOP;
END;
IF browser.class.serialize THEN ReleaseSerial[browser];
END;
GetSerial: ENTRY PROC [browser: Browser] =
BEGIN
WHILE browser.serial < 1 DO
WAIT browser.newSerial;
ENDLOOP;
browser.serial ← browser.serial - 1;
END;
ReleaseSerial: ENTRY PROC [browser: Browser] =
BEGIN
browser.serial ← browser.serial + 1;
BROADCAST browser.newSerial;
END;
GetPaneVertex: PUBLIC PROC [pane: Pane] RETURNS [vertex: Vertex] = {RETURN [pane.vertex]};
GetChildVertex: PUBLIC PROC [pane: Pane, index: CARDINAL--origin 0--] RETURNS [vertex: Vertex] = {
IF pane.entries[index].vertex = nullVertex THEN {
pane.entries[index].vertex ← GN[pane.vertex, pane.myFilter, pane.entries[index].edgeLabel, pane.entries[index].vertexLabel, index+1];
};
RETURN [pane.entries[index].vertex];
};
GN: PROC [v: Vertex, df: Graphs.DirectionFilter, en, vn: ROPE, index: INT] RETURNS [n: Vertex] = {
e: Edge;
[e, n] ← Graphs.GetNeighbor[v, df, en, vn, index]};
GetPaneUpEdge: PUBLIC PROC [pane: Pane] RETURNS [parent: Pane, index: CARDINAL--origin 0--] =
{RETURN [pane.parent, pane.myIndex]};
Expand: PUBLIC PROC [pane: Pane, index: CARDINAL--origin 0--, cmdData, browserData: REF ANY, button: Menus.MouseButton, ctl, shift, paint: BOOLEAN] =
BEGIN
directionFilter: Graphs.DirectionFilter = SELECT cmdData FROM
$Outgoing => [TRUE, FALSE, TRUE],
$Both => Graphs.allDirections,
$Incoming => [TRUE, TRUE, FALSE],
ENDCASE => ERROR;
childPane: Pane;
childVertex: Vertex;
IF pane.expanded = index THEN RETURN;
IF pane.expanded IN [0 .. pane.length) THEN Contract[pane, pane.expanded, NIL, NIL, button, ctl, shift, FALSE];
pane.expanded ← index;
childVertex ← GetChildVertex[pane, index];
IF (childVertex.class.GetNeighborCount = NIL) OR (childVertex.class.GetNeighborCount[childVertex, directionFilter] > 0) THEN
BEGIN
childPane ← MakePane[browser: pane.browser, vertex: childVertex, parent: pane, myIndex: index, directionFilter: directionFilter, paint: paint];
pane.browser.panes ← CONS[childPane, pane.browser.panes];
END
ELSE IF paint THEN ViewerOps.PaintViewer[viewer: pane.browser.container, hint: client];
END;
Contract: PUBLIC PROC [pane: Pane, index: CARDINAL--origin 0--, cmdData, browserData: REF ANY, button: Menus.MouseButton, ctl, shift, paint: BOOLEAN] =
BEGIN
panes: LIST OF Pane;
IF index # pane.expanded THEN RETURN;
FOR panes ← pane.browser.panes, panes.rest WHILE panes.first # pane DO
ViewerOps.DestroyViewer[viewer: panes.first.asViewer, paint: FALSE];
ENDLOOP;
pane.browser.panes ← panes;
pane.expanded ← pane.length;
IF paint THEN ViewerOps.PaintViewer[viewer: pane.browser.container, hint: client];
END;
Setup: PROC =
BEGIN
ViewerOps.RegisterViewerClass[flavor: paneFlavor, class: paneClass];
MJSContainers.RegisterClass[viewerFlavor: $GraphBrowser, class: NEW [MJSContainers.MJSContainerClassRep ← [adjust: SizeChange]]];
END;
Setup[];
END.