Tri-state control widget

A place for you to show off what you have made with LiveCode

Moderators: FourthWorld, heatherlaine, Klaus, kevinmiller, robinmiller

Post Reply
stam
Posts: 2686
Joined: Sun Jun 04, 2006 9:39 pm
Location: London, UK

Tri-state control widget

Post by stam » Thu Aug 03, 2023 2:29 am

Hi all,

Following recent efforts to create a tri-state control with groups (which worked well) I applied this to the new script widget facility in LC10 DP5.

It did take a couple of days to adapt my scripts to this but I'm happy with the end-result, not in the least because this took about 8-10 hours to complete whereas having to learn LCB and write this in LCB would have taken months... The .lce file is installed as usual with extension manager (Tools menu), will create a new icon in the Toolbar and the widget's properties are available in the IDE's property inspector.

The widget is similar to the existing switch control widget, but offers 3 states instead of 2 (I needed this to indicate 1 of 2 values or unassigned).
Screenshot 2023-08-03 at 01.14.55.png
Many thanks to Heather and team for the really quite helpful lesson on script widgets: https://lessons.livecode.com/m/98525/l/ ... ipt-widget. It would have been good to have a bit more detail about property metadata (ideally the switch value would be an enum rather than textfield, but not sure how to do this.

Widget and source code and more detail available from my GitHub under MIT iicence: https://github.com/stam66/tristate

S.

livecodeali
Livecode Staff Member
Livecode Staff Member
Posts: 192
Joined: Thu Apr 18, 2013 2:48 pm

Re: Tri-state control widget

Post by livecodeali » Thu Aug 03, 2023 9:08 am

Hi stam,

To use an enum editor in the PI for your switchValue property, you just need to return comma delimited `options` as metadata. You can change the label associated with each enum value using `|`, so you might do

Code: Select all

/* Metadata */
getProp propertyMetadata[pProperty]
	switch pProperty
		case "switchValue"
			return { \
				"editor": "com.livecode.pi.enum", \
				"options": "Left|-1,Centre|0,Right|1", \
				"default": 0, \
				"section": "Basic", \
				"label": "Switch Value" \
			}
		break
Cheers!
Ali

stam
Posts: 2686
Joined: Sun Jun 04, 2006 9:39 pm
Location: London, UK

Re: Tri-state control widget

Post by stam » Thu Aug 03, 2023 2:51 pm

livecodeali wrote:
Thu Aug 03, 2023 9:08 am
Hi stam,

To use an enum editor in the PI for your switchValue property, you just need to return comma delimited `options` as metadata. You can change the label associated with each enum value using `|
Thanks Ali, very much appreciated (and on that note, is there a reference to look these things up somewhere?).

However, I can't quite get it to work... Using your example I am able to show the property as a dropdown menu but the value it assigns is the whole text, not the label (i.e. the popup shows Left|-1 which of course doesn't work; and when changing the state of the widget, it shows the actual value, not the label (i.e. the pop up just shows -1, 0 or 1):
Screenshot 2023-08-03 at 14.39.12.png

This is my declaration in the propertyMetadata handler:

Code: Select all

case "switchValue"
            return { \
                  "default": "Centre|0", \
                  "label": "Switch position", \
                  "editor": "com.livecode.pi.enum", \
                  "options": "Left|-1,Centre|0,Right|1", \
                  "section": "Basic" }
            break
S.
--------------------------------------
EDIT: Never mind, I refactored the code significantly and have changed the options for 'switchValue' to left/center/right insisted of -1/0/1 - using the enum now works just fine (seems the label option didn't work for me...)
I'll upload a revision to GitHub for anyone wanting this... Thanks once again for the advice with enum.

livecodeali
Livecode Staff Member
Livecode Staff Member
Posts: 192
Joined: Thu Apr 18, 2013 2:48 pm

Re: Tri-state control widget

Post by livecodeali » Thu Aug 03, 2023 7:52 pm

Oh strange - that should work. Possibly a bug- either that or I've forgotten the correct syntax for it!

stam
Posts: 2686
Joined: Sun Jun 04, 2006 9:39 pm
Location: London, UK

Re: Tri-state control widget

Post by stam » Mon Aug 21, 2023 3:53 am

I've updated the widget with some corrections by Bernd - I've also added a new feature that I needed for my own project - inline labels:
Screenshot 2023-08-21 at 02.55.50.png
Labels are shown or hidden with the 'hasLabels' property of the widget.
There are 2 1-char labels for the centre position and a larger label for left or right positioning. All texts are set in the 'content' section of the property inspector. The text size scales on widget height, within the constraint of a min and max text size that are set in the 'text' section of the property inspector; the textColor depends on luminosity of backgroundColor (ie dark text on light background and vice versa).
Vertical alignment isn't perfect and sometimes needs a slight 1-2 pixel nudget in height - I may well change the labels to buttons to be rid of this issue, but if anyone has any suggestion, would be grateful!
property inspector panels.jpg
The scriptOnlyStack and the built widget require LC 10 DP 5, and are available here: https://github.com/stam66/tristate



For those that don't have access to script widgets, you can use it as a normal group with the script:

Code: Select all

local sHasCreatedVisuals, sMouseX, sClickedIndicator
local sBackroundID, sIndicatorID, sTextLeftID, sTextRightID, sTextLongID

########################### PROPERTIES ###########################
// rightColor
getProp rightColor
	local tColor
	put the rightColor of me into tColor
	if tColor is empty then put "#C90076" into tColor
	return tColor
end rightColor

setProp rightColor pColor
	set the rightColor of me to pColor
	updateVisualControls
end rightColor

// leftColor
getProp leftColor
	local tColor
	put the leftColor of me into tColor
	if tColor is empty then put "#2986CC" into tColor
	return tColor
end leftColor

setProp leftColor pColor
	set the leftColor of me to pColor
	updateVisualControls
end leftColor

// centerColor
getProp centerColor
	local tColor
	put the centerColor of me into tColor
	if tColor is empty then put "#F4F6F7" into tColor
	return tColor
end centerColor

setProp centerColor pColor
	set the centerColor of me to pColor
	updateVisualControls
end centerColor

// indicatorColor
getProp indicatorColor
	local tColor
	put the indicatorColor of me into tColor
	if tColor is empty then put "#E6E8E8" into tColor
	return tColor
end indicatorColor

setProp indicatorColor pColor
	set the indicatorColor of me to pColor
	updateVisualControls
end indicatorColor

// switchValue
getProp switchValue
	local tValue
	put the switchValue of me into tValue
	if tValue is empty then put "Center" into tValue
	return tValue
end switchValue

setProp switchValue pValue
	set the switchValue of me to pValue
	updateIndicatorForValue
end switchValue

// left text
getProp leftText
	local tText
	put the leftText of me into tText
	if tText is empty then put "Left" into tText
	return tText
end leftText

setProp leftText pText
	set the leftText of me to pText
	updateVisualControls
end leftText

// Left text small
getProp leftTextSmall
	local tText
	put the leftTextSmall of me into tText
	if tText is empty then put "L" into tText
	return tText
end leftTextSmall

setProp leftTextSmall pText
	set the leftTextSmall of me to pText
	updateVisualControls
end leftTextSmall

// right text
getProp rightText
	local tText
	put the rightText of me into tText
	if tText is empty then put "Right" into tText
	return tText
end rightText

setProp rightText pText
	set the rightText of me to pText
	updateVisualControls
end rightText

// Right text small
getProp rightTextSmall
	local tText
	put the rightTextSmall of me into tText
	if tText is empty then put "R" into tText
	return tText
end rightTextSmall

setProp rightTextSmall pText
	set the rightTextSmall of me to pText
	updateVisualControls
end rightTextSmall

// max label textSize
getProp maxTextSize
	local tSize
	put the maxTextSize of me into tSize
	if tSize is empty then put 30 into tSize
	return tSize
end maxTextSize

setProp maxTextSize pSize
	set the maxTextSize of me to pSize
	updateVisualControls
end maxTextSize

// min labeL textSize
getProp minTextSize
	local tSize
	put the minTextSize of me into tSize
	if tSize is empty then put 9 into tSize
	return tSize
end minTextSize

setProp minTextSize pSize
	set the minTextSize of me to pSize
	updateVisualControls
end minTextSize

// hasLabels
getProp hasLabels
    local tBool
    put the hasLabels of me into tBool
    if tBool is empty then put true into tBool
    return tBool
end hasLabels

setProp hasLabels pHasLabels
    if pHasLabels is not a boolean then
        return "Invalid parameter: not a boolean"
        exit hasLabels
    end if
    set the hasLabels of me to pHasLabels
    updateVisualControls
end hasLabels

########################### /PROPERTIES ##########################

on openControl
	createVisualControls
	layoutVisualControls
	updateVisualControls
end openControl

on resizeControl
    if not sHasCreatedVisuals then exit resizeControl
    layoutVisualControls
    formatTextFields
end resizeControl

private command createVisualControls
	if sHasCreatedVisuals then exit createVisualControls
	set the clipsToRect of me to true
	lock screen
	// background
	if there is not a graphic "background" of me then
		create graphic "background" in me
		set the width of me to 120
		set the height of me to 70
	end if
	put the id of graphic "background" of me into sBackroundID
	set the style of control id sBackroundID to "roundRect"
	set the opaque of control id sBackroundID to true
	set the linesize of control id sBackroundID to 1
	// labels
	set the margins of the templateField to "0,8,0,8"
	set the opaque of the templateField to false
	set the showBorder of the templateField to false
	set the threeD of the templateField to false
	set the locktext of the templateField to true
	set the dontWrap of the templateField to true
	set the traversalOn of the templateField to false
	set the showFocusBorder of the templateField to false
	set the textAlign of the templateField to "center"

	if there is not a field "textLong" of me then create field "textLong" in me
	put the id of field "textLong" of me into sTextLongID
	if there is not a field "textLeft" of me then create field "textLeft" in me
	put the id of field "textLeft" of me into sTextLeftID
	if there is not a field "textRight" of me then create field "textRight" in me
	put the id of field "textRight" of me into sTextRightID

	// indicator
	if there is not a graphic "indicator" of me then create graphic "indicator" in me
	put the id of graphic "indicator" of me into sIndicatorID
	set the style of control id sIndicatorID to "oval"
	set the opaque of control id sIndicatorID to true
	set the linesize of control id sIndicatorID to 1
	set the backgroundColor of control id sIndicatorID to "#E6E8E8"
	local tInnerGlow
	put "normal" into tInnerGlow["blendMode"]
	put 255, 255, 255 into tInnerGlow["color"]
	put "gaussian" into tInnerGlow["filter"]
	put 255 into tInnerGlow["opacity"]
	put 255 into tInnerGlow["range"]
	put 20 into tInnerGlow["size"]
	put "edge" into tInnerGlow["source"]
	put 0 into tInnerGlow["spread"]
	set the innerGlow of control id sIndicatorID to tInnerGlow

	set the switchValue of me to "center"

	put true into sHasCreatedVisuals
	reset the templateGraphic
	reset the templateField
	layoutVisualControls
end createVisualControls

private command layoutVisualControls ## fix by Bernd to stop the mesmerising wandering of the indicator...
	if not sHasCreatedVisuals then exit layoutVisualControls

	local tRect, tDiameter
	lock screen
	// background rect
	put the rect of me into tRect
	add 2 to item 1 of tRect
	add 2 to item 2 of tRect
	subtract 2 from item 3 of tRect
	subtract 2 from item 4 of tRect
	set the rect of control id sBackroundID to tRect
	// indicator rect
	put the height of control id sBackroundID - 4 into tDiameter
	set the width of control id sIndicatorID to tDiameter
	set the height of control id sIndicatorID to tDiameter

	## resize the indicator in current location
	switch the switchValue of me
		case "Left"
			set the left of control id sIndicatorID to the left of control id sBackroundID + 2
			set the top of control id sIndicatorID to the top of control id sBackroundID + 2
			break
		case "Center"
			set the loc of control id sIndicatorID to the loc of control id sBackroundID
			break
		case "Right"
			set the right of control id sIndicatorID to the right of control id sBackroundID - 2
			set the top of control id sIndicatorID to the top of control id sBackroundID + 2
			break
	end switch
	set the roundRadius of control id sBackroundID to tDiameter

	if the mouse is up then updateIndicatorForValue
end layoutVisualControls

private command updateVisualControls
	if not sHasCreatedVisuals then exit updateVisualControls
	switch the switchValue of me
		case "Left"
			set the backgroundColor of control id sBackroundID to the leftColor of me
			break
		case "Center"
			set the backgroundColor of control id sBackroundID to the centerColor of me
			break
		case "Right"
			set the backgroundColor of control id sBackroundID to the rightColor of me
			break
	end switch
	set the backgroundColor of control id sIndicatorID to the indicatorColor of me
    // display and set text labels
	formatTextFields
end updateVisualControls


private command formatTextFields
    if not sHasCreatedVisuals then exit formatTextFields
    if the hasLabels of me is false then
        hide control id sTextLongID
        hide control id sTextLeftID
        hide control id sTextRightID
        exit formatTextFields
    else
        local tOffset, tSize
        put (the width of control id sBackroundID - the width of control id sIndicatorID) / 4 into tOffset
        ------- text field rects ------
        // resize/reposition long label
        set the width of control id sTextLongID to the width of control id sBackroundID - the width of control id sIndicatorID - 20
        set the height of control id sTextLongID to the height of control id sBackroundID
        set the loc of control id sTextLongID to the loc of control id sBackroundID
        // resize/reposition left text label
        set the width of control id sTextLeftID to the width of control id sIndicatorID / 2
        set the height of control id sTextLeftID to the height of control id sBackroundID
        set the loc of control id sTextLeftID to the the left of control id sBackroundID + tOffset, item 2 of the loc of control id sBackroundID
        // resize/reposition right text label
        set the width of control id sTextRightID to the width of control id sIndicatorID / 2
        set the height of control id sTextRightID to the height of control id sBackroundID
        set the loc of control id sTextRightID to the right of control id sBackroundID - tOffset, item 2 of the loc of control id sBackroundID
        
        ------- format field text ------
        # get text size
        if length(the leftText of me) > length(the rightText of me) then
            set the text of control id sTextLongID to the leftText of me
        else
            set the text of control id sTextLongID to the rightText of me
        end if
        put the maxTextSize of me into tSize
        repeat
            set the textSize of control id sTextLongID to tSize
            if the formattedHeight of control id sTextLongID > the height of control id sTextLongID then
                subtract 1 from tSize
            else
                exit repeat
            end if
            if tSize <= the minTextSize of me then exit repeat
        end repeat
        # apply textSize, topMargin, appropriate textColor for background
        lock screen
        formatFieldText tSize, sTextLeftID
        formatFieldText tSize, sTextRightID
        formatFieldText tSize, sTextLongID
        
        ------- field text ------
        // show/hide fields and set text values
        switch the switchValue of me
            case "left"
                hide control id sTextLeftID
                hide control id sTextRightID
                set the left of control id sTextLongID to the right of control id sIndicatorID + 5
                set the text of control id sTextLongID to the leftText of me
                show control id sTextLongID
                break
            case "center"
                hide control id sTextLongID
                set the text of control id sTextLeftID to the leftTextSmall of me
                set the text of control id sTextRightID to the rightTextSmall of me
                show control id sTextLeftID
                show control id sTextRightID
                break
            case "right"
                hide control id sTextLeftID
                hide control id sTextRightID
                set the right of control id sTextLongID to the left of control id sIndicatorID - 5
                set the text of control id sTextLongID to the rightText of me
                show control id sTextLongID
        end switch
    end if
end formatTextFields

private command formatFieldText pSize, pControlID
    set the textColor of control id pControlID to textColorForBackground(the backgroundColor of control id sBackroundID)
    set the textSize of control id pControlID to pSize
    set the topMargin of control id pControlID to \
          (round((the height of control id pControlID - the formattedHeight of control id pControlID)/2 \
          + the formattedHeight of control id pControlID / 5))
end formatFieldText

private command updateIndicatorForValue
    local tX
    if not sHasCreatedVisuals then exit updateIndicatorForValue
    	
    switch the switchValue of me
        case "Left"
            set the backgroundColor of control id sBackroundID to the leftColor of me
            put the left of control id sBackroundID + 3 + the width of control id sIndicatorID / 2 into tX
            move control id sIndicatorID to tX, item 2 of the loc of control id sBackroundID in 200 milliseconds
            set the left of control id sIndicatorID to the left of control id sBackroundID + 2
            break
        case "Center"
            set the backgroundColor of control id sBackroundID to the centerColor of me
            move control id sIndicatorID to the loc of control id sBackroundID in 200 milliseconds
            break
        case "Right"
            set the backgroundColor of control id sBackroundID to the rightColor of me
            put the right of control id sBackroundID - 3 - the width of control id sIndicatorID / 2 into tX
            move control id sIndicatorID to tX, item 2 of the loc of control id sBackroundID in 200 milliseconds
            set the right of control id sIndicatorID to the right of control id sBackroundID - 2
            break
    end switch
    formatTextFields
end updateIndicatorForValue

on mouseDown
	if "groupbehavior" is in the long name of the target then exit to top -- stop behavior stack from firing mouseDowns, remove in widget

	if the mouseLoc is within the rect of control id sIndicatorID then
		put true into sClickedIndicator
		put the mouseH into sMouseX
	end if
	pass mouseDown
end mouseDown

on mouseUp pKey
	local tSegmentClicked, tRect, tR1, tR2, tR3, t3
	if "groupbehavior" is in the long name of the target then exit to top -- stop behavior stack from firing mouseUps, remove in widget

	if sClickedIndicator then   // drag indicator 1 step in drag direction
		local tValue
		put the switchValue of me into tValue
		if the mouseH - sMouseX > 0 and tValue is "Left" then // go right
			put "Center" into tValue
		else if the mouseH - sMouseX > 0 and tValue is "Center" then // go right
			put "Right" into tValue
		else if the mouseH - sMouseX < 0 and tValue is "Right" then //go left
			put "Center" into tValue
		else if the mouseH - sMouseX < 0 and tValue is "Center" then //go left
			put "Left" into tValue
		end if
		set the switchValue of me to tValue
	else                        // establish 3 click zones to see if left, centre or right
		put the width of control id sBackroundID / 3 into t3
		put the rect of control id sBackroundID into tR1
		put item 1 of tR1 + t3 into item 3 of tR1
		put tR1 into tR2
		add t3 to item 1 of tR2
		add t3 to item 3 of tR2
		put tR2 into tR3
		add t3 to item 1 of tR3
		add t3 to item 3 of tR3
		// apply value based on click zone
		if the mouseLoc is within tR1 then
			set the switchValue of me to "Left"
		else if the mouseLoc is within tR2 then
			set the switchValue of me to "Center"
		else if the mouseLoc is within tR3 then
			set the switchValue of me to "Right"
		end if
	end if
	put false into sClickedIndicator
	put empty into sMouseX
end mouseUp

on mouseRelease
	local tValue
	if "groupbehavior" is in the long name of the target then exit to top
	put the switchValue of me into tValue
	if the mouseH - sMouseX > 0 and tValue is "Left" then // go right
		put "Center" into tValue
	else if the mouseH - sMouseX > 0 and tValue is "Center" then // go right
		put "Right" into tValue
	else if the mouseH - sMouseX < 0 and tValue is "Right" then //go left
		put "Center" into tValue
	else if the mouseH - sMouseX < 0 and tValue is "Center" then //go left
		put "Left" into tValue
	end if
	set the switchValue of me to tValue
	put false into sClickedIndicator
	put empty into sMouseX
end mouseRelease

# return dark textColor for light backgrounds & vice versa
private function textColorForBackground pColor
    local hsp, r, g, b
    if char 1 of pColor is "#" and length(pColor) = 7 then // hex
        put baseConvert(char 2 to 3 of pColor, 16, 10) into r
        put baseConvert(char 4 to 5 of pColor, 16, 10) into g
        put baseConvert(char 6 to 7 of pColor, 16, 10) into b
    else if length(pColor) = 6 then
        put baseConvert(char 1 to 2 of pColor, 16, 10) into r
        put baseConvert(char 3 to 4 of pColor, 16, 10) into g
        put baseConvert(char 5 to 5 of pColor, 16, 10) into b
    else if pColor contains ","  then// rbg
        put item 1 of pColor into r
        put item 2 of pColor into g
        put item 3 of pColor into b
    end if
    put sqrt(0.299 * (r * r) + 0.587 * (g * g) + 0.114 * (b * b)) into hsp
    if hsp > 127.5 then
        return "black"
    else
        return "white"
    end if
end textColorForBackground

I added this to a substack ("tristateBehavior") to use as a behaviour and can create a new 'group gadget' using a button with the script

Code: Select all

on mouseUp pButtonNumber
    local tName
    put "tristate 1" into tName
    repeat 
        if there is a group tName then 
            add 1 to the last word of tName
        else 
            exit repeat
        end if
    end repeat
    create group tName
    set the behavior of group tName to the long id of stack "tristateBehavior"
    send "openControl" to group tName
end mouseUp
This generates a group that is identical to the widget, but obviously doesn't have access to the property panel and you'd need to set the properties by code.

Post Reply

Return to “Made With LiveCode”