Domanda

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
                            }))
    }
  }
}
È stato utile?

Soluzione

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 }))
    }
  }
}

Altri suggerimenti

Here is the solution that I found by studying on Stackoverflow. I adapted from the example of stop timer. The key is to use Swing Timer instead of the generic timer, and Listener interface for the timer value display panel.

My previous attempt to use @Bindable would still work, but it requires all setting to the bindable timeStr through setTimeStr routine. (Thanks to Dave's help!)

Stackoverflow is great place to learn.

Here is the code.

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.text.DecimalFormat;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.Timer;

/** @following the example of http://stackoverflow.com/questions/2576909 */
/** adapted for count-down timer */
public class JTimeLabel extends JLabel implements ActionListener {

  private static final String Start = "Start";
  private static final String Stop = "Stop";
  private DecimalFormat df = new DecimalFormat("000.0");
  private Timer timer = new javax.swing.Timer(100, this);

  private int countDownMinutes = 25;
  private long countDownMillis = 25*60*1000;
  private long expireMillis = countDownMillis + System.currentTimeMillis();

  public JTimeLabel() {
    this.setHorizontalAlignment(JLabel.CENTER);
    this.setText(when());
  }

  public void actionPerformed(ActionEvent ae) {// this is for update the timer value
    setText(when());
  }

  public void start() { // reset the expiration time and start the timer
    expireMillis = countDownMillis + System.currentTimeMillis();
    timer.start();
  }

  public void stop() {
    timer.stop();
  }

  private String when() {// show count-down timer value
    if (expireMillis > System.currentTimeMillis()) {
      long remainingMillis = expireMillis - System.currentTimeMillis()
      int seconds = ((int) (remainingMillis / 1000))  % 60 ;
      int minutes =((int) (remainingMillis / (1000*60))) % 60;
      return (String.format("%02d:%02d", minutes, seconds))
    } else {// handle the completion of the count-down timer
      timer.stop ();
      return "00:00"
    }   
  } 

  private static void create() {
    JFrame f = new JFrame();
    f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    final JTimeLabel jtl = new JTimeLabel();
    jtl.setFont(new Font("Dialog", Font.BOLD, 32));
    f.add(jtl, BorderLayout.CENTER);

    final JButton button = new JButton(Stop);
    button.addActionListener(new ActionListener() {
                               public void actionPerformed(ActionEvent e) {
                                 String cmd = e.getActionCommand();
                                 if (Stop.equals(cmd)) {
                                   jtl.stop();
                                   button.setText(Start);
                                 } else {
                                   jtl.start();
                                   button.setText(Stop);
                                 }

                               }
                             });
    f.add(button, BorderLayout.SOUTH);
    f.pack();
    f.setVisible(true);
    jtl.start();
  }

  public static void main(String[] args) {
    EventQueue.invokeLater(new Runnable() {
                             public void run() {
                               create();
                             }
                           });
  }
}
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top