Search code examples
user-interfacegroovybindableswingbuildercountdowntimer

Can @Bindable variable's change in different thread can be reflected the corresponding UI element?


I'm learning to implement a count down timer with GUI showing the time reduction. I'm using Groovy's @Bindable in the hope that the change of time reduction can be displayed automatically in the corresponding UI label.

The reduction of the count-down time value is done in the timer thread, separated from the UI thread. The countdown timer is not being updated in the UI, however.

What's the appropriate way to have the count-down time in the UI update properly?

import groovy.swing.SwingBuilder
import java.awt.FlowLayout as FL
import javax.swing.BoxLayout as BXL
import javax.swing.JFrame
import groovy.beans.Bindable
import java.util.timer.*  

// A count-down timer using Bindable to reflcet the reduction of time, when the reduction is done in a TimerTask thread

class CountDown {
  int delay = 5000   // delay for 5 sec.  
  int period = 60*1000  // repeat every minute.  
  int remainingTime = 25*60*1000
  // hope to be able to update the display of its change:
  @Bindable String timeStr = "25:00"
  public void timeString () {
    int seconds = ((int) (remainingTime / 1000))  % 60 ;
    int minutes =((int) (remainingTime / (1000*60))) % 60;
    timeStr = ((minutes < 9) ? "0" : "") + String.valueOf (minutes)  + ":" + ((seconds < 9) ? "0" : "") + String.valueOf (seconds)
  }
  public void update () {
    if (remainingTime >= period)
      remainingTime =  (remainingTime - period)
    // else // indicate the timer expires on the panel
    // println remainingTime
    // convert remainingTime to be minutes and secondes
    timeString()
    println timeStr // this shows that the TimerTaskCountDown thread is producting the right reduction to timeStr
  }
}

model = new CountDown()
class TimerTaskCountDown extends TimerTask {
  public TimerTaskCountDown (CountDown modelIn) {
    super()
    model = modelIn
  }
  CountDown model
  public void run() {
    model.update() // here change to model.timeStr does not reflected
  }  
}  

Timer timer = new Timer()  
timer.scheduleAtFixedRate(new TimerTaskCountDown(model), model.delay, model.period)

def s = new SwingBuilder()
s.setVariable('myDialog-properties',[:])
def vars = s.variables
def dial = s.dialog(title:'Pomodoro', id:'working', modal:true, 
                    // locationRelativeTo:ui.frame, owner:ui.frame, // to be embedded into Freeplane eventually
                    defaultCloseOperation:JFrame.DISPOSE_ON_CLOSE, pack:true, show:true) {
  panel() {
    boxLayout(axis:BXL.Y_AXIS)
    panel(alignmentX:0f) {
      flowLayout(alignment:FL.LEFT)
      label text: bind{"Pomodoro time: " + model.timeStr}
    }
    panel(alignmentX:0f) {
      flowLayout(alignment:FL.RIGHT)
      button(action: action(name: 'STOP', defaultButton: true, mnemonic: 'S',
                            closure: {model.timeStr = "stopped"; vars.ok = true//; dispose() // here the change to model.timeStr gets reflected in the label
                            }))
    }
  }
}

Solution

  • Yes, it can. Nutshell: call setTimeStr instead of setting the property directly.

    Bypassing the setter meant that none of the code added by @Bindable was being executed, so no property change notifications were being sent.

    Other edits include minor cleanup, noise removal, shortening delay to speed debugging, etc.

    import groovy.swing.SwingBuilder
    import java.awt.FlowLayout as FL
    import javax.swing.BoxLayout as BXL
    import javax.swing.JFrame
    import groovy.beans.Bindable
    import java.util.timer.*  
    
    class CountDown {
      int delay = 1000
      int period = 5 * 1000
      int remainingTime = 25 * 60 *1000
    
      @Bindable String timeStr = "25:00"
    
      public void timeString() {
        int seconds = ((int) (remainingTime / 1000))  % 60 ;
        int minutes =((int) (remainingTime / (1000*60))) % 60;
    
        // Here's the issue
        // timeStr = ((minutes < 9) ? "0" : "") + minutes + ":" + ((seconds < 9) ? "0" : "") + seconds
        setTimeStr(String.format("%02d:%02d", minutes, seconds))
      }
    
      public void update() {
        if (remainingTime >= period) {
          remainingTime -= period
        }
    
        timeString()
      }
    }
    
    class TimerTaskCountDown extends TimerTask {
      CountDown model
    
      public TimerTaskCountDown (CountDown model) {
        super()
        this.model = model
      }
    
      public void run() {
        model.update()
      }  
    }  
    
    model = new CountDown()
    ttcd = new TimerTaskCountDown(model)
    
    timer = new Timer()  
    timer.scheduleAtFixedRate(ttcd, model.delay, model.period)
    
    def s = new SwingBuilder()
    s.setVariable('myDialog-properties',[:])
    
    def dial = s.dialog(title:'Pomodoro', id:'working', modal:false,  defaultCloseOperation:JFrame.DISPOSE_ON_CLOSE, pack:true, show:true) {
      panel() {
        boxLayout(axis:BXL.Y_AXIS)
        panel(alignmentX:0f) {
          flowLayout(alignment:FL.LEFT)
          label text: bind { "Pomodoro time: " + model.timeStr }
        }
    
        panel(alignmentX:0f) {
          flowLayout(alignment:FL.RIGHT)
          button(action: action(name: 'STOP', defaultButton: true, mnemonic: 'S', closure: { model.timeStr = "stopped"; vars.ok = true }))
        }
      }
    }