Property Grid?

BlitzMax Forums/BlitzMax Programming/Property Grid?

You know what's good for a level editor application? A property grid! Now I'm pretty sure ( bad eyesight permitting ) that MaxGUI doesn't have one, but I'm wondering whether anyone has coded one into MaxGUI. I've never really looked at the source of MaxGUI so I have no idea how easy or hard that would be. But if anyone has one in MaxGUI they'd be prepared to share ( for free, or for a charge ) then I'd be very interested.

And yes, I know they're not strictly necessary. I know that a bunch of gadgets on a tab works ok, but I really think a property grid is much nicer if possible.

I made one for my own gui editor, its a bit ugly but it does the job. here you go =)

Note!.. the key handling doesnt work properly :/

proplist.bmx
SuperStrict

Const PROP_TEXT:Int = 0 ' text input
Const PROP_NUM:Int = 1 ' number input
Const PROP_LIST:Int = 2 ' list
Const PROP_ELIST:Int = 3 ' editable list
Const PROP_ILIST:Int = 4 ' icon list

Type TPropertyField
	Field Class:Int
	Field Caption:String
	Field Text:String
	Field Index:Int
	Field Items:String[]
	Field Icons:Int[]
	Field IconStrip:TIconStrip
	Field Owner:TPropertyList
	
	Method StartEdit()
		'Local xx:Int = (GadgetWidth(Owner.gadget) / 2) - 2
		Local xx:Int = (TPropertyList.PROPERTY_WIDTH * (FontSize(Owner.font) - 1)) - FontSize(Owner.font)
		SetGadgetShape( Owner.controls[Class], xx - 3, Index * TPropertyList.PROPERTY_HEIGHT, xx - 1,TPropertyList.PROPERTYFIELD_HEIGHT)
		Select Class
			Case PROP_LIST, PROP_ELIST
				ClearGadgetItems Owner.controls[Class]
				For Local i:Int = 0 Until Items.Length
					AddGadgetItem( Owner.controls[Class], Items[i])
					If Text = Items[i] Then SelectGadgetItem Owner.controls[Class], i
				Next
				If SelectedGadgetItem( Owner.controls[Class]) < 0 Then
					SelectGadgetItem Owner.controls[Class], 0
				EndIf
				
			Case PROP_ILIST
				ClearGadgetItems Owner.controls[Class]				
				SetGadgetIconStrip Owner.controls[Class], IconStrip
				For Local i:Int = 0 Until Items.Length
					AddGadgetItem( Owner.controls[Class], Items[i], 0, Icons[i])
					If Text = Items[i] Then SelectGadgetItem Owner.controls[Class], i
				Next
				If SelectedGadgetItem( Owner.controls[Class]) < 0 Then
					SelectGadgetItem Owner.controls[Class], 0
				EndIf
						
			Case PROP_NUM
				If Text.Length <= 0 Then Text = "0"
		EndSelect	
		Owner.controls[Class].SetText( Text)		
		ShowGadget Owner.controls[Class]
		Select Class
			Case PROP_TEXT, PROP_NUM
				SelectTextAreaText( Owner.controls[Class])
		EndSelect
		ActivateGadget Owner.controls[Class]
	EndMethod
	
	Method EndEdit( store:Int = True)
		Local t:String
		Select Class
			Case PROP_TEXT, PROP_NUM
				t = TextAreaText( Owner.controls[Class])
			Case PROP_LIST, PROP_ELIST, PROP_ILIST
				t = Owner.controls[Class].GetText()
		EndSelect
		If (t <> Text) And store Then
			Text = t
			Owner.UpdateGadget()
		EndIf
		DeselectGadgetItem( Owner.Gadget, Index)
		HideGadget Owner.controls[Class]
	EndMethod	
EndType

Type TPropertyList
	Const PROPERTY_HEIGHT:Int = 14
	Const PROPERTY_WIDTH:Int  = 20
	Const PROPERTYFIELD_HEIGHT:Int = 17

	Field props:TList = New TList
	Field Gadget:TGadget
	Field controls:TGadget[5]
	Field font:TGUIFont
	
	Field lastprop:TPropertyField
	
	Function Create:TPropertyList( x:Int,y:Int,w:Int,h:Int, group:TGadget)
		Local list:TPropertyList = New TPropertyList
		list.Init( x,y, w,h, group)
		Return list
	EndFunction	
	
	Function FilterText:Int( event:TEvent, context:Object)
		If event.ID = EVENT_KEYDOWN Then 
			Local props:TPropertyList = TPropertyList( context)
			If Not props Then Return True
			Select event.Data
				Case KEY_ENTER
					' complete edit
					props.EndEdit()
					Return False
				Case KEY_ESCAPE
					' cancel edit
					props.CancelEdit()
					Return False
				Case KEY_UP
					' complete edit and goto previous property
					If props.lastprop Then
						Local index:Int = props.lastprop.Index - 1
						If index >= 0 Then
							props.lastprop.EndEdit()
							ActivateGadget( props.Gadget)							
							SelectGadgetItem( props.Gadget, index)
							'props.lastprop = TPropertyField( props.Props.ValueAtIndex( index))
							'If props.lastprop Then props.lastprop.StartEdit()
						EndIf
					EndIf
				Case KEY_DOWN
					' complete edit and goto next property
					If props.lastprop Then
						Local index:Int = props.lastprop.Index + 1
						If index < props.lastprop.Owner.Props.Count() Then
							props.lastprop.EndEdit()
							ActivateGadget( props.Gadget)							
							SelectGadgetItem( props.Gadget, index)
							'props.lastprop = TPropertyField( props.Props.ValueAtIndex( index))
							'If props.lastprop Then props.lastprop.StartEdit()
						EndIf
					EndIf			
			EndSelect
		EndIf
		Return True
	EndFunction
	
	Function FilterNum:Int( event:TEvent, context:Object)
		If FilterText( event, context) Then
			Return (event.Data >= Asc("0")) And (event.Data <= Asc("9"))
		EndIf
		Return False
	EndFunction	
	
	Method CancelEdit()
		If lastprop Then
			lastprop.EndEdit( False)
			lastprop = Null
		EndIf	
	EndMethod
	
	Method EndEdit()
		If lastprop Then
			lastprop.EndEdit( True)
			lastprop = Null
		EndIf	
	EndMethod
	
	Method Init( x:Int,y:Int, w:Int,h:Int, group:TGadget)
		
		Gadget = CreateListBox( x,y, w,h, group)
		font = LoadGuiFont( "Lucida Console", 10)
		SetGadgetFont( Gadget, font)
		
		controls[PROP_TEXT] = CreateTextArea( 0,-64, 32,PROPERTYFIELD_HEIGHT, Gadget)
		SetGadgetFilter controls[PROP_TEXT], FilterText, Self
		
		controls[PROP_NUM] = CreateTextArea( 0,-64, 32,PROPERTYFIELD_HEIGHT, Gadget)
		SetGadgetFilter controls[PROP_NUM], FilterNum, Self
		
		controls[PROP_LIST] = CreateComboBox( 0,-64, 32,PROPERTYFIELD_HEIGHT, Gadget)
		controls[PROP_ELIST] = CreateComboBox( 0,-64, 32,PROPERTYFIELD_HEIGHT, Gadget, COMBOBOX_EDITABLE)
		controls[PROP_ILIST] = CreateComboBox( 0,-64, 32,PROPERTYFIELD_HEIGHT, Gadget)		

		SetGadgetBorder( controls[PROP_TEXT], BORDER_THIN)
		SetGadgetBorder( controls[PROP_NUM], BORDER_THIN)
		
		HideGadget controls[PROP_TEXT]
		HideGadget controls[PROP_NUM]
		HideGadget controls[PROP_LIST]
		HideGadget controls[PROP_ELIST]
		HideGadget controls[PROP_ILIST]
	EndMethod
	
	Method AddTextProperty:TPropertyField( caption:String, text:String = Null)
		Local prop:TPropertyField = New TPropertyField
		prop.Class = PROP_TEXT
		prop.Caption = caption
		prop.Text = text
		prop.Owner = Self
		prop.Index = props.Count()
		props.AddLast( prop)
		Return prop
	EndMethod
	
	Method AddNumberProperty:TPropertyField( caption:String, text:String = "0")
		Local prop:TPropertyField = New TPropertyField
		prop.Class = PROP_NUM
		prop.Caption = caption
		prop.Text = text
		prop.Owner = Self
		prop.Index = props.Count()
		props.AddLast( prop)
		Return prop
	EndMethod	
	
	Method AddListProperty:TPropertyField( caption:String, items:String[], index:Int = 0)
		Local prop:TPropertyField = New TPropertyField
		prop.Class = PROP_LIST
		prop.Caption = caption
		prop.Items = items
		If index < 0 Then 
			prop.Text = items[0]
		ElseIf index >= items.Length Then 
			prop.Text = items[items.Length-1]
		Else
			prop.Text = items[index]
		EndIf
		prop.Index = props.Count()
		prop.Owner = Self
		props.AddLast( prop)
		Return prop
	EndMethod
	
	Method AddEditListProperty:TPropertyField( caption:String, items:String[], index:Int = 0)
		Local prop:TPropertyField = New TPropertyField
		prop.Class = PROP_ELIST
		prop.Caption = caption
		prop.Items = items
		If index < 0 Then 
			prop.Text = items[0]
		ElseIf index >= items.Length Then 
			prop.Text = items[items.Length-1]
		Else
			prop.Text = items[index]
		EndIf
		prop.Index = props.Count()
		prop.Owner = Self
		props.AddLast( prop)
		Return prop
	EndMethod
	
	Method AddIconListProperty:TPropertyField( caption:String, items:String[], iconstrip:TIconStrip, icons:Int[] = Null, index:Int = 0)
		Local prop:TPropertyField = New TPropertyField
		prop.Class = PROP_ILIST
		prop.Caption = caption
		prop.Items = items
		prop.IconStrip = iconstrip
		prop.Icons = icons
		If index < 0 Then 
			prop.Text = items[0]
		ElseIf index >= items.Length Then 
			prop.Text = items[items.Length-1]
		Else
			prop.Text = items[index]
		EndIf		
		prop.Index = props.Count()
		prop.Owner = Self
		props.AddLast( prop)
		Return prop
	EndMethod	
	
	Method UpdateGadget()
		Local index:Int = SelectedGadgetItem( Gadget)
		ClearGadgetItems( Gadget)
		For Local prop:TPropertyField = EachIn props
			Local text:String = LSet( prop.Caption,PROPERTY_WIDTH) + "|"
			If prop.Text.Length > 0 Then
				If prop.Text.Length > PROPERTY_WIDTH Then
					' downsize string
					AddGadgetItem( Gadget, text + prop.Text[..PROPERTY_WIDTH-3] + "...", 0,-1,"", prop)
				Else
					AddGadgetItem( Gadget, text + RSet( prop.Text, PROPERTY_WIDTH), 0,-1,"", prop)
				EndIf
			Else
				AddGadgetItem( Gadget, text + RSet( "", PROPERTY_WIDTH), 0,-1,"", prop)
			EndIf
		Next
		SelectGadgetItem( Gadget, index)
	EndMethod
	
	Method ProcessEvent:Int( event:TEvent)
		Select event.ID
			Case EVENT_GADGETSELECT
				Select event.Source
					Case Gadget
						EndEdit()
						Local prop:TPropertyField = TPropertyField( EventExtra())
						If prop Then
							prop.StartEdit()
							lastprop = prop
						EndIf
						Return True
				EndSelect
				
				
			Case EVENT_GADGETACTION
				Select event.Source
					Case Gadget
						EndEdit()
						Return True
						
					Case Controls[PROP_LIST], Controls[PROP_ELIST]
						EndEdit()
						Return True
				EndSelect	
		EndSelect
		Return False
	EndMethod
EndType


It uses this to set the border on some of the gadgets WIN32 only (just comment out any SetGadgetBorder function calls if you dont want it)
GadgetBorder.bmx
SuperStrict

?MacOS
DebugLog "GadgetBorder.bmx doesnt support MacOS"
?Linux
DebugLog "GadgetBorder.bmx doesnt support Linux"
?

Private

?Win32
Const SWP_DRAWFRAME:Int = 32
Const SWP_FRAMECHANGED:Int = 32
Const SWP_HIDEWINDOW:Int = 128
Const SWP_NOACTIVATE:Int = 16
Const SWP_NOCOPYBITS:Int = 256
Const SWP_NOMOVE:Int = 2
Const SWP_NOSIZE:Int = 1
Const SWP_NOREDRAW:Int = 8
Const SWP_NOZORDER:Int = 4
Const SWP_SHOWWINDOW:Int = 64
Const SWP_NOOWNERZORDER:Int = 512
Const SWP_NOREPOSITION:Int = 512
Const SWP_NOSENDCHANGING:Int = 1024
Const SWP_DEFERERASE:Int = 8192
Const SWP_ASYNCWINDOWPOS:Int =  16384

Const RDW_ERASE:Int = 4
Const RDW_FRAME:Int = 1024
Const RDW_INTERNALPAINT:Int = 2
Const RDW_INVALIDATE:Int = 1
Const RDW_NOERASE:Int = 32
Const RDW_NOFRAME:Int = 2048
Const RDW_NOINTERNALPAINT:Int = 16
Const RDW_VALIDATE:Int = 8
Const RDW_ERASENOW:Int = 512
Const RDW_UPDATENOW:Int = 256
Const RDW_ALLCHILDREN:Int = 128
Const RDW_NOCHILDREN:Int = 64

Const WS_BORDER:Int = $800000 ' thin border
Const WS_EX_CLIENTEDGE:Int = 512 ' 3d border

Extern "Win32"
	Function GetWindowLong:Int( hwnd:Int, index:Int) = "GetWindowLongA@8"
	Function SetWindowLong:Int( hwnd:Int, index:Int, value:Int) = "SetWindowLongA@12"
	Function SetWindowPos:Int( hwnd:Int, hwndafter:Int, x:Int,y:Int, cx:Int,cy:Int, flags:Int)
	Function RedrawWindow:Int( hwnd:Int, rect:Byte Ptr, region:Int, flags:Int)
EndExtern
?

Public

Const BORDER_NONE:Int = 0
Const BORDER_3D:Int = 1
Const BORDER_THIN:Int = 2

Function SetGadgetBorder( gadget:TGadget, flags:Int)
	?Win32
	Local hwnd:Int = gadget.Query( QUERY_HWND)
	Local style:Int = GetWindowLong( hwnd, GWL_STYLE)
	Local exstyle:Int = GetWindowLong( hwnd, GWL_EXSTYLE)	
	Select flags
		Case BORDER_NONE
			' remove both border types
			If style & WS_BORDER Then SetWindowLong( hwnd, GWL_STYLE, style & ~WS_BORDER)
			If exstyle & WS_EX_CLIENTEDGE Then SetWindowLong( hwnd, GWL_EXSTYLE, exstyle & ~WS_EX_CLIENTEDGE)
		Case BORDER_THIN
			' add thin border & remove 3d border
			If Not (style & WS_BORDER) Then SetWindowLong( hwnd, GWL_STYLE, style | WS_BORDER)
			If exstyle & WS_EX_CLIENTEDGE Then SetWindowLong( hwnd, GWL_EXSTYLE, exstyle & ~WS_EX_CLIENTEDGE)		
		Case BORDER_3D
			' remove thin border & add 3d border
			If style & WS_BORDER Then SetWindowLong( hwnd, GWL_STYLE, style & ~WS_BORDER)
			If Not (exstyle & WS_EX_CLIENTEDGE) Then SetWindowLong( hwnd, GWL_EXSTYLE, exstyle | WS_EX_CLIENTEDGE)			
	EndSelect
	' update window frame
	SetWindowPos( hwnd,0, 0,0,0,0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED)
	RedrawWindow( hwnd, Null,0, RDW_INVALIDATE | RDW_NOCHILDREN | RDW_UPDATENOW | RDW_FRAME)
	?
EndFunction


And here is the test source:
test.bmx
SuperStrict

Import "proplist.bmx"
Import "GadgetBorder.bmx"

Const WINDOW_FLAGS:Int = WINDOW_TITLEBAR | WINDOW_RESIZABLE '| WINDOW_MENU

' main window
Global window:TGadget = CreateWindow( "Properties", 64,64, 340,480, Null, WINDOW_FLAGS | WINDOW_CLIENTCOORDS)

Global props:TPropertyList = TPropertyList.Create( 0,0, 340,ClientHeight(window), window)
SetGadgetLayout props.Gadget, 1,1,1,1

Global StandardIconStrip:TIconStrip = LoadIconStrip( "icons.png")

Local LayoutItems:String[] = [ "CENTERED", "ALIGNED", "RELATIVE" ]
Local BooleanItems:String[] = [ "FALSE", "TRUE" ]
Local IconStripItems:String[] = [ "NONE", "STANDARD" ]
Local StandardIconItems:String[] = [ "NONE", "0", "1", "2", "3", "4", "5", "6", "7"]
Local StandardIconIcons:Int[] = [ -1, 0, 1, 2, 3, 4, 5, 6, 7 ]

Local prop:TPropertyField
props.AddNumberProperty( "ID")
props.AddTextProperty( "Caption")
props.AddIconListProperty( "Icon", StandardIconItems, StandardIconStrip, StandardIconIcons)
props.AddListProperty( "IconStrip", IconStripItems)
props.AddTextProperty( "Description")
props.AddListProperty( "Layout.Right", LayoutItems)
props.AddListProperty( "Layout.Left", LayoutItems)
props.AddListProperty( "Layout.Top", LayoutItems)
props.AddListProperty( "Layout.Bottom", LayoutItems)

props.UpdateGadget()

While WaitEvent()
	If Not props.ProcessEvent( CurrentEvent) Then
		Select EventID()			
			Case EVENT_WINDOWCLOSE
				Select EventSource() 
					Case window	Exit
				EndSelect
		EndSelect
	EndIf
Wend

End


EDIT: oh,, and heres the icon file for the test ;)

That's really neat, Grable. Thanks for sharing it!