Using PolyList to Create Dropdown Menus

by Ben Jack on April 25, 2023 3 comments

This tutorial will guide you through creating a dropdown menu using a PolyList widget. The finished product will be able to display an array of data, even if the array itself contains more arrays. The completed stack should look like the below example, where you can drag a button on, give it a name and click it to create and build the PolyList. You can learn more about the PolyList widget here. Note, Polylist is an addon for LiveCode, if you don’t already own it you can get it here.

Creating a PolyList

To use a PolyList to create dropdown menus, you first need to understand how to create and use a PolyList. To create a PolyList you can either drag it from the tools palette or create it with LiveCode Script by typing

create widget “myPolyList” as "com.livecode.widget.polylist" 

Next, you need to know how to set the dataLayout and tsvData, these both have to be done with the script, and simple examples of each are shown below.

Data Layout Example

local tDataLayout
put "name" into tDataLayout[1]["name"]
put "90" into tDataLayout[1]["width"]
put “90" into tDataLayout[1]["height"]
put "10" into tDataLayout[1]["top"]
put "10" into tDataLayout[1]["left"]
put "text" into tDataLayout[1]["content-type"]

set the dataLayout of widget “myPolyList” to tDataLayout

TSV Data Example

local tTsvData
put “name” & return into tTsvData
put “John Smith” & return after tTsvData
put “Jane Doe” & return after tTsvData

set the tsvData of widget “myPolyList” to tTsvData

This will create a simple PolyList that just shows someone’s name, but PolyList can be used for so much more, and this is going to cover one example of that.

Creating the Dropdown

This tutorial is going to guide you to create a dropdown PolyList, similar to the one that will be featured in the new script editor.

Looking at this PolyList it may appear that there are just 2 pieces of data on each row (the key and the value), but there are actually several more. Each row also contains the ability to have an arrow, and the supporting vertical lines. In total there are 5 subitems on each row, and a function can be created to set this up which will look like the below:

function getPolyListDataLayout
local rDataLayout
   put "line" into rDataLayout[1]["name"]
   put "100" into rDataLayout[1]["height"]
   put "0" into rDataLayout[1]["top"]
   put "34px" into rDataLayout[1]["left"]
   put "1px" into rDataLayout[1]["width"]
   put "color" into rDataLayout[1]["content-type"]

   put "halfline" into rDataLayout[2]["name"]
   put "30" into rDataLayout[2]["height"]
   put "70" into rDataLayout[2]["top"]
   put "34px" into rDataLayout[2]["left"]
   put "1px" into rDataLayout[2]["width"]
   put "color" into rDataLayout[2]["content-type"]

   put "arrow" into rDataLayout[3]["name"]
   put "14px" into rDataLayout[3]["width"]
   put "14px" into rDataLayout[3]["height"]
   put "9px" into rDataLayout[3]["top"]
   put "27px" into rDataLayout[3]["left"]
   put "icon-path" into rDataLayout[3]["content-type"]
   put "102,102,102,255" into rDataLayout[3]["color"]

   put "name" into rDataLayout[4]["name"]
   put "24px" into rDataLayout[4]["height"]
   put "4px" into rDataLayout[4]["top"]
   put "55px" into rDataLayout[4]["left"]
   put "50" into rDataLayout[4]["width"]
   put "text" into rDataLayout[4]["content-type"]

   put "value" into rDataLayout[5]["name"]
   put "24px" into rDataLayout[5]["height"]
   put "4px" into rDataLayout[5]["top"]
   put "50" into rDataLayout[5]["left"]
   put "50" into rDataLayout[5]["width"]
   put "text" into rDataLayout[5]["content-type"]

   return rDataLayout
end getPolyListDataLayout

 Line and half line are the depth lines to show when a section is opened, half line is the line going from the dropdown arrow to the bottom of the row, and line covers the whole height of the row. The next subitem is arrow, which will contain the SVG path for the arrow, showing whether it is opened or not, and the final two subitems are the name (key) and the value, positioned on the left half and right half of the row respectively.

Now that we have the data layout set, we need to work on generating the TSV data. This may seem simple at first as you just need to convert your array to a TSV string, but you also need to account for nested arrows, which means this TSV generation function needs to support recursion. On top of this, the function needs to check whether it should recurse or not based on whether the array is opened or not.

So to start we should create a global variable to store which keys have been opened, this can be created by putting

local sOpenVariables

at the top of your script file, and a second global variable needs to be created that will be the array to inspect, this can be created using

local sArray

but will need to be populated in code. Once this is done, a method needs to be created to update the PolyList. This will handle setting the TSV data of the PolyList and will be called every time a dropdown is opened or closed.

command update
local tTsvData
   put "arrow" & tab & "name" & tab & "value" & tab & "line" & tab & "halfline" & tab & "_namePath" & return into tTsvData
   put generateTsvData(sArray, 0, empty) after tTsvData
   if the last character of tTsvData is return then
      delete the last character of tTsvData
   end if
   set the tsvData of widget "myPolyList" to tTsvData
end update

In the header line of the TSV data you may notice there is a key defined which doesn’t reference a subitem, “_namePath”. This is metadata for the row and will be used to uniquely reference this specific row, and will be stored in the sOpenVariables variable to know whether the row is opened or not. On top of this, the update method references a function that we haven’t created yet, generateTsvData, which will do a lot of the heavy lifting for this dropdown. 

In the generateTsvData function, we need to do several things initially. The first is to create a prefix for the key, this will show that the key is a child of a parent by indenting it, like in the above example. Next, each key in the array needs to be iterated over to check whether the value for this key is an array itself. If it is not then the TSV string can just be created, otherwise, we will need to check whether the dropdown is opened, and create an arrow that reflects this. As well as this, if the dropdown is opened, this function needs to be called recursively, passing the value to inspect, increasing the depth level, and passing the current “_namePath” to be used as a parent. The finished function should look like this:

function generateTsvData pData, pDepth, pParents
local rTsvData, tDepthPrefix

   put empty into tDepthPrefix
   repeat for pDepth times
      put "   " after tDepthPrefix
   end repeat

   repeat for each key tKey in pData
      local tNamePath
      put pParents & tKey into tNamePath
      if the number of elements of pData[tKey] is 0 then
         local tLineColor
         put "102,102,102,0" into tLineColor
         if pDepth > 0 then
            put "102,102,102,102" into tLineColor
         end if
         put empty & tab & tDepthPrefix & tKey & tab & pData[tKey] & tab & tLineColor & tab & "255,255,255,0" & tab & tNamePath & return after rTsvData
      else
         local tHalfLineColor, tArrow
         if tNamePath is not among the items of sOpenVariables then
            put "m 1.8024106,0.1431135 a 1.046,1.046 0 0 0 -0.576172,0.296875 l -0.990234,0.988282 a 1.046,1.046 0 0 0 0,1.4843749 l 4.21875,4.21875 -4.21875,4.2226556 a 1.046,1.046 0 0 0 0,1.482422 l 0.988281,0.990234 a 1.046,1.046 0 0 0 1.484375,0 L 8.659833,7.8735824 a 1.041,1.041 0 0 0 0,-1.478515 L 2.7086606,0.4399885 a 1.046,1.046 0 0 0 -0.90625,-0.296875 z" into tArrow
            put "255,255,255,0" into tHalfLineColor
         else
            put "M6.7,20.1.327,13.72a1.121,1.121,0,0,1,0-1.59l1.06-1.06a1.121,1.121,0,0,1,1.59,0l4.52,4.52,4.52-4.52a1.121,1.121,0,0,1,1.59,0l1.06,1.06a1.121,1.121,0,0,1,0,1.59L8.289,20.1a1.115,1.115,0,0,1-1.585,0Z" into tArrow
            put "102,102,102,102" into tHalfLineColor
         end if
         put tArrow & tab & tDepthPrefix & tKey & tab & empty & tab & "255,255,255,0" & tab & tHalfLineColor & tab & tNamePath & return after rTsvData

         if tNamePath is among the items of sOpenVariables then
            put generateTsvData(pData[tKey], pDepth + 1, tNamePath & colon) after rTsvData
         end if
      end if
   end repeat
   return rTsvData
end generateTsvData

With each recursion, the variable pDepth will increase by 1, and the variable pParents will contain the current path to the key excluding the key itself. For example, if you have an array

tArray[“key1”][“key2”][“key3”] = “value”

then the pParents at the time when the TSV data for key3 is created will be “key1:key2” but the name path for key3 will be “key1:key2:key3”. To decide whether the lines are shown or not, the color of them is changed. The color given is an RGBA value, and so the 4th piece of data (the alpha) can be 0, which means the line will be invisible. This is used to hide the lines.

The dropdown is almost complete, finally, some code just needs to be created to allow the dropdowns to function. This will rely on the subItemClick method of the PolyList, which is called whenever a subitem is clicked. Within this method, we first need to check whether the subitem clicked was an arrow or not, as we don’t want a dropdown to toggle whenever a user clicks any subitem on the row. Next we need to make sure that the value of the arrow isn’t empty, as even when an arrow is not visible, the subitem is still there and can still be clicked. If an arrow does exist, then we just need to update the variable sOpenVariables to either add or remove the current key path. This may sound simple, but we need to ensure that when a parents dropdown is closed, it closes all the childrens dropdowns as well. This can be done by iterating over each item in sOpenVariables and deleting all occurrences that start with the current name path. The finished method should look like this:

on subItemClick pItemIndex, pSubItemName
 if pSubItemName is "arrow" then
      local tItemData, tVarName
      /* Set data pointer */
      set the itemPointer of the target to pItemIndex
      put the itemContent of the target into tItemData

      if tItemData[pSubItemName] is empty then
         exit subItemClick
      end if
      if tItemData["_namePath"] is not among the items of sOpenVariables then
         put tItemData["_namePath"] & comma before sOpenVariables
         if the last character of sOpenVariables is comma then
            delete the last character of sOpenVariables
         end if
      else
         set wholematches to true
         repeat for each item tItem in sOpenVariables
            if tItem begins with tItemData["_namePath"] then
               delete item itemoffset(tItem,sOpenVariables) of sOpenVariables
            end if
         end repeat
      end if
      update
   end if
end subItemClick

Now that this is all done, all we need to do is create a PolyList with the name “myPolyList” and populate the sArray variable with data. To help set this up create a button with the name “setupButton”, put this code in your script, and click the button:

on mouseUp
 if the short name of the target is "setupButton" then
      if there is not a widget "myPolyList" then
         create widget "myPolyList" as "com.livecode.widget.polylist"
         set the dataLayout of widget "myPolyList" to getPolyListDataLayout()
      end if
   end if
   set the width of control "setupButton" to the width of me
   set the height of control "setupButton" to 60
   set the topLeft of control "setupButton" to the topLeft of me

   set the width of widget "myPolyList" to the width of me
   set the height of widget "myPolyList" to the height of me - 60
   set the topLeft of widget "myPolyList" to the bottomleft of control "setupButton"
   set the itemheight of widget "myPolyList" to 32
   set the strokeBody of widget "myPolyList" to false
   set the strokeItems of widget "myPolyList" to false
   set the vScrollBar of widget "myPolyList" to "Off"
   set the plItemColor of widget "myPolyList" to "255,255,255,0"
   set the plHiliteColor of widget "myPolyList" to "228,239,243,255"
   set the hoverWithMouse of widget "myPolyList" to true
   set the mouseHoverHilite of widget "myPolyList" to true
   set the margin of widget "myPolyList" to 0
   set the borderWidth of widget "myPolyList" to 0

   put empty into sArray
   put "No Depth Value" into sArray["No Depth Key"]
   put "1 Depth Value 1" into sArray["Depth Key"]["1 Depth Key 1"]
   put "1 Depth Value 2" into sArray["Depth Key"]["1 Depth Key 2"]
   put "2 Depth Value 1" into sArray["Depth Key"]["2 Depth Key 1"]["2 Depth Key 1"]
   put "2 Depth Value 2" into sArray["Depth Key"]["2 Depth Key 1"]["2 Depth Key 2"]
   update
end mouseUp

Once all this is created, then you should have a working dropdown PolyList. This just outlines a single example, but this can be modified to suit whatever purpose you need. Try making changes to the code you’ve produced to get it to fit your needs, or just to learn more about how it works.

Exercises

  1. Allow values to support a return in them. Currently, this will throw an error, test this with
    put “Value line 1” & return & “Value line 2” into sArray[“No Depth Key”]
  2. Allow the divide between key and value to be moved like this:
  3. Create a filter field, to search the keys or values (or allow the user to pick between keys, values, or both)
  4. Create a sort order (alphabetical, reverse alphabetical)
Ben JackUsing PolyList to Create Dropdown Menus

Related Posts

Take a look at these posts

3 comments

Join the conversation
  • James Hale - May 2, 2023 reply

    Great post.
    You mention creating 2 global variables sOpenVariables and sArray, but show the script examples as local.
    I.e. “local sOpenVariables“ and “local sArray”
    Rather than,
    ‘global sOpenVariables“ and “global sArray”

    Ben - May 2, 2023 reply

    Hi James,

    Sorry for the confusion, when I’m talking about those variables I meant global as can be accessed by any function within the file, but not actual global variables. By defining the “sOpenVariables” and “sArray” outside a function it means the whole script can access them 🙂

    Thanks!

    James Hale - May 2, 2023 reply

    Thanks Ben, you might want to change the script snippets you have in the lesson though.

Join the conversation

*