More fun with Modules!

BlitzMax Modules Forums/Brucey's Modules/More fun with Modules!

Requirements
This is fun little app requires the Graphviz + GraphvizMax2D modules.

What does it do?
Given a module name, it displays a scrollable, zoomable, clickable Type hierarchy of said module in a window, also indicating any external types it uses.

Usage
Right-mouse drag - move view
Scrollwheel - zoom in/out
Left-mouse click - select node.

Possible Improvements
When a node is clicked on, it would be nice to see the bbdoc for it ;-)


Anyhoo... here it is.
SuperStrict

Framework BaH.Graphviz
Import BaH.GraphvizMax2D

Import BRL.StandardIO
Import BRL.glmax2d
Import BRL.MaxUtil

Global nodeTypes:String[] = [ "/", "Module","Type", "Const","Field","Global","Method","Function","Keyword"]

' we won't show these babies!
Global ignoreMods:String[] = [ "brl.mod", "pub.mod" ]

' Change this for you favourite module
Local file:String = ModuleSource("bah.graphviz")


processFile(file)

doVisuals()

End


Function processFile(file:String)

	Local text:String = LoadText(file)
	
	Local inrem:Int
	
	Local docPath:String = file
	Local typePath:String
		
	
	For Local line:String = EachIn text.split("~n")
	
		line = line.Trim()
		Local low:String=line.ToLower()
		
		Local i:Int
		Local id:String = ParseIdent( low,i )
			
		If id="end" id:+ ParseIdent( low,i )
		
		If i< low.length And low[i]=Asc(":")
			id:+":"
			i:+1
		EndIf

		If inrem
	
			If id="endrem"
				
				inrem=False
					
			End If	
	
		Else If id="rem"
	
			inrem=True
			
		Else If id="endtype"
	
			If typePath
				docPath=typePath
				typePath=""
			EndIf
			
		Else If id="import" Or id="include"

			Local parsed:String = ParseString( line,i )
			If parsed Then
				Local p$=ExtractDir( file )+"/"+parsed
			'	
				If ExtractExt( p ).ToLower()="bmx"
					processFile p
				EndIf
			Else
				Local imp:String = line[i..].Trim().ToLower()
				If FindModule(imp) Then
					Local src:String = ModuleSource(imp)
					Local ig:Int = False
					For Local ignore:String = EachIn ignoreMods
						If src.find(ignore) >= 0 Then
							ig = True
						End If
					Next
					If Not Ig Then
						If Not TNode._pathMap.ValueForKey( ModuleSource(imp) )
							ProcessFile(ModuleSource(imp))
						End If
					End If
				End If
			End If
		Else
		
			Local kind:String, proto:String
	
			For Local t$=EachIn nodeTypes
				If id<>t.ToLower() Continue
				kind=t
				proto=line
				id=ParseIdent( line,i )
				Exit
			Next
	
	
			If kind

				Local path$
	
				Select kind
				Case "Type"
					If typePath Throw "Type path already set"
					typePath=docPath
					docPath:+"/"+id
					path=docPath
				Case "Module"
					path=docPath
				Default
					If Not docPath Throw "No doc path"
					path=docPath+"/"+id
				End Select
				
				Local i:Int=proto.Find( ")=" )
				If i<>-1 
					proto=proto[..i+1]
					If id.StartsWith( "Sort" ) proto:+" )"	'lazy!!!!!
				EndIf
				i=proto.Find( "=New" )
				If i<>-1
					proto=proto[..i]
				EndIf

				Local node:TNode=TNode.Create( id,path,kind )
				If Not node
					Return
				End If

				node.setProto(proto)

			End If
		End If
	
	Next
End Function


Function FindModule:Int(impMod:String)
	Global mods:TList = EnumModules()
	
	impMod = impMod.ToLower()
	
	For Local modid$=EachIn mods
		If impMod = modid Then
			Return True
		End If
	Next

	Return False
End Function

Function IsAlphaChar:Int( char:Int )
	Return (char>=Asc("A") And char<=Asc("Z")) Or (char>=Asc("a") And char<=Asc("z")) Or (char=Asc("_"))
End Function

Function IsDecChar:Int( char:Int )
	Return (char>=Asc("0") And char<=Asc("9"))
End Function

Function IsIdentChar:Int( char:Int )
	Return IsAlphaChar( char ) Or IsDecChar( char )
End Function

Function IsHexChar:Int( char:Int )
	Return IsDecChar( char ) Or (char>=Asc("A") And char<=Asc("F")) Or (char>=Asc("a") And char<=Asc("f"))
End Function

Function IsBinChar:Int( char:Int )
	Return (char>=Asc("0") And char<=Asc("1"))
End Function

Function IsPunctChar:Int( char:Int )
	Select char
	Case Asc("."),Asc(","),Asc(";"),Asc(":" ),Asc("!"),Asc("?") Return True
	End Select
End Function

Function ParseWS:String( t:String,i:Int Var )
	Local i0:Int=i
	While i<t.length And t[i]<=32
		i:+1
	Wend
	Return t[i0..i]
End Function

Function ParseIdent:String( t:String, i:Int Var )
	ParseWS t,i
	Local i0:Int=i
	If i<t.length And IsAlphaChar( t[i] )
		i:+1
		While i<t.length And (IsIdentChar( t[i] ) Or t[i]=Asc("."))
			i:+1
		Wend
	EndIf
	Return t[i0..i]
End Function

Function ParseString:String( t:String,i:Int Var )
	ParseWS t,i
	If i=t.length Or t[i]<>Asc("~q") Return Null
	i:+1
	Local i0:Int=i
	While i<t.length And t[i]<>Asc("~q" )
		i:+1
	Wend
	Local q$=t[i0..i]
	If i<t.length i:+1
	Return q
End Function

Type TNode

	Field id$			'eg: BRL.Audio
	Field path$		'eg: Modules/Audio/Audio"
	Field kind$		'eg: "Module", "Function", "Type" etc
	
	Field proto$		'eg: Function LoadImage(...)
	Field bbdoc$		'eg: Load an image (shortdesc?)
	Field returns$	'eg: A new image
	Field about$		'eg: blah etc blah (longdesc?)
	Field params:TList	'eg: [x - the x coord, y - the y coord]
	
	Field docDir$		'eg: ../mod/brl.mod/max2d.mod/doc
	Field example$	'eg: LoadImage.bmx (path)
	
	Field children:TList=New TList
	
	Field superType:TNode
	Field superTypeName:String
	
	Function Create:TNode( id$,path$,kind$ )

		Local t:TNode=TNode( _pathMap.ValueForKey( path ) )
		
		If t
			If t.kind<>"/" Return Null
		Else
			t=New TNode
			_pathMap.Insert path,t
		EndIf

		t.id=id
		t.path=path
		t.kind=kind
		
		Local q:TNode=t
		
		Repeat
			If path="/" Exit
			path=ExtractDir( path )

			Local p:TNode=TNode( _pathMap.ValueForKey( path ) )
			If Not p
				p=New TNode
				p.id=StripDir(path)
				p.path=path
				p.kind="/"
				p.children.AddLast q
				_pathMap.Insert path,p
				q=p

			Else

				' is there a mod on this level? rather add it to the module...
				Local done:Int
				For Local m:TNode = EachIn p.children
					If m.kind = "Module" Then
						m.children.AddLast q
						done = True
						Exit
					End If
				Next
				If Not done
					p.children.AddLast q
				End If
				Exit
			EndIf
		Forever
		
		Return t
	End Function
	
	Method setProto(p:String)
		proto = p
		If kind = "Type" Then
			Local i:Int = p.find("Extends")
			If i > 0 Then
				superTypeName = p[i..].split(" ")[1]
				_needSuper.insert(path, Self)
			End If
		End If
	End Method
	
	Global _pathMap:TMap=New TMap
	Global _needSuper:TMap = New TMap
End Type

Function processDeep(sub:TGVSubGraph, parent:TNode)

	For Local subNode:TNode = EachIn parent.children
		If subNode.kind = "Type" Then

			Local gNode:TGVNode = sub.addNode(subNode.id)
			gNode.setAttr(ATTR_NODE_SHAPE, "box")
			gNode.setAttr("style", "filled")
			gNode.setAttr(ATTR_NODE_FILLCOLOR, "lemonchiffon")
			If subNode.id = "wxEvent" Then
				gNode.setAttr("rank", "min")
			End If
			If subNode.id = "wxFontMapper" Then
				gNode.setAttr("rank", "max")
			End If
		End If

		If Not subNode.children.isEmpty() Then
			processDeep(sub, subNode)
		End If
	Next


End Function

Function doVisuals()
	
	Local renderer:TGVGraphviz = TGVGraphviz.Create(1000, 600)
	
	
	Local graph:TGVGraph = TGVGraph.Create()
	graph.setAttr("clusterrank", "local")
	'graph.setAttr("rankdir", "LR")
	'graph.setAttr("overlap", "false")
	'graph.setAttr("sep", "1")
	'graph.setAttr("size", "6,6")
	
	' modules first...
	For Local node:TNode = EachIn TNode._pathMap.Values()
		If node.kind = "Module" Then

			Local sub:TGVSubGraph = graph.addSubGraph("cluster_" + node.id)
			sub.setAttr(ATTR_LABEL, node.id)

			sub.setAttr("rank", "sink")
			sub.setAttr("style", "filled")
			sub.setAttr(ATTR_NODE_FILLCOLOR, "honeydew")
			
			processDeep(sub, node)
		
		
		End If
	Next
	
	' initial type load...
	For Local node:TNode = EachIn TNode._pathMap.Values()
		If node.kind = "Type" Then
		
			Local subType:TGVNode = graph.findNode(node.id)
			
			If Not subType Then
				Local gNode:TGVNode = graph.addNode(node.id)
				gNode.setAttr(ATTR_NODE_SHAPE, "box")
				gNode.setAttr("style", "filled")
				gNode.setAttr(ATTR_NODE_FILLCOLOR, "lemonchiffon")
			End If
		
		End If
	Next
	
	' Any super types?
	For Local node:TNode = EachIn TNode._needSuper.Values()
		If node.kind = "Type" Then
		
			Local subType:TGVNode = graph.findNode(node.id)
			Local superType:TGVNode = graph.findNode(node.superTypeName)
			
			If Not superType Then
				superType = graph.AddNode(node.superTypeName)
				superType.setAttr(ATTR_FONTCOLOR, "red")
				superType.setAttr(ATTR_NODE_SHAPE, "box")
			End If
		
			' make the join
			Local edge:TGVEdge = graph.addEdge(superType, subType)
			edge.setAttr("dir", "back")
			edge.setAttr("arrowtail", "empty")
			
		End If
	Next
	
	' now to construct the graph..
	renderer.buildGraph(graph)
	
	' and display it..
	showGraph(renderer)

End Function

Function showGraph(renderer:TGVGraphviz)
	Graphics 1000, 600, 0
	
	SetBlend alphablend
	
	
	renderer.layout("dot")
	renderer.fit(1000, 600)
	
	
	Local mx:Int
	Local my:Int
	Local mz:Int
	Local oldZ:Int
	Local buttons:Int[] = New Int[3]	
	
	While Not KeyDown(KEY_ESCAPE)

		Cls
		
		SetColor(255, 255, 255)
		
		mx = MouseX()
		my = MouseY()
		mz = MouseZ()
		
		' tell graphviz we've moved the mouse
		renderer.mouseMove(mx, my)
		
		For Local i:Int = 1 To 3
			If MouseDown(i)
				If Not buttons[i - 1] Then
					renderer.MouseDown(mx, my, i) ' mouse button press
					buttons[i - 1] = 1
				End If
			Else
				If buttons[i - 1] Then
					buttons[i - 1] = 0
					renderer.mouseUp(mx, my, i) ' mouse button release
				End If
			End If
		Next
		
		If mz <> oldZ Then
			If mz < oldZ Then
				renderer.mouseScroll(mx, my, -1) ' scroll up - zoom in
			Else
				renderer.mouseScroll(mx, my, 1) ' scroll down - zoom out
			End If
			oldZ = mz
		End If
		
		' refresh/redraw the graph
		renderer.refresh()
	
		' draw the current tooltip under the mouse - if there is one!
		renderer.drawTooltip(mx, my + 8)
		
		' get data for selected "node" (for anything else, returns Null)
		Local obj:TGViewObject = renderer.selectedObject(GV_VIEW_NODE)
		
		If obj Then
			SetImageFont(Null)
			SetColor 100, 100, 255
			
			DrawText obj.name, 400, 0
			DrawText obj.skind, 400, 15
			
			For Local i:Int = 0 Until obj.attributes.length
				DrawText obj.attributes[i].name + " : " + obj.attributes[i].value, 400, 60 + i * 15
			Next
		End If
	
		Flip
	Wend
End Function


Nifty usage of your graph modules :)