-- CASCADING CHECKBOXES -- -- -- © January 2001, James Newton -- -- This behavior should be attached to a group of checkbox button -- sprites and a group of shape sprite. The shape sprites are used to -- mask any checkboxes which are not currently available. -- -- You can use this behavior to ensure that the state of certain -- checkboxes determines the availablity of subsidiary checkboxes. -- -- For example, in a password dialog, the "Remember password?" button -- must be checked before the "Open automatically?" button can be -- selected. -- PROPERTY DECLARATIONS -- property spriteNum property mySprite property myMember -- Property set for both #button and #shape sprites property buttonID -- Unique name shared by button/shape sprite set -- Properties set only for #button sprites property isEnabled -- Hilite state on beginSprite property enableRule -- Deeper buttons enabled if enableRule = hilite property dependentButtons -- Initially a comma-delimited string(me, -- converted to a list of symbols on beginSprite property ourParentInstance -- pointer to the first instance of this -- behavior to be instanciated. This first -- instance will manage the whole button tree. -- Property of ourParentInstance only: property ourCascadeList -- nested property list with the format -- [#buttonName: [#locZ: , #rect: , -- #enableRule: , #hilite: , -- #sprite: , #branches: ], ...] -- The property may be a list with the same format. -- SPRITE EVENTS -- on beginSprite(me) me.initialize() end beginSprite on endSprite(me) -- Ensure that no reciprocal references remain if myMember.type = #button then ourParentInstance.trimBranches(buttonID) end if ourParentInstance = VOID end endSprite -- EVENT HANDLER -- on mouseUp(me) if myMember.type = #button then ourParentInstance.cascadingCheckbox(buttonID, myMember.hilite) end if end mouseUp -- PUBLIC METHOD -- on getCheckbuttonStates(me, propertyList) ---------------------------- -- Called by an different script -- -- RETURNS: a property list of button names and values for enabled -- buttons only. -------------------------------------------------------------------- if ilk(propertyList) <> #propList then propertyList = [:] else if propertyList.count() then -- A previous instance of this behavior has already replied exit end if if ourParentInstance = me then return me.getCascadeStates(propertyList) else -- channel the request to ourParentList return ourParentInstance.getCascadeStates(propertyList) end if end getCheckbuttonStates -- PRIVATE METHODS -- on initialize(me) ---------------------------------------------------- -- Sent by beginSprite() -- -- ACTIONS: -- * Interrogates existing instances of this sprite for the oldest -- instance to use as ourParentInstance -- * Places data concerning the current #button and #shape sprite -- group in ourParentInstance's data list (ourCascadeList) -- * Refreshes the display of the whole button tree to take this -- latest sprite and its settings into account -------------------------------------------------------------------- mySprite = sprite(spriteNum) myMember = mySprite.member -- Find the parent instance for all sprites with this behavior. -- This instance will act as the manager for the whole group of -- checkbox/shape sprites. It will remain in existence until the -- last sprite using it is destroyed. ourParentInstance = [] sendAllSprites(#getParentInstance, ourParentInstance) ourParentInstance = ourParentInstance.getLast() if voidP(ourParentInstance) then ourParentInstance = me end if -- Add the button sprite data to ourCascadeList if myMember.type = #button then myMember.hilite = isEnabled ourParentInstance.addBranch( \ buttonID, \ mySprite.rect, \ mySprite.locZ, \ me.stringToList(dependentButtons), \ enableRule, \ isEnabled \ ) else -- Add the shape sprite instances to ourCascadeList so that the -- buttons can be enabled or disabled by calling the appropriate -- shape instances. Shape sprites should logically be in a higher- -- numbered sprite channel if buttonID = #Blue2 then nothing end if ourParentInstance.placeOnBranch(buttonID, mySprite) end if ourParentInstance.setAvailability() end initialize on stringToList(me, theString) --------------------------------------- -- Called by initialize() -- -- PARAMETER: should be a comma-delimited string of -- single-word items -- -- RETURNS: a list of symbols -------------------------------------------------------------------- theList = [] if stringP(theString) then if theString <> "" then saveDelimiter = the itemDelimiter the itemDelimiter = "," i = theString.item.count repeat while i theList.append(symbol(theString.item[i])) i = i - 1 end repeat the itemDelimiter = saveDelimiter end if end if return theList end stringToList -- INTER-INSTANCE COMMUNICATION -- on getParentInstance(me, parentList) --------------------------------- -- Sent to all sprites on initialize() -- -- PARAMETER: parentList is a linear list -- -- ACTION: Retrieves the current parent instance (if there is one) -- from the first sprite to reply. -------------------------------------------------------------------- if not parentList.count then -- This is the first sprite to reply if ilk(ourParentInstance, #instance) then -- This sprite has a pointer to the parent instance to share parentList.append(ourParentInstance) --.getLast()) end if end if end getParentInstance -- PARENT INSTANCE METHODS -- -- Property only used by ourParentInstance: -- property ourCascadeList -- Reading the values on getCascadeStates(me, propertyList, nestedList) -------------------- -- Sent by getCheckbuttonStates() and recursively -- -- RETURNS: a property list of button names and values for enabled -- buttons only. -------------------------------------------------------------------- if voidP(nestedList) then nestedList = ourCascadeList end if buttonCount = nestedList.count repeat with i = 1 to buttonCount branch = nestedList[i] if branch.count then branchName = nestedList.getPropAt(i) state = branch.hilite propertyList.addProp(branchName, state) if state = branch.enableRule then me.getCascadeStates(propertyList, branch.branches) end if end if end repeat return propertyList end getCascadeStates -- Setting up the button tree on placeOnBranch(me, checkboxID, shapeSprite, nestedList) ------------ -- Called by initialize() for #shape instances and recursively -- -- ACTION: Ensures that a reference to the #shape sprite is -- available to the #button's data list, which is added by -- addBranch() -------------------------------------------------------------------- if voidP(ourCascadeList) then -- has just been created by this instance. ourCascadeList = [:] end if atRoot = voidP(nestedList) if atRoot then nestedList = ourCascadeList end if branch = nestedList[checkboxID] if listP(branch) then -- We have found the appropriate place for this instance branch[#sprite] = shapeSprite -- Ensure that the shape sprite appears at a higher locZ than the -- button it should cover zLoc = branch[#locZ] if zLoc then shapeSprite.locZ = zLoc + 1 else nestedList[checkboxID] = shapeSprite end if return TRUE -- for the variable in the previous iteration end if -- Look deeper into the button tree i = nestedList.count repeat while i branch = nestedList[i] branches = branch[#branches] if listP(branches) then placed = me.placeOnBranch(checkboxID, shapeSprite, branches) if placed then return TRUE -- pass this value up to the previous iteration end if end if i = i - 1 end repeat if atRoot and not placed then -- No data for this checkboxID was found anywhere in the tree. -- Place this offshoot at the root and wait for the button -- instance to pick it up and place it correctly. (Normally the -- button sprite should be in a lower-numbered channel, so this -- case should rarely if ever occur). ourCascadeList[checkboxID] = shapeSprite return TRUE end if end placeOnBranch on addBranch(me, checkboxID, theRect, zLoc, offshoots, rule, hilite) -- Called by initialize() for #button instances. -- -- ACTION: Creates a data list (branch) for the current button and -- ensures that it is placed in the appropriate place in the -- button tree. -------------------------------------------------------------------- -- Convert offshoots to a (potentially nested) property list branches = [:] i = offshoots.count repeat while i branches[offshoots[i]] = [:] i = i - 1 end repeat -- Create data list for the current button dataList = [ \ #locZ: zLoc, \ #rect: theRect, \ #enableRule: rule, \ #hilite: hilite, \ #sprite: 0, \ #branches: branches \ ] if voidP(ourCascadeList) then -- has just been created. Add data -- concerning its own button to the button tree. ourCascadeList = [:] ourCascadeList[checkBoxID] = dataList else -- Pick up the shape sprite if it has already been placed at -- the root of the tree branchSprite = ourCascadeList[checkboxID] if ilk(branchSprite, #sprite) then branchSprite.locZ = dataList.locZ + 1 dataList[#sprite] = branchSprite ourCascadeList.deleteProp(checkboxID) end if -- Find where checkboxID may already appear in ourCascadeList grafts = me.graftToBranches(ourCascadeList,checkboxID,dataList,0) -- Check if any existing roots are an offshoot of this branch i = ourCascadeList.count repeat while i branchName = ourCascadeList.getPropAt(i) if offshoots.getPos(branchName) then -- This is a offshoot of the current branch branches[branchName] = ourCascadeList[i] ourCascadeList.deleteAt(i) end if i = i - 1 end repeat if not grafts then -- Place the current dataList at the root ourCascadeList[checkboxID] = dataList end if end if end addBranch on graftToBranches(me, mainList, checkBoxID, dataList, grafts) ------- -- Called by addBranch() and recursively -- -- ACTION: Places in the appropriate place in the -- button tree. If the behavior on shape sprite has already been -- instanciated, the sprite reference will be added to . -------------------------------------------------------------------- i = mainList.count repeat while i trunk = mainList[i] trunkName = mainList.getPropAt(i) if trunkName = checkboxID then if ilk(trunk, #sprite) then -- A pointer to the shape's sprite is already in place: copy -- it into dataList, which will replace it. dataList[#sprite] = trunk -- Ensure that the shape sprite is in a higher channel than -- the button sprite trunk.locZ = dataList.locZ + 1 else if trunk.count then -- There appear to be two sprites with the same buttonID return call(#errorAlert, [me], mainList, checkboxID, dataList) end if mainList[i] = dataList -- Don't allow a given branch to be grafted in more than one -- place return TRUE else branches = trunk[#branches] if listP(branches) then grafts=me.graftToBranches(branches,checkBoxID,dataList,grafts) end if end if i = i - 1 end repeat return grafts end graftToBranches on setAvailability(me) ----------------------------------------------- -- Called by initialize(), trimBranches() -- -- ACTION: Refreshes the enabled state of all the buttons in the -- button tree. This handler is called twice for each button, once -- by the instance on the button itself, once by the instance on the -- shape sprite associated with it. Since the behavior cannot tell -- in advance how many buttons will be used, a number of unnecessary -- refreshes will be made. -------------------------------------------------------------------- i = ourCascadeList.count() repeat while i branch = ourCascadeList[i] if listP(branch) then branchName = ourCascadeList.getPropAt(i) branches = branch[#branches] if branches <> [:] then me.cascadingCheckbox(branchName, branch.hilite) end if end if i = i - 1 end repeat end setAvailability on cascadingCheckbox(me, clickedID, hilite, nestedList) --------------- -- Sent by mouseUp(), setAvailability() and recursively -- -- ACTION: Toggles the property in ourCascadeList -- associated with the button that was clicked, then calls -- cascadeState() to enable or disable the dependent buttons. -------------------------------------------------------------------- if voidP(nestedList) then nestedList = ourCascadeList end if branch = nestedList[clickedID] if voidP(branch) then -- Continue looking in all deeper branches i = nestedList.count repeat while i branch = nestedList[i] if listP(branch) then offshoots = branch[#branches] if listP(offshoots) then cascadingCheckbox(me, clickedID, hilite, offshoots) end if end if i = i - 1 end repeat else -- Set the property of this branch... branch.hilite = hilite -- ... and hide the shape sprite. (If this button is at the root -- of the tree, it does not need to have a shape sprite to disable -- if, but it may have one anyway, for other reasons. When this -- handler is called from initialize(), this optional shape -- sprite will need to be hidden. may not be -- complete, so we have to be careful). branchSprite = branch[#sprite] if ilk(branchSprite, #sprite) then branchSprite.rect = rect(0, 0, 0, 0) end if -- Treat all the buttons in the offshoots branches = branch[#branches] if ilk(branches, #propList) then me.cascadeState(branches, hilite = branch.enableRule) end if end if end cascadingCheckbox on cascadeState(me, branches, enable) -------------------------------- -- Sent by cascadingCheckbox() and recursively -- -- ACTION: Enables or disables the buttons whose availability is -- dependent on the status of the button that was clicked. -------------------------------------------------------------------- i = branches.count repeat while i branch = branches[i] -- Show the shape sprite for this dependent button in its -- appropriate state... branchSprite = branch[#sprite] if ilk(branchSprite, #sprite) then if enable then branchSprite.rect = rect(0, 0, 0, 0) else branchSprite.rect = branch.rect end if -- ... and continue for the lower ramifications of dependency if branch <> [:] then if (branch.hilite = branch.enableRule) then me.cascadeState(branch[#branches], enable) else me.cascadeState(branch[#branches], 0) end if end if end if i = i - 1 end repeat end cascadeState on trimBranches(me, checkbuttonID, nestedList) ----------------------- -- Sent by endSprite() and recursively -- -- ACTION: Deletes all references to the sprite that is ending from -- . will not appear as the value of a -- property at level 1 of ourCascadeList. If it is found at a lower -- level, the entire branch that it is found in will be trimmed. -------------------------------------------------------------------- if voidP(nestedList) then nestedList = ourCascadeList end if i = nestedList.count repeat while i branchName = nestedList.getPropAt(i) branch = nestedList[i] branches = branch[#branches] if branchName = checkbuttonID then -- this is the branch to cut -- Place any deeper branches at the root branchCount = branches.count repeat while branchCount branchName = branches.getPropAt(branchCount) ourCascadeList[branchName] = branches[branchCount] branchCount = branchCount - 1 end repeat -- Cut current branch nestedList.deleteAt(i) -- Refresh the display in its new state me.setAvailability() else if ilk(branches, #propList) then -- Look deeper into the button tree trimBranches(me, checkbuttonID, branches) end if i = i - 1 end repeat end trimBranches -- BEHAVIOR PARAMETERS -- on isOKToAttach(me, spriteType, spriteNumber) -- Allow only #button and rect #shape members if spriteType = #graphic then theMember = sprite(spriteNumber).member case theMember.type of #button: return TRUE #shape: return theMember.shapeType = #rect otherwise return FALSE end case else return FALSE end if end isOKToAttach on getPropertyDescriptionList(me) ------------------------------------ -- This handler provides two different Behavior Parameters dialogs. -- * If this sprite is a #button then four parameters will be set -- (buttonID, isEnabled, dependentButtons and, enableRule). In -- this case, buttonID refers to the current sprite. -- * If this sprite is a #shape then only the buttonID parameter -- will be set. In this case it will refer to the button sprite -- which should be enabled or disabled. -------------------------------------------------------------------- spriteNumber = the currentSpriteNum if the currentSpriteNum then memberType = sprite(spriteNumber).member.type else memberType = #button end if propertiesList = [:] case memberType of #button: -- buttonID, isEnabled, enableRule & dependentButtons propertiesList[ \ #buttonID] = [ \ #comment: "Unique single-word ID for this checkbox:", \ #format: #symbol, \ #default: symbol("checkbox"&spriteNumber) \ ] propertiesList[ \ #isEnabled] = [ \ #comment: "Enable this checkbox on beginSprite?", \ #format: #boolean, \ #default: FALSE \ ] propertiesList[ \ #enableRule] = [\ #comment: "When the state of this checkbox is...", \ #format: #boolean, \ #default: TRUE \ ] propertiesList[ \ #dependentButtons] = [ \ #comment: \ "...enable checkboxes with the IDs:"&RETURN&\ "(Use commas to separate multiple IDs)", \ #format: #string, \ #default: "checkbox1,checkbox2" \ ] #shape: -- buttonID only propertiesList[ \ #buttonID] = [ \ #comment: "Enable/disable the checkbox with the ID", \ #format: #symbol, \ #default: #checkbox \ ] end case return propertiesList end getPropertyDescriptionList on getBehaviorTooltip(me) return \ "Use with a matched set of #button and #shape sprites."&\ RETURN&RETURN&\ "This behavior allows you to enable and disable one group"&RETURN&\ "of buttons depending on the hilite state of a given button." end getBehaviorTooltip on getBehaviorDescription(me) return \ "CASCADING CHECKBOXES"&RETURN&RETURN&\ "This behavior should be attached to a group of checkbox button sprites and a group of shape sprite. The shape sprites are used to mask any checkboxes which are not currently available."&RETURN&RETURN&\ "You can use this behavior to ensure that the state of certain checkboxes determines the availablity of subsidiary checkboxes."&\ RETURN&RETURN&\ "For example, in a password dialog, the 'Remember password?' button must be checked before the 'Open automatically?' button can be selected." end getBehaviorDescription