Here's a handy superflexible spinnergadget!

BlitzMax Forums/BlitzMax GUI Programming/Here's a handy superflexible spinnergadget!

Tadaa!

So, what's so special about it? There's lots of explanation in the tsmartspinner file, but in short:

<> can look like anything, by using your own hooked *external* functions, you never need to touch the smartspinner core

<> horizontal or vertical dragging with polarity choice

<> supports cursors, home, end, page up/dn, mousewheel

<> each spinner can record a value into one of the 10 slots, and one can easily recall such values with a single keypress

<> has one built-in simple displayer, showing the actual value, and with shadow & color options

<> right-mouse-button generates a gadgetaction event

So what can you do with it?

Obviously it's a spinner, so whenever you want a box with a number you can change: smartspinner is your solution.
It has tactful events, telling you when spinning starts, happens and stops. This is convenient when some renderer is constantly rendering according to values defined in these spinners. When changing these values the start and stop events could tell the renderer to temporarily stop rendering and continue when the new value is set.

The real power lies in the external drawing. Rather than bloating the original type with zillions of styles, it now sends out an update event while a canvas is opened for drawing. As long as you make a function that listens to that event, this function can be your new spinnerstyle. Two examples are given, one as a hooked function, and one as a hooked method in a type.

Examples of new drawing functions:
- display the (int) value as a float, by dividing it by 10000 orso
- have a strip of images in a type of a rotating knob, and draw this knob
- have a lookup table with strings in a type and draw these strings (0: "yes", 1: "no", 2: "cancel"), with the rightclick you can click this spinner and so this spinner more or less acts as a normal button of which the content can change.
- make a displayer using your own font


Using it requires barely any brains. If there are no spinners included then it still works, even if you've set a smartspinner to refer to one. Another advantage of using events, so to say..

I created wrapper functions to define things, whereas some would've preferred methods. I purely chose this way to make it look uniform with Blitz' own gadget functions. It isn't much work to make 'em methods nonetheless, allthough I don't think you really gain something with it. (and I don't feel like doing it :P)

So, have a go with it, if there're no obvious flaws, I'll dump it in the archives.

(oh, minor detail: I've tested it only on my 98 system in Bmax1.22 :P)

tsmartspinner_example.bmx
SuperStrict
Include "tsmartspinner_spinners.bmx"

Local window:tgadget=CreateWindow("",300,200,640,480)

Local	MySpinner1:TSmartspinner=CreateSmartspinner(4,32,64,16,window,1)
		SetSmartspinnerExtern MySpinner1,0

Local	MySpinner1b:TSmartspinner=CreateSmartspinner(72,32,64,16,window,1)
		SetSmartspinnerExtern MySpinner1,0
		SetSmartspinnerStyle MySpinner1b,0,0
		
Local 	MySpinner2:TSmartspinner=CreateSmartspinner(4,64,64,24,window,1)
		SetSmartspinnerExtern MySpinner2,2
		SetSmartspinnerStyle MySpinner2,1,1
		
Local 	MySpinner3:TSmartspinner=CreateSmartspinner(4,96,32,32,window,1)
		SetSmartspinnerExtern MySpinner3,1


Local spin2:TSpinner2=New TSpinner2

Repeat
	WaitEvent()
	If EventID()=EVENT_WINDOWCLOSE End
	SetGadgetText window,"Smartspinner - CS_TBL (GC: "+GCMemAlloced()+")"
	
	If EventID()=EVENT_GADGETACTION 		Print"rightclicked     "+EventData()
	If EventID()=EVENT_SMARTSPINNERSTART 	Print"started changing "+EventData()
	If EventID()=EVENT_SMARTSPINNERCHANGING	Print"....changing.... "+EventData()
	If EventID()=EVENT_SMARTSPINNERCHANGED 	Print"stopped changing "+EventData()
	
Forever


tsmartspinner_spinners.bmx
Include "tsmartspinner.bmx"

AddHook EmitEventHook,Spinner1
Function Spinner1:Object(id:Int,data:Object,context:Object)
	Local ev:TEvent=TEvent(data);If ev.id<>EVENT_SMARTSPINNERUPDATE Return data
	Local p:TSmartspinner=TSmartspinner(ev.extra);If p.style<>1 Return data	' each style is to have a unique number
																			' in this case it's: '1'
	p.ext=1 ' 0: the internal number display is shown when this function is done
			'    this can be used to draw an image using a function like this, to serve as
			'    backdrop for the internal number display (you might then want to disable the shadow option though)
			' 1: the internal number display is not shown when this function is done

	' ---------------------------------------------------------------------------------------------------------
	' Everything below is up to you! with p.<field> you can access the fields of the smartspinner
	' (stick to the relevant fields, as shown at the top of the Type!)
	' ---------------------------------------------------------------------------------------------------------

	SetClsColor 128,128,128;Cls			
	Local x:Int=p.canvaswidth/2
	Local y:Int=p.canvasheight/2
	
	SetColor 255,255,255
	DrawLine x,y,x+Sin(p.currentvalue*3)*15,y+Cos(p.currentvalue*3)*15
	DrawOval x-2,y-2,5,5
EndFunction

Type TSpinner2
	
	Function eventhook:Object(id:Int,data:Object,context:Object)
		If TSpinner2(context) TSpinner2(context).ev TEvent(data);Return data	
	EndFunction
	
	Method New()
		AddHook EmitEventHook,eventhook,Self
	End Method
	
	Method Free()
		RemoveHook EmitEventHook,eventhook
		GCCollect()
	End Method
	
	Method ev(event:TEvent)
		If event.id=EVENT_SMARTSPINNERUPDATE
			Local p:TSmartspinner=TSmartspinner(event.extra)
			If p.style<>2 Return ' spinnerstyle: '2'
			
			p.ext=0 ' internal displayer will be shown after this function
			
			' -------------------------------------------------------------------------------------------------
			' Everything below is up to you! with p.<field> you can access the fields of the smartspinner
			' (stick to the relevant fields, as shown at the top of the Type!)
			' -------------------------------------------------------------------------------------------------
			
			SetClsColor 128,64,0;Cls
			SetColor 128,192,255
			DrawRect p.currentvalue,0,4,4
		EndIf
	End Method		
End Type


tsmartspinner.bmx
Const EVENT_SMARTSPINNERUPDATE:Int=13370330
Const EVENT_SMARTSPINNERSTART:Int=13370331
Const EVENT_SMARTSPINNERCHANGING:Int=13370332
Const EVENT_SMARTSPINNERCHANGED:Int=13370333

Type TSmartspinner
Rem

TSmartspinner v1.0 - by CS_TBL, 1st and 2nd Easter day '07


+--------+
| Usage: |
+--------+

Local MySpinner:TSmartspinner=CreateSmartspinner(x,y,width,height,parent,canvasstyle)


+------------------+
| Relevant events: |
+------------------+

EVENT_SMARTSPINNERUPDATE
> Used in external hooked functions, .extra contains the smartspinner object

EVENT_SMARTSPINNERSTART
> emited when the user starts spinning, .source contains the smartspinner, .data contains the currentvalue

EVENT_SMARTSPINNERCHANGING
> emited when the spinner is spinning, .source contains the smartspinner, .data contains the currentvalue

EVENT_SMARTSPINNERCHANGED
> emited when the user stops spinning, .source contains the smartspinner, .data contains the currentvalue

EVENT_GADGETACTION
> emited when the user rightclicks a spinner, .source contains the smartspinner, .data contains the currentvalue


+----------------------+
| (wrapped-)Functions: |
+----------------------+

(wrapper-)functions:

SetSmartspinnerValues smartspinner,value,lowlimit,highlimit,homevalue,endvalue
>	Sets the current value, the low and high limits, and the values for the [home] and [end] keys


SetSmartspinnerColors smartspinner,txtR,txtG,txtB,bckR,bckG,bckB
>	Sets the rgb values for text and background for the default/internal displaystyle


SetSmartspinnerStyle smartspinner,direction,polarity
>	Sets the dragging direction (0 or 1), and the polarity (0 or 1)


SetSmartspinnerShadow smartspinner,state
>	Sets shadow on (1) or off (0) for the default/internal displaystyle


SetSmartspinnerExtern smartspinner,style
>	Sets the spinnerstyle, this number corresponds with a style check in a hooked function or hooked type-method
	This allows another function to take over the drawing process and thus create a wide range of different
	visual styles. Put even something simple as representing the internal int value as a float by manually dividing
	by 10000 in such a function orso. This outsourcing without ever touching the SmartSpinner core is what
	makes it so flexible and easy to use!


SetSmartspinnerDragrange smartspinner,amount
>	Sets the dragrange, this is a divider (so can't be 0) intending to allow larger drag range to change the value
	Essential to avoid having to use a microscope to change a value by one pixel, often a pain in the A$$ in music-
	software.. :S


EnableSmartspinner smartspinner
> Enables the smartspinner gadget


DisableSmartspinner smartspinner
> Disables the smartspinner gadget


ShowSmartspinner smartspinner
> Shows the smartspinner gadget


HideSmartspinner smartspinner
> Hides the smartspinner gadget


Function RelocateSmartspinner smartspinner,x,y
> Relocates the smartspinner gadget


+------------+
| Operating: |
+------------+

leftmouse dragging, horizontally or vertically
rightmouse clicking, emits a gadgetaction event
mousewheel
cursors
page up/dn
home, end
0..9 (choose value from a memory slot)
SHIFT + 0..9 (record value into a memory slot)
Mouse and keys that change the value are influenced by the polarity


+-----------------------------------------------+
| Possible improvements, prolly not done by me: |
+-----------------------------------------------+

- Alternative way to visualise a disabled smartspinner (had some issues with blend/shade modes and drawtext o_O)
- Resize the canvas/spinner
- way more common spinnerstyles
- Extra wrapper functions to change cursor/page/wheel amounts

EndRem

' ---------------------------------------------------------------------------------------------------------

	' relevant values for external drawing
	
	Field value:Int
	
	Field minvalue:Int=-999
	Field maxvalue:Int=999
	Field homevalue:Int=0
	Field endvalue:Int=999
	Field pagevalue:Int=20 ' value to add using PageUp/Dn
	Field cursorvalue:Int=1 ' value to add using the cursors
	Field wheelvalue:Int=1 ' value to add using the mousewheel
	Field dragrange:Int=6 ' a divider, must not be 0 for obvious reasons, allows to work in a less microscopic way
	Field dir:Int=0 ' 0: up-down, 1: left-right
	Field polarity:Int=1 ' 1, -1 (note: 0 and 1 are used as argument in the wrapper function)
	Field drawshadow:Int=0 ' draws a shadow in the default/internal spinnerstyle
	Field style:Int=0 ' 0=internal, the rest is custom
	Field txtR:Int=192
	Field txtG:Int=192
	Field txtB:Int=255
	Field bckR:Int=96
	Field bckG:Int=96
	Field bckB:Int=160
	Field currentvalue:Int
	Field canvaswidth:Int
	Field canvasheight:Int
				

	' internal stuff

	Field parent:tgadget
	Field dragvalue:Int
	Field startdragvalue:Int
	Field canvas:tgadget
	Field lmd:Int
	Field ext:Int
	Field SHIFT:Int
	Field rec:Int[10],record:Int
	Field active:Int=1

	Function eventhook:Object(id:Int,data:Object,context:Object)
		If TSmartspinner(context) TSmartspinner(context).ev TEvent(data);Return data	
	EndFunction
	
	Method New()
		AddHook EmitEventHook,eventhook,Self
	End Method
	
	Method Free()
		RemoveHook EmitEventHook,eventhook
		GCCollect()
	End Method
	
	Method ev(event:TEvent)
	
		If event.source=canvas
		
			If event.id=EVENT_GADGETPAINT update
			
			If event.id=EVENT_MOUSEDOWN
				If event.data=1 ' left mousebutton
					lmd=1
					startdragvalue=delta(event)
					dragvalue=0
					update					
					Send EVENT_SMARTSPINNERSTART
				EndIf
				
				If event.data=2 ' right mousebutton
					Send EVENT_GADGETACTION
				EndIf
				
			EndIf
			
			If event.id=EVENT_MOUSEENTER
				ActivateGadget canvas
			EndIf

			If event.id=EVENT_MOUSELEAVE
				record=0
				update
				ActivateGadget parent
			EndIf
			
			If event.id=EVENT_MOUSEUP
				If event.data=1
					lmd=0
					value:+delta(event)-startdragvalue
					value=Limit(value,minvalue,maxvalue)
					update
					Send EVENT_SMARTSPINNERCHANGED
				EndIf
			EndIf
			
			If event.id=EVENT_MOUSEMOVE
				If lmd
					dragvalue=delta(event)-startdragvalue
					value=Limit(value,minvalue,maxvalue)
					update
					Send EVENT_SMARTSPINNERCHANGING
				EndIf
			EndIf
			
			If event.id=EVENT_MOUSEWHEEL
				value:-Sgn(event.data)*wheelvalue*polarity
				value=Limit(value,minvalue,maxvalue)
				update
				Send EVENT_SMARTSPINNERCHANGED
			EndIf
			
			If event.id=EVENT_KEYDOWN Or event.id=EVENT_KEYREPEAT
			
				Select event.data
					Case 33 ' pgup
						value:-pagevalue*polarity
					Case 34 ' pgup
						value:+pagevalue*polarity
					Case 35 ' end
						value=endvalue
					Case 36 ' home
						value=homevalue
					Case 37 ' left
						value:-cursorvalue*polarity
					Case 39 ' right
						value:+cursorvalue*polarity
					Case 38 ' up
						value:-cursorvalue*polarity
					Case 40 ' down
						value:+cursorvalue*polarity
					Case 160,161
						SHIFT=1
				EndSelect
				
				If event.data>=48 And event.data<=57 ' 0..9
					If SHIFT
						record=1
						rec[event.data-48]=value
					EndIf
					value=rec[event.data-48]
				EndIf
				
				value=Limit(value,minvalue,maxvalue)
				update
				Send EVENT_SMARTSPINNERCHANGING
			EndIf

			If event.id=EVENT_KEYUP
				Select event.data
					Case 160,161
						SHIFT=0
						record=0
				EndSelect
				update
				Send EVENT_SMARTSPINNERCHANGED
			EndIf
			
		EndIf
	End Method
	
	Method Send(what:Int)
		MakeCurrentvalue
		Local newevent:TEvent=New TEvent
		newevent.id=what
		newevent.source=Self
		newevent.data=currentvalue
		EmitEvent newevent
		newevent=Null
		GCCollect	
	End Method
	
	Method MakeCurrentvalue()
		currentvalue=value
		If lmd currentvalue:+dragvalue
		currentvalue=Limit(currentvalue,minvalue,maxvalue)	
	End Method
	
	Method Limit:Int(v:Int,l:Int,h:Int)
		If v<l Return l
		If v>h Return h
		Return v
	End Method
	
	Method delta:Int(e:TEvent)
		Select dir
			Case 0 Return (e.y*polarity)/dragrange
			Case 1 Return (e.x*polarity)/dragrange
		End Select
	End Method

	
	Method update()
		ext=0
		
		SetGraphics CanvasGraphics(canvas)
			SetClsColor bckR,bckG,bckB;Cls		
			
			MakeCurrentvalue
			
			Local newevent:TEvent=New TEvent
			newevent.id=EVENT_SMARTSPINNERUPDATE
			newevent.extra=Self
			EmitEvent newevent
			newevent=Null
			GCCollect
			
			If Not ext
			
				If drawshadow
					SetColor bckR/3,bckG/3,bckB/3
					DrawRect 0,0,canvaswidth,1
					DrawRect 0,1,1,canvasheight-1
					
					SetColor bckR/1.5,bckG/1.5,bckB/1.5
					DrawRect 1,1,canvaswidth-1,1
					DrawRect 1,2,1,canvasheight-2
				EndIf
				
				SetColor txtR,txtG,txtB
				If lmd SetColor txtR*1.5,txtG*1.5,txtB*1.5
				
				DrawText currentvalue,canvaswidth/2-TextWidth(currentvalue)/2,canvasheight/2-TextHeight(currentvalue)/2
			EndIf
			
			If record
				SetColor 64,0,0;	DrawOval 2,2,7,7
				SetColor 255,0,0;	DrawOval 3,3,5,5
			EndIf
			
			If Not active
				SetColor 160,160,160
				For Local t:Int=0 To canvasheight-1 Step 2
					DrawRect 0,t,canvaswidth,1
				Next
			EndIf
		Flip
	End Method
	
End Type

Function CreateSmartspinner:TSmartspinner(x:Int,y:Int,w:Int,h:Int,parent:tgadget,canvasstyle:Int=0)
	If parent<>Null
		Local a:TSmartspinner=New TSmartspinner
			a.canvas=CreateCanvas(x,y,w,h,parent,canvasstyle)
			a.canvaswidth=ClientWidth(a.canvas)
			a.canvasheight=ClientHeight(a.canvas)
			a.parent=parent
		Return a
	Else
		RuntimeError "Error: parent not found! CreateSmartspinner("+x+", "+y+", "+w+", "+h+", ???, "+canvasstyle+")"
	EndIf
End Function

Function SetSmartspinnerValues(smartspinner:TSmartspinner,v:Int,low:Int,high:Int,homevalue:Int,endvalue:Int)
	If smartspinner<>Null
		smartspinner.value=v
		smartspinner.minvalue=low
		smartspinner.maxvalue=high
		smartspinner.homevalue=homevalue
		smartspinner.endvalue=endvalue
		smartspinner.update
	Else
		RuntimeError "Error: smartspinner not found! SetSmartspinnerValues(???, "+v+", "+low+", "+high+", "+homevalue+", "+endvalue+")"
	EndIf
End Function

Function SetSmartspinnerColors(smartspinner:TSmartspinner,txtR:Int, txtG:Int, txtB:Int, bckR:Int, bckG:Int, bckB:Int)
	If smartspinner<>Null
		smartspinner.bckR=bckR
		smartspinner.bckG=bckG
		smartspinner.bckB=bckB
		smartspinner.txtR=txtR
		smartspinner.txtG=txtG
		smartspinner.txtB=txtB
		smartspinner.update
	Else
		RuntimeError "Error: smartspinner not found! SetSmartspinnerColors(???, "+txtR+", "+txtG+", "+txtB+", "+bckR+", "+bckG+", "+bckB+")"
	EndIf
End Function

Function SetSmartspinnerStyle(smartspinner:TSmartspinner,direction:Int, polarity:Int)
	If smartspinner<>Null
		polarity:&1
		smartspinner.dir=direction
		smartspinner.polarity=-1+polarity*2
	Else
		RuntimeError "Error: smartspinner not found! SetSmartspinnerStyle(???, "+direction+", "+polarity+")"
	EndIf
End Function

Function SetSmartspinnerShadow(smartspinner:TSmartspinner,state:Int)
	If smartspinner<>Null
		smartspinner.drawshadow=state
	Else
		RuntimeError "Error: smartspinner not found! SetSmartspinnerShadow(???, "+state+")"
	EndIf
End Function

Function SetSmartspinnerExtern(smartspinner:TSmartspinner,style:Int)
	If smartspinner<>Null
		smartspinner.style=style
	Else
		RuntimeError "Error: smartspinner not found! SetSmartspinnerExtern(???, "+style+")"
	EndIf
End Function

Function SetSmartspinnerDragrange(smartspinner:TSmartspinner,amount:Int)
	If smartspinner<>Null
		If amount<1
			RuntimeError "Error: dragrange <1 SetSmartspinnerDragrange(smartspinner, "+amount+")"
		Else
			smartspinner.dragrange=amount
		EndIf
	Else
		RuntimeError "Error: smartspinner not found! SetSmartspinnerDragrange(???, "+amount+")"
	EndIf
End Function

Function EnableSmartspinner(smartspinner:TSmartspinner)
	If smartspinner<>Null
		smartspinner.active=1
		smartspinner.update
		EnableGadget smartspinner.canvas		
	Else
		RuntimeError "Error: smartspinner not found! EnableSmartspinner(???)"
	EndIf
End Function

Function DisableSmartspinner(smartspinner:TSmartspinner)
	If smartspinner<>Null
		smartspinner.active=0
		smartspinner.update
		DisableGadget smartspinner.canvas
	Else
		RuntimeError "Error: smartspinner not found! DisableSetSmartspinner(???)"
	EndIf
End Function

Function ShowSmartspinner(smartspinner:TSmartspinner)
	If smartspinner<>Null
		ShowGadget smartspinner.canvas
	Else
		RuntimeError "Error: smartspinner not found! ShowSmartspinner(???)"
	EndIf
End Function

Function HideSmartspinner(smartspinner:TSmartspinner)
	If smartspinner<>Null
		HideGadget smartspinner.canvas
	Else
		RuntimeError "Error: smartspinner not found! HideSmartspinner(???)"
	EndIf
End Function

Function RelocateSmartspinner(smartspinner:TSmartspinner,x:Int,y:Int)
	If smartspinner<>Null
		SetGadgetShape smartspinner.canvas,x,y,GadgetWidth(smartspinner.canvas),GadgetHeight(smartspinner.canvas)
	Else
		RuntimeError "Error: smartspinner not found! HideSmartspinner(???)"
	EndIf
End Function


I got "attempt to access field or method of a null object" in tsmartspinner.bmx ...

Method ev(event:TEvent)

If event.source=canvas
... on this line.

Odd, here everything works like a charm. I might be trying 1.24 then.

Do you get this when running the given stuff?

I just wrapped a If event <> Null ... EndIf on these ev methods and now everything works.

But did you get that error with my example program?

Yes.