add support for delayed starts for runs

This commit is contained in:
Gered 2015-12-02 14:27:26 -05:00
parent 20bd866eba
commit f37b331f39
4 changed files with 93 additions and 47 deletions

View file

@ -12,6 +12,7 @@ import java.beans.PropertyChangeSupport;
import java.io.IOException; import java.io.IOException;
import java.io.ObjectInputStream; import java.io.ObjectInputStream;
import java.io.Serializable; import java.io.Serializable;
import java.security.InvalidParameterException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -150,6 +151,13 @@ public class Run implements TableModel, Serializable {
*/ */
private transient TableModelSupport tmSupport; private transient TableModelSupport tmSupport;
/**
* Number of milliseconds to delay the clock for when the run is started. A
* non-zero value here means the clock starts at a negative time and counts
* up to zero at which point the run starts being timed normally.
*/
private long delayedStart;
private String goal; private String goal;
private boolean segmented; private boolean segmented;
@ -171,6 +179,7 @@ public class Run implements TableModel, Serializable {
throw new NullPointerException("null run name"); throw new NullPointerException("null run name");
} }
this.name = name; this.name = name;
this.delayedStart = 1000;
segments = new ArrayList<Segment>(); segments = new ArrayList<Segment>();
goal = ""; goal = "";
segmented = false; segmented = false;
@ -201,6 +210,10 @@ public class Run implements TableModel, Serializable {
return goal; return goal;
} }
public long getDelayedStart() {
return delayedStart;
}
public boolean isSegmented() { public boolean isSegmented() {
return segmented; return segmented;
} }
@ -585,6 +598,12 @@ public class Run implements TableModel, Serializable {
pcSupport.firePropertyChange(GOAL_PROPERTY, old, goal); pcSupport.firePropertyChange(GOAL_PROPERTY, old, goal);
} }
public void setDelayedStart(long delayedStart) {
if (delayedStart < 0)
throw new InvalidParameterException("negative delayed start");
this.delayedStart = delayedStart;
}
/** /**
* Inserts the given segment at the end. If it's the first segment being * Inserts the given segment at the end. If it's the first segment being
* added, the run becomes {@link State#READY}, meaning it can be started. * added, the run becomes {@link State#READY}, meaning it can be started.
@ -663,7 +682,7 @@ public class Run implements TableModel, Serializable {
if (state == null || state == State.ONGOING) { if (state == null || state == State.ONGOING) {
throw new IllegalStateException("illegal state to start"); throw new IllegalStateException("illegal state to start");
} }
startTime = System.nanoTime() / 1000000L; startTime = (System.nanoTime() / 1000000L) + delayedStart;
current = 0; current = 0;
state = State.ONGOING; state = State.ONGOING;
segments.get(current).setStartTime(startTime); segments.get(current).setStartTime(startTime);

View file

@ -1,42 +1,16 @@
package org.fenix.llanfair.dialog; package org.fenix.llanfair.dialog;
import java.awt.Component; import org.fenix.llanfair.*;
import java.awt.Dimension; import org.fenix.llanfair.config.Accuracy;
import java.awt.GridBagLayout; import org.fenix.utils.gui.GBC;
import java.awt.Rectangle;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import javax.swing.AbstractCellEditor; import javax.swing.*;
import javax.swing.DefaultCellEditor;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener; import javax.swing.event.ListSelectionListener;
import javax.swing.filechooser.FileNameExtensionFilter; import javax.swing.filechooser.FileNameExtensionFilter;
import javax.swing.table.DefaultTableCellRenderer; import javax.swing.table.*;
import javax.swing.table.JTableHeader; import java.awt.*;
import javax.swing.table.TableCellEditor; import java.awt.event.*;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import org.fenix.llanfair.Language;
import org.fenix.llanfair.Llanfair;
import org.fenix.llanfair.Run;
import org.fenix.llanfair.Segment;
import org.fenix.llanfair.Time;
import org.fenix.utils.gui.GBC;
/** /**
* Boîte de dialogue permettant lédition dune course. {@code EditDialog} * Boîte de dialogue permettant lédition dune course. {@code EditDialog}
@ -131,6 +105,8 @@ implements ActionListener, ListSelectionListener {
private JTextField runGoal; private JTextField runGoal;
private JTextField runDelayedStart;
// ----------------------------------------------------------- CONSTRUCTEURS // ----------------------------------------------------------- CONSTRUCTEURS
/** /**
@ -148,9 +124,12 @@ implements ActionListener, ListSelectionListener {
setTitle(Language.EDITING.get()); setTitle(Language.EDITING.get());
String delayedStartString = new Time(run.getDelayedStart()).toString(Accuracy.HUNDREDTH);
runTitle = new JTextField(run.getName(), 61); runTitle = new JTextField(run.getName(), 61);
runTitleLabel = new JLabel(Language.RUN_TITLE.get()); runTitleLabel = new JLabel(Language.RUN_TITLE.get());
runGoal = new JTextField(run.getGoal(), 48); runGoal = new JTextField(run.getGoal(), 48);
runDelayedStart = new JTextField(delayedStartString, 5);
segments = new JTable(run) { segments = new JTable(run) {
@Override protected JTableHeader createDefaultTableHeader() { @Override protected JTableHeader createDefaultTableHeader() {
return new JTableHeader(columnModel) { return new JTableHeader(columnModel) {
@ -192,14 +171,16 @@ implements ActionListener, ListSelectionListener {
*/ */
private void placeComponents() { private void placeComponents() {
setLayout(new GridBagLayout()); setLayout(new GridBagLayout());
add(runTitleLabel, GBC.grid(0, 0).insets(4, 4, 0, 4)); add(runTitleLabel, GBC.grid(0, 0).insets(4, 4, 0, 4).anchor(GBC.LE));
add(runTitle, GBC.grid(1, 0, 3, 1).insets(4, 0, 0, 4).anchor(GBC.LS)); add(runTitle, GBC.grid(1, 0, 3, 1).insets(4, 0, 0, 4).anchor(GBC.LS));
add(new JLabel("" + Language.LB_GOAL), GBC.grid(0, 1).insets(4, 4, 0, 4)); add(new JLabel("" + Language.LB_GOAL), GBC.grid(0, 1).insets(4, 4, 0, 4).anchor(GBC.LE));
add(runGoal, GBC.grid(1, 1).insets(4, 0, 0, 4).anchor(GBC.LS)); add(runGoal, GBC.grid(1, 1).insets(4, 0, 0, 4).anchor(GBC.LS));
add(segmented, GBC.grid(2, 1, 2, 1).insets(4, 0, 0, 4).anchor(GBC.LS)); add(new JLabel("Delayed Start"), GBC.grid(0, 2).insets(4, 4, 0, 4).anchor(GBC.LE));
add(segmentsLabel, GBC.grid(0, 2, 4, 1).insets(5, 4, 4, 0) add(runDelayedStart, GBC.grid(1, 2).insets(4, 0, 0, 4).anchor(GBC.LS));
.anchor(GBC.BL)); add(segmented, GBC.grid(2, 2, 2, 1).insets(4, 0, 0, 4).anchor(GBC.LS));
add(scrollPane, GBC.grid(0, 3, 3, 4).insets(0, 4, 0, 0)); add(segmentsLabel, GBC.grid(0, 3).insets(5, 4, 4, 0)
.anchor(GBC.LE));
add(scrollPane, GBC.grid(1, 3, 3, 4).insets(4, 4, 0, 0).anchor(GBC.LS));
add(addSegment, GBC.grid(3, 3).insets(0, 4).anchor(GBC.FLS)); add(addSegment, GBC.grid(3, 3).insets(0, 4).anchor(GBC.FLS));
add(remSegment, GBC.grid(3, 4).insets(4, 4).anchor(GBC.FLS)); add(remSegment, GBC.grid(3, 4).insets(4, 4).anchor(GBC.FLS));
add(moveUp, GBC.grid(3, 5).insets(0, 4).anchor(GBC.FLS)); add(moveUp, GBC.grid(3, 5).insets(0, 4).anchor(GBC.FLS));
@ -263,9 +244,35 @@ implements ActionListener, ListSelectionListener {
moveUp.setToolTipText("" + Language.TT_MOVE_SEGMENT_UP); moveUp.setToolTipText("" + Language.TT_MOVE_SEGMENT_UP);
segmented.setToolTipText("" + Language.TT_ED_SEGMENTED); segmented.setToolTipText("" + Language.TT_ED_SEGMENTED);
// parse the entered delayed start time whenever focus leaves the field
// (this lets us prompt up errors if the user enters something invalid immediately)
runDelayedStart.addFocusListener(new FocusAdapter() {
@Override
public void focusLost(FocusEvent e) {
long result = parseDelayedStartTime(runDelayedStart.getText());
if (result == -1)
runDelayedStart.setBackground(Color.RED);
else
runDelayedStart.setBackground(Color.WHITE);
}
});
updateButtons(); updateButtons();
} }
private long parseDelayedStartTime(String text) {
long result;
try {
Time time = new Time(text);
result = time.getMilliseconds();
} catch (Exception e) {
JOptionPane.showMessageDialog(this, e.getMessage(), Language.ERROR.get(), JOptionPane.ERROR_MESSAGE);
result = -1;
}
return result;
}
/** /**
* Procédure à invoquer lorsque ce composant réalise une action. Sont donc * Procédure à invoquer lorsque ce composant réalise une action. Sont donc
* capturés ici, tous les évènements daction des sous-composants, comme * capturés ici, tous les évènements daction des sous-composants, comme
@ -290,6 +297,10 @@ implements ActionListener, ListSelectionListener {
run.setName(runTitle.getText()); run.setName(runTitle.getText());
run.setGoal(runGoal.getText()); run.setGoal(runGoal.getText());
run.setSegmented(segmented.isSelected()); run.setSegmented(segmented.isSelected());
long delayedStart = parseDelayedStartTime(runDelayedStart.getText());
run.setDelayedStart(delayedStart == -1 ? 0 : delayedStart);
dispose(); dispose();
} else if (source.equals(cancel)) { } else if (source.equals(cancel)) {

View file

@ -59,6 +59,12 @@ class Core extends JPanel implements ActionListener {
*/ */
private static final int MIN_WIDTH = 50; private static final int MIN_WIDTH = 50;
/**
* Dummy object used so we can get a string format for zero time instead of new'ing up
* a Time object each time.
*/
private static final Time zeroTime = new Time(0);
// ------------------------------------------------------------- ATTRIBUTES // ------------------------------------------------------------- ATTRIBUTES
/** /**
@ -299,6 +305,11 @@ class Core extends JPanel implements ActionListener {
Time splitElapsed = new Time(now - run.getStartTime()); Time splitElapsed = new Time(now - run.getStartTime());
Time segmentElapsed = new Time(now - current.getStartTime()); Time segmentElapsed = new Time(now - current.getStartTime());
if (splitElapsed.getMilliseconds() < 0)
splitTimer.setForeground(Color.GRAY);
else
splitTimer.setForeground(Settings.colorTimer.get());
if (run.getState().equals(State.PAUSED)) { if (run.getState().equals(State.PAUSED)) {
splitTimer.setText("" + pauseTime); splitTimer.setText("" + pauseTime);
if (blinkTime == 0L || now - blinkTime >= 400L) { if (blinkTime == 0L || now - blinkTime >= 400L) {
@ -314,9 +325,14 @@ class Core extends JPanel implements ActionListener {
} }
blinkTime = now; blinkTime = now;
} }
} else {
if (splitElapsed.getMilliseconds() < 0) {
splitTimer.setText("-" + splitElapsed);
segmentTimer.setText("" + zeroTime);
} else { } else {
splitTimer.setText("" + splitElapsed); splitTimer.setText("" + splitElapsed);
segmentTimer.setText("" + segmentElapsed); segmentTimer.setText("" + segmentElapsed);
}
if (!splitLoss && splitElapsed.compareTo(splitTime) > 0) { if (!splitLoss && splitElapsed.compareTo(splitTime) > 0) {
splitLoss = true; splitLoss = true;

View file

@ -114,14 +114,14 @@ LB_FT_LIVE = Live:
LB_FT_SEGMENT = P.Se: LB_FT_SEGMENT = P.Se:
LB_FT_SPLIT = P.Sp: LB_FT_SPLIT = P.Sp:
ABOUT_MESSAGE = ABOUT_MESSAGE =
ERROR_OPENING = There was an error opening Â?{0},Â? the file may no longer exist, may be corrupted or incompatible or you may not be authorized to read in that folder. ERROR_OPENING = There was an error opening "{0}", the file may no longer exist, may be corrupted or incompatible or you may not be authorized to read in that folder.
ERROR_SAVING = There was an error saving Â?{0},Â? you may not be authorized to write inside this folder. Warning: The run was NOT saved ! ERROR_SAVING = There was an error saving "{0}", you may not be authorized to write inside this folder. Warning: The run was NOT saved !
ICON_TOO_BIG = ICON_TOO_BIG =
ILLEGAL_TIME = Your time cannot be lower than or equal to zero. ILLEGAL_TIME = Your time cannot be lower than or equal to zero.
ILLEGAL_SEGMENT_TIME = When setting a previously undefined segment time, the new time cannot exceed or equal the next segment time. ILLEGAL_SEGMENT_TIME = When setting a previously undefined segment time, the new time cannot exceed or equal the next segment time.
INPUT_NAN = Value of Â?{0}Â? must be a number. INPUT_NAN = Value of "{0}" must be a number.
INPUT_NEGATIVE = Value of Â?{0}Â? must be a positive number. INPUT_NEGATIVE = Value of "{0}" must be a positive number.
INVALID_TIME_STAMP = The input string Â?{0}Â? is not a valid time stamp. INVALID_TIME_STAMP = The input string "{0}" is not a valid time stamp.
WARN_BETTER_RUN = It seems that you've established a new personal best. Do you want to save your run? WARN_BETTER_RUN = It seems that you've established a new personal best. Do you want to save your run?
WARN_BETTER_TIMES = It seems that you've beaten some of your best segment times (up to your last split.) Do you want to save them? (The run itself won't be saved.) WARN_BETTER_TIMES = It seems that you've beaten some of your best segment times (up to your last split.) Do you want to save them? (The run itself won't be saved.)
WARN_RESET_SETTINGS = Are you sure you want to reset your settings? WARN_RESET_SETTINGS = Are you sure you want to reset your settings?