Tuesday, March 4, 2014

EventTracker: Creating a Chess Clock for Faculty Meetings and Other Events

Being a university professor, I sit through a lot of meetings. It is not unusual for one or two people to dominate a meeting, but to be fair I really don't think most of the time they know they're doing it. But, sometimes I wish each person at the meeting had one of those timers that chess players use. You know, the one with a clock for each player that tracks how much time each is taking during the match. Wouldn't it be great, I think to myself as I sit there, if everyone at the meeting had to start a clock each time they started to speak? Then, at the end of the meeting we could tell how much time each of us spent talking. Hey, maybe it would encourage a little more listening!

This little daydream has inspired my next LiveCode project - an app that allows someone to document who is at a meeting, when and how often they talk, and how long they talk.

Here's a description of the basic design. You show up at a meeting, take a look around the room, and quickly enter everyone's name into a text field. Most meetings I attend have about 10-15 people, so this wouldn't take very long. Then, in the blink of an eye, a set of unique buttons appears on the screen, one with each person's name on the list. Someone else quickly shows up at the last minute? Just add their name to the list and tap the create button again to update. You also have the option to sort the list.

Then, during the meeting, as different people speak, you just tap the button with their name and their name and a date/time stamp are added to a text field. You can also edit the text field if you like, such as by adding some comments to the end of any of the text entries.

When the meeting is over, you can generate a quick report of who spoke how often and for how long -- just summative stuff. But, you could also copy all of the raw data and paste it into Excel or SPSS for a more sophisticated analysis.

Of course, a tool like this could be used for all sorts of things, such as the following:
  • Sports: Which side controls the ball during a basketball game and for how long?
  • Nature: Which birds you see at a bird feeder during the day and how long they feed?
  • Politics: Which panelists or guests on a Sunday morning news show talk when and for how long?
  • Young children's behavior patterns: What a young child does during a specific hour of the day and for how long?
  • Television: When particular commercials come on during a favorite program and for how long? 
  • Education: Who participates in a small group discussion?
So, I'm calling this app "EventTracker" - here is a screen shot:

[ Get the free LiveCode Community version. ]


 https://drive.google.com/file/d/0B3W_1u8Y2RLRVGVDYk5EZ3hKVnM/edit?usp=sharing


This is really just an offshoot of the Classroom Observation Tool I wrote about previously. But, there are some key differences. For example, the design of this app records the time for each single event, whereas the Classroom Observation Tool allowed the user to string together a series of markers before applying a time stamp. The most significant difference, I think, is that this app takes advantage of the fact that you can create a script to copy and paste any object -- such as a button -- within the program.

Copying and Pasting Buttons with Code


OK, let's consider how copying and pasting buttons with code is done. To demonstrate, I've built another LiveCode stack with two cards -- a main card and a card for a library. This stack will help explain the key ideas which I use in the EventTracker app. Here's a screen shot:


[ Get the free LiveCode Community version. ]



On the library card (not shown), I've placed a single button called "template." On the main card, I have several buttons with progressively more complex code. The first is titled "Version 1: copy/paste 1 button" -- here's the script:

on mouseUp
   copy button "template" on card "library" to this card
   set the location of button "template" to 100,50
   set name of button "template" to "New Button"
   set label of button "New Button" to "New Button"
end mouseUp

Line 1 copies and pastes the "template" button from the library card to the main card. It then moves this freshly pasted button to the top, left position of the screen, as denoted by 100,50 (100 pixels from the left side and 50 pixels from the top).

Line 3 renames the button as "New Button," and line 4 gives it the label "New Button."

What if we want more than just one new button? Well, let's repeat this process a few times in a second version of the script found in the button titled "Version 2: copy/paste 3 buttons":

on mouseUp
     put 50 into y
     repeat 3 times
      copy button "template" on card "library" to this card
      set the location of button "template" to 100,y
      put item 2 of the location of button "template" into y
      set name of button "template" to "New Button"
      set label of button "New Button" to "New Button"
      add 30 to y
   end repeat
end mouseUp

I first create a local variable "y" and put the value 50 into it. This will be used to indicate how far down the first copied and pasted button should be placed.

I then have a block of code that I repeat three times just for illustration purposes. It's almost identical to the version 1 script, except that it takes into account the fact that buttons 2 and 3 have to be placed slightly further down on the card after they are pasted. So, I just arbitrarily added 30 to y at the end of each repeat. Button 2 is placed at location 100,80 and Button 3 is placed at location 100,110.

OK, this is all well and good, but how do I get unique names into each of the new buttons. Version 3 of the script (in the button "Copy/Paste Button") takes care of this by relying on a text field called "names." It looks at all of the names entered into the field and creates a unique button for each:

on mouseUp
   put 50 into y
   repeat with i = 1 to the number of lines in field "names"
      put line i of field "names" into varName
      copy button "template" on card "library" to this card
      set the location of button "template" to 100,y
      put item 2 of the location of button "template" into y
      set name of button "template" to varName
      set label of button varName to varName
   add 10+height of button "template" on card "library" to y
   end repeat
end mouseUp

Instead of just repeating three times, this script repeats for as many names that appear in the field. During each repeat loop, it puts the name of the next person on the list into a local variable called "varName," with help from another local variable "i" that is keeping track of which line of the text field (and hence which person's name) is being considered.

The button "Delete All Buttons" does just that - it deletes the buttons. This button relies on another field "previous names" to do this accurately. I refer below to the "previous names" field as a "shadow field" because it is hidden in the final version and works "in the shadows." The list of names is copied and pasted to the "previous names" field right before the group of buttons is created from the list of names. When the button group needs to be updated, the first thing I do is delete all of the existing buttons by referring to the "previous names" field. 

Features of the Final Version


OK, let's take a look at the final version of the app. The final version has a lot of other interesting features besides the copying and pasting of buttons with code.

Distributing the Buttons Evenly


I was tempted to take the easy way out and just have the buttons -- no matter how many -- begin displaying in the top left of the card and down until a first column filled, then start a second column. However, I decided it would be cool to distribute the buttons evenly in the designated space on the card. The designated space is the left half of the card. This was fun to do and created a little brain teaser for me involving some simple mathematics. A total of 10 buttons fit well in one column. So, if there were 10 or less buttons, I wanted those to be shown in a column in the exact middle of the designated space.

When there are more than 10 buttons, but no more than 20, then I figured that the spacing would need to be the width of the stack divided by 2, and then take and divide that number by 3, or (w/2)/3 where w is the width of the stack.

When there are more than 20 buttons, but no more than 30, then the spacing needs to be the width of the stack divided by 8.

When there are more than 30 buttons, but no more than 40, then the spacing needs to be the width of the stack divided by 16. However, the first column starts w/8 from the left edge of the screen.

I added some code to catch if more than 40 names or events are entered. If this happens, a short message pops up for 2 seconds to let the user know about the 40 event limit.

Use of "Shadow Fields"


I am finding myself using what I personally call "Shadow Fields" to facilitate projects such as this. Shadow fields are simply fields that help out various scripts in various ways, but they aren't visible to the user -- they are working "in the shadows."

I have two particular noteworthy examples at work in this app. The first is the field called "previous names" that I've already referred to in this post. As soon as the list of events is created and settled upon, I use the following script in the button "Generate Buttons" to copy the events from the field "names" and paste them into the field "previous names":

   put field "names" into field "previous names"

This acts as a way to preserve the current list of button labels, which is important for many of the app's scripts, such as when all the event buttons need to be deleted, as I've already mentioned.

The second shadow field I use in this application is called "datastreamSeconds," which works closely with the field "datastream," which is the field containing the data log (on the right side of the screen). The field "datastreamSeconds" is used to assist in figuring out the time in seconds for each event on the fly. For example, as you run the app, notice how the event in the last line shows "pending" for the time and then is updated with the number of seconds as soon as the next event is logged. The field ""datastreamSeconds" maintains a log of the time for each event in seconds using the function "seconds."At first, I tried keeping track of this information using a set of variables, but this proved too clunky, especially when you consider that I always give the user the option to erase the last entry. To compute the time for a particular event, I just subtract the time of one event with the next event. To see how it works, just turn on the visibility of the field "datastreamSeconds" using the Application Browser tool.

Generating a Summary Report


Like a lot of my projects, I like to rely on other tools, such as Excel, do the heavy lifting on other tasks or needs, such as generating reports. So, the list of events generated in this app is "Excel Ready" in the sense that one can copy and paste the lines in the field directly into Excel. Then, using the "Text to Columns" option in Excel, you can distribute the comma-separated data into individual columns. But, I thought it would be good to quickly give the user the most basic summary statistics: frequency of each event (i.e. person), and total time of each event. 

Here is the complete script for the button "Generate Report," with my explanation coming right after:

on mouseUp
   show field "report"
   hide me
   hide field "datastream"
   hide button "non-event"
   hide button "event marker"
   hide button "Edit Buttons"
   hide button "Erase Last Entry"
   hide button "Delete All Entries"
   show button "close report"
   put empty into field "report"
   put "Excel Ready Format" into line 1 of field report
   put "Button Label, Frequency, Total Time (Seconds)" into line 2 of field report
   repeat with i = 1 to the number of lines in field "previous names"
      put line i of field "previous names" into varTempName
     
      put 0 into varCount
      repeat with j = 2 to the number of lines in field "datastream"
         if item 4 of line j of field "datastream" = varTempName then add 1 to varCount
      end repeat

     
      put 0 into varTimeTotal
      repeat with k = 2 to the number of lines in field "datastream"-1
         if item 4 of line k of field "datastream" = varTempName then
            add item 3 of line k of field "datastream" to varTimeTotal
         end if
      end repeat

     
      put varTempName&","&varCount&","&varTimeTotal into line i+2 of field "report"
     
   end repeat
  
end mouseUp
There are three important loops here, each with its own repeat command. I color coded them to make them easier to see and understand. First, there is an overall loop (the start and end points color coded blue) that repeats for as many buttons there are. For example, let's imagine we have five people at a meeting: Tom, Dick, Harry, Susan, and Jane. Each person's name, in succession (i.e. once per loop), is put into a local variable "varTempName." Within this loop are two independent loops that look for that person's name. 

The first (color coded green) just looks to see how many times that person's name is listed in the field "datastream." The local variable "varCount" is incremented by one each time the name is found.

The second loop (color coded orange) also looks to see if the person's name is mentioned in each line of the field "datastream," but it then takes item 4 of the line containing the number of seconds for that event and adds it to another local variable called "varTimeTotal".

The second to last line of the blue loop puts the summary information for each person on a unique line in the field "report."

I also put this summary information in "Excel ready" format so that one could easily create graphs or charts using Excel.

Option to Sort the List


Sorting a list of information is amazingly easy to do, thanks to the fact that LiveCode has a "sort" command:

     sort field "names" ascending text

However, I wanted to give the user the option of sorting. So, I added a checkmark button called "sort." The "hilite" property is used detect whether the button is checked or unchecked (i.e. true or false):

   if the hilite of button "sort" is true then
      sort field "names" ascending text
   end if

Search and Destroy Empty Lines


I found that errors occurred if the user leaves any blank lines after editing the button list. In short, a blank line creates a button without a name, which is problematic for many reasons. So, I had to create some script to "search and destroy" any blank lines. Early on, I wrote this script in the "Generate Buttons":

   put 0 into c
   repeat with i = 1 to the number of lines in field "names"
      if line i of field "names" = "" then add 1 to c
   end repeat
   repeat c times
      delete line 1 of field "names"
   end repeat

This worked great, but only on the condition that the list is always sorted alphabetically first -- by doing so the blank lines "rise" to the top of the list. This script does not work if the list remains unsorted, an option I decided late in my design. So, I had to come up with a script that work would either way.

My first solution did not work, though I remain a little baffled as to why:

   repeat with i = 1 to the number of lines in field "names"
      if line i of field "names" = "" then delete line i of field "names"
   end repeat

This "kind of" works, but not reliably. It will catch a stray empty line, but if there are two or more empty lines in a row, it does not reliably delete them all. So, I came up with the following script that, although somewhat inelegant, seems to work great:

   put false into flag
   repeat until flag is true
      repeat with i = 1 to the number of lines in field "names"
         if line i of field "names" = "" then delete line i of field "names"
      end repeat
      put true into flag
      repeat with i = 1 to the number of lines in field "names"
         if line i of field "names" = "" then put false into flag
      end repeat
   end repeat

This again has a total of three loops, with again two loops inside of one loop. Basically, this combines my initial script with code to repeat so long as there continues to be a blank line in the list. First, I set a local variable "flag" to false and runs that first "search and destroy" script that almost works. Then I "put true into flag" with the hope that all blank lines are gone. But, I then run another loops that searches for blank lines. If it finds one, it sets flag back to false at which point the first loop (starting with "repeat until flag is true") runs again. It continues this until all blank lines are gone.

Interestingly, in testing this out, it never seems to take more than a total of 4 loops to "eradicate" all of the blank lines, no matter how many blank lines I include or where I include them. I obviously don't understand completely the nuances of the "delete line" command, but this script does the job nonetheless.

Final Thoughts


This app was yet another weekend project. I don't think I spent more than about 4 hours on it. This again speaks to the power of LiveCode as a prototyping tool. Obviously, much more work could be devoted to this app, its graphic design being just one example. As always, if I were to create a standalone application, I'd have to add the script that saves the data to a text file. But, when just left as a LiveCode file, the names and data log remain in their respective fields between sessions.

As I close this blog posting, you might be wondering how much I tend to dominate meetings. My humble response is that I am a model of restraint, no matter how spirited the conversation. (Yeah, right!) Well, at least I've proved in this post that I am thinking about my own talking and listening behaviors. And I hypothesize that if you reflect on whether or not you are talking too much during a meeting, you are likely to talk less and listen more. Perhaps I should collect some data to test this hypothesis.