diff --git a/src/main/java/org/fenix/llanfair/Actions.java b/src/main/java/org/fenix/llanfair/Actions.java index 4a2e355..12b5219 100644 --- a/src/main/java/org/fenix/llanfair/Actions.java +++ b/src/main/java/org/fenix/llanfair/Actions.java @@ -2,23 +2,6 @@ package org.fenix.llanfair; import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.io.xml.DomDriver; -import java.awt.Dimension; -import java.awt.event.ActionEvent; -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.FileReader; -import java.io.InputStream; -import java.io.ObjectInputStream; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.ResourceBundle; -import javax.swing.Action; -import javax.swing.JFileChooser; -import javax.swing.JOptionPane; import org.fenix.llanfair.config.Settings; import org.fenix.llanfair.dialog.EditRun; import org.fenix.llanfair.dialog.EditSettings; @@ -26,6 +9,14 @@ import org.fenix.llanfair.extern.WSplit; import org.fenix.utils.about.AboutDialog; import org.jnativehook.keyboard.NativeKeyEvent; +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.io.*; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ResourceBundle; + /** * Regroups all actions, in the meaning of {@link Action}, used by Llanfair. * All inputs and menu items callbacks are processed by this delegate to @@ -35,410 +26,410 @@ import org.jnativehook.keyboard.NativeKeyEvent; * @version 1.0 */ final class Actions { - - private static final long GHOST_DELAY = 300L; - private static ResourceBundle BUNDLE = null; - - private Llanfair master; - - private File file; - private JFileChooser fileChooser; - - private volatile long lastUnsplit; - private volatile long lastSkip; - /** - * Creates a new delegate. This constructor is package private since it - * only need be called by the main class. - * - * @param owner the Llanfair instance owning this delegate - */ - Actions( Llanfair owner ) { - assert ( owner != null ); - master = owner; - - file = null; - fileChooser = new JFileChooser( "." ); - - lastUnsplit = 0L; - lastSkip = 0L; - - if ( BUNDLE == null ) { - BUNDLE = Llanfair.getResources().getBundle( "llanfair" ); - } - } + private static final long GHOST_DELAY = 300L; + private static ResourceBundle BUNDLE = null; - /** - * Processes the given native key event. This method must be called from - * a thread to prevent possible deadlock. - * - * @param event the native key event to process - */ - void process( NativeKeyEvent event ) { - assert ( event != null ); - - int keyCode = event.getKeyCode(); - Run run = master.getRun(); - Run.State state = run.getState(); - - if ( keyCode == Settings.KEY_SPLT.get() ) { - split(); - } else if ( keyCode == Settings.KEY_RSET.get() ) { - reset(); - } else if ( keyCode == Settings.KEY_USPL.get() ) { - unsplit(); - } else if ( keyCode == Settings.KEY_SKIP.get() ) { - skip(); - } else if ( keyCode == Settings.KEY_STOP.get() ) { - if ( state == Run.State.ONGOING ) { - run.stop(); - } - } else if ( keyCode == Settings.KEY_PAUS.get() ) { - if ( state == Run.State.ONGOING ) { - run.pause(); - } else if ( state == Run.State.PAUSED ) { - run.resume(); - } - } else if ( keyCode == Settings.KEY_LOCK.get() ) { - master.setIgnoreNativeInputs( !master.ignoresNativeInputs() ); - } - } - - /** - * Processes the given action event. It is assumed here that the action - * event is one triggered by a menu item. This method must be called from - * a thread to prevent possible deadlock. - * - * @param event the event to process - */ - void process( ActionEvent event ) { - assert ( event != null ); - - Run run = master.getRun(); - MenuItem source = ( MenuItem ) event.getSource(); - - if ( source == MenuItem.EDIT ) { - EditRun dialog = new EditRun( run ); - dialog.display( true, master ); - } else if ( source == MenuItem.NEW ) { - if ( confirmOverwrite() ) { - master.setRun( new Run() ); - } - } else if ( source == MenuItem.OPEN ) { - open( null ); - } else if ( source == MenuItem.OPEN_RECENT ) { - open( new File( event.getActionCommand() ) ); - } else if ( source == MenuItem.IMPORT ) { - imprt(); - } else if ( source == MenuItem.SAVE ) { - run.saveLiveTimes( !run.isPersonalBest() ); - run.reset(); - save(); - } else if ( source == MenuItem.SAVE_AS ) { - file = null; - save(); - } else if ( source == MenuItem.RESET ) { - reset(); - } else if ( source == MenuItem.LOCK ) { - master.setIgnoreNativeInputs( true ); - } else if ( source == MenuItem.UNLOCK ) { - master.setIgnoreNativeInputs( false ); - } else if ( source == MenuItem.SETTINGS ) { - EditSettings dialog = new EditSettings(); - dialog.display( true, master ); - } else if ( source == MenuItem.ABOUT ) { - about(); - } else if ( source == MenuItem.EXIT ) { - if ( confirmOverwrite() ) { - master.dispose(); - } - } - } - - /** - * Performs a split or starts the run if it is ready. Can also resume a - * paused run in case the run is segmented. - */ - private void split() { - Run run = master.getRun(); - Run.State state = run.getState(); - if ( state == Run.State.ONGOING ) { - long milli = System.nanoTime() / 1000000L; - long start = run.getSegment( run.getCurrent() ).getStartTime(); - if ( milli - start > GHOST_DELAY ) { - run.split(); - } - } else if ( state == Run.State.READY ) { - run.start(); - } else if ( state == Run.State.PAUSED && run.isSegmented() ) { - run.resume(); - } - } - - /** - * Resets the current run to a ready state. If the user asked to be warned - * a pop-up will ask confirmation in case some live times are better. - */ - private void reset() { - Run run = master.getRun(); - if ( run.getState() != Run.State.NULL ) { - if ( !Settings.GNR_WARN.get() || confirmOverwrite() ) { - run.reset(); - } - } - } - - /** - * Performs an "unsplit" on the current run. If a split has been made, it - * is canceled and the time that passed after said split is added back to - * the timer, as if the split had not taken place. - */ - private void unsplit() { - Run run = master.getRun(); - Run.State state = run.getState(); - if ( state == Run.State.ONGOING || state == Run.State.STOPPED ) { - long milli = System.nanoTime() / 1000000L; - if ( milli - lastUnsplit > GHOST_DELAY ) { - lastUnsplit = milli; - run.unsplit(); - } - } - } - - /** - * Skips the current split in the run. Skipping a split sets an undefined - * time for the current segment and merges the live time of the current - * segment with the following one. - */ - private void skip() { - Run run = master.getRun(); - if ( run.getState() == Run.State.ONGOING ) { - long milli = System.nanoTime() / 1000000L; - if ( milli - lastSkip > GHOST_DELAY ) { - lastSkip = milli; - run.skip(); - } - } - } - - /** - * Displays a dialog to let the user select a file. The user is able to - * cancel this action, which results in a {@code null} being returned. - * - * @return a file selected by the user or {@code null} if he canceled - */ - private File selectFile() { - int option = fileChooser.showDialog( master, - Language.action_accept.get() ); - - if ( option == JFileChooser.APPROVE_OPTION ) { - return fileChooser.getSelectedFile(); - } else { - return null; - } - } - - /** - * Asks the user to confirm the discard of the current run. The popup - * window will only trigger if the current run has not been saved after - * some editing or if the run presents better times. - * - * @return {@code true} if the user wants to discard the run - */ - private boolean confirmOverwrite() { - boolean before = master.ignoresNativeInputs(); - master.setIgnoreNativeInputs( true ); - - Run run = master.getRun(); - boolean betterRun = run.isPersonalBest(); - boolean betterSgt = run.hasSegmentsBest(); + private Llanfair master; - if ( betterRun || betterSgt ) { - String message = betterRun - ? Language.WARN_BETTER_RUN.get() - : Language.WARN_BETTER_TIMES.get(); - - int option = JOptionPane.showConfirmDialog( master, message, - Language.WARNING.get(), JOptionPane.YES_NO_CANCEL_OPTION, - JOptionPane.WARNING_MESSAGE ); - - if ( option == JOptionPane.CANCEL_OPTION ) { - master.setIgnoreNativeInputs( false ); - return false; - } else if ( option == JOptionPane.YES_OPTION ) { - run.saveLiveTimes( !betterRun ); - run.reset(); - save(); - } - } - master.setIgnoreNativeInputs( before ); - return true; - } - - /** - * Opens the given file. If the file is {@code null}, the user is asked - * to select one. Before anything is done, the user is also asked for - * a confirmation if the current run has not been saved. - * - * @param file the file to open - */ - void open( File file ) { - if ( !confirmOverwrite() ) { - return; - } - if ( file == null ) { - if ( ( file = selectFile() ) == null ) { - return; - } - } - this.file = file; - String name = file.getName(); - try { - open(); - } catch ( Exception ex ) { - master.showError( Language.error_read_file.get( name ) ); - this.file = null; - } - } - - /** - * Opens the currently selected file. This method will first try to open - * the file using the new method (XStream XML) and if it fails will try to - * use the legacy method (Java ObjectStream.) - * - * @throws Exception if the reading operation fails - */ - private void open() throws Exception { - BufferedInputStream in = null; - try { - in = new BufferedInputStream( new FileInputStream( file ) ); - try { - in.mark( Integer.MAX_VALUE ); - xmlRead( in ); - } catch ( Exception ex ) { - in.reset(); - legacyRead( new ObjectInputStream( in ) ); - } - MenuItem.recentlyOpened( "" + file ); - } catch ( Exception ex ) { - throw ex; - } finally { - try { - in.close(); - } catch ( Exception ex ) { - //$FALL-THROUGH$ - } - } - } - - /** - * Reads a stream on a run file using the new (since 1.5) XML method. This - * method will not be able to read a legacy run file and will throw an - * exception if confronted with such run file. - * - * @param in the input stream on the run file - */ - private void xmlRead( InputStream in ) { - XStream xml = new XStream( new DomDriver() ); - master.setRun( ( Run ) xml.fromXML( in ) ); - } - - /** - * Reads a stream on a run file using the legacy Java method. This method - * might fail if the given file is not a Llanfair run. - * - * @param in an input stream on the run file - * @throws Exception if the stream cannot be read - */ - private void legacyRead( ObjectInputStream in ) throws Exception { - master.setRun( ( Run ) in.readObject() ); - try { - Settings.GNR_SIZE.set( ( Dimension ) in.readObject(), true ); - } catch ( Exception ex ) { - // $FALL-THROUGH$ - } - } - - /** - * Saves the currently opened run to the currently selected file. If no - * file has been selected, the user is asked for one. - */ - private void save() { - if ( file == null ) { - if ( ( file = selectFile() ) == null ) { - return; - } - } - Settings.GNR_COOR.set( master.getLocationOnScreen(), true ); - Settings.GNR_SIZE.set( master.getSize(), true ); - - String name = file.getName(); - BufferedOutputStream out = null; - try { - XStream xml = new XStream( new DomDriver() ); - out = new BufferedOutputStream( new FileOutputStream( file ) ); - xml.toXML( master.getRun(), out ); - } catch ( Exception ex ) { - master.showError( Language.error_write_file.get( name ) ); - } finally { - try { - out.close(); - } catch ( Exception ex ) { - // $FALL-THROUGH$ - } - } - } - - /** - * Imports a run from another timer application. If no file has been - * selected, the user is asked for one. As of now, only WSplit run files - * are supported. - */ - private void imprt() { - if ( !confirmOverwrite() ) { - return; - } - if ( file == null ) { - if ( ( file = selectFile() ) == null ) { - return; - } - } - String name = file.getName(); - BufferedReader in = null; - try { - in = new BufferedReader( new FileReader( file ) ); - WSplit.parse( master, in ); - } catch ( Exception ex ) { - master.showError( Language.error_import_run.get( name ) ); - } finally { - try { - in.close(); - } catch ( Exception ex ) { - // $FALL-THROUGH$ - } - } - } - - /** - * Displays the "about" dialog. The dialog displays the version of Llanfair, - * the creative commons licence, the credits of development, a link to - * Llanfair's website and a link to donate. - */ - private void about() { - AboutDialog dialog = new AboutDialog( - master, Language.title_about.get() ); - dialog.setMessage( BUNDLE.getString( "about" )); - try { - dialog.setWebsite( new URL( BUNDLE.getString( "website" ) ) ); - } catch ( MalformedURLException ex ) { - // $FALL-THROUGH$ - } - try { - dialog.setDonateLink( new URL( BUNDLE.getString( "donate" ) ), - Llanfair.getResources().getIcon( "donate" ) ); - } catch ( MalformedURLException ex ) { - // $FALL-THROUGH$ - } - dialog.display(); - } + private File file; + private JFileChooser fileChooser; + + private volatile long lastUnsplit; + private volatile long lastSkip; + + /** + * Creates a new delegate. This constructor is package private since it + * only need be called by the main class. + * + * @param owner the Llanfair instance owning this delegate + */ + Actions( Llanfair owner ) { + assert ( owner != null ); + master = owner; + + file = null; + fileChooser = new JFileChooser( "." ); + + lastUnsplit = 0L; + lastSkip = 0L; + + if ( BUNDLE == null ) { + BUNDLE = Llanfair.getResources().getBundle( "llanfair" ); + } + } + + /** + * Processes the given native key event. This method must be called from + * a thread to prevent possible deadlock. + * + * @param event the native key event to process + */ + void process( NativeKeyEvent event ) { + assert ( event != null ); + + int keyCode = event.getKeyCode(); + Run run = master.getRun(); + Run.State state = run.getState(); + + if ( keyCode == Settings.KEY_SPLT.get() ) { + split(); + } else if ( keyCode == Settings.KEY_RSET.get() ) { + reset(); + } else if ( keyCode == Settings.KEY_USPL.get() ) { + unsplit(); + } else if ( keyCode == Settings.KEY_SKIP.get() ) { + skip(); + } else if ( keyCode == Settings.KEY_STOP.get() ) { + if ( state == Run.State.ONGOING ) { + run.stop(); + } + } else if ( keyCode == Settings.KEY_PAUS.get() ) { + if ( state == Run.State.ONGOING ) { + run.pause(); + } else if ( state == Run.State.PAUSED ) { + run.resume(); + } + } else if ( keyCode == Settings.KEY_LOCK.get() ) { + master.setIgnoreNativeInputs( !master.ignoresNativeInputs() ); + } + } + + /** + * Processes the given action event. It is assumed here that the action + * event is one triggered by a menu item. This method must be called from + * a thread to prevent possible deadlock. + * + * @param event the event to process + */ + void process( ActionEvent event ) { + assert ( event != null ); + + Run run = master.getRun(); + MenuItem source = ( MenuItem ) event.getSource(); + + if ( source == MenuItem.EDIT ) { + EditRun dialog = new EditRun( run ); + dialog.display( true, master ); + } else if ( source == MenuItem.NEW ) { + if ( confirmOverwrite() ) { + master.setRun( new Run() ); + } + } else if ( source == MenuItem.OPEN ) { + open( null ); + } else if ( source == MenuItem.OPEN_RECENT ) { + open( new File( event.getActionCommand() ) ); + } else if ( source == MenuItem.IMPORT ) { + imprt(); + } else if ( source == MenuItem.SAVE ) { + run.saveLiveTimes( !run.isPersonalBest() ); + run.reset(); + save(); + } else if ( source == MenuItem.SAVE_AS ) { + file = null; + save(); + } else if ( source == MenuItem.RESET ) { + reset(); + } else if ( source == MenuItem.LOCK ) { + master.setIgnoreNativeInputs( true ); + } else if ( source == MenuItem.UNLOCK ) { + master.setIgnoreNativeInputs( false ); + } else if ( source == MenuItem.SETTINGS ) { + EditSettings dialog = new EditSettings(); + dialog.display( true, master ); + } else if ( source == MenuItem.ABOUT ) { + about(); + } else if ( source == MenuItem.EXIT ) { + if ( confirmOverwrite() ) { + master.dispose(); + } + } + } + + /** + * Performs a split or starts the run if it is ready. Can also resume a + * paused run in case the run is segmented. + */ + private void split() { + Run run = master.getRun(); + Run.State state = run.getState(); + if ( state == Run.State.ONGOING ) { + long milli = System.nanoTime() / 1000000L; + long start = run.getSegment( run.getCurrent() ).getStartTime(); + if ( milli - start > GHOST_DELAY ) { + run.split(); + } + } else if ( state == Run.State.READY ) { + run.start(); + } else if ( state == Run.State.PAUSED && run.isSegmented() ) { + run.resume(); + } + } + + /** + * Resets the current run to a ready state. If the user asked to be warned + * a pop-up will ask confirmation in case some live times are better. + */ + private void reset() { + Run run = master.getRun(); + if ( run.getState() != Run.State.NULL ) { + if ( !Settings.GNR_WARN.get() || confirmOverwrite() ) { + run.reset(); + } + } + } + + /** + * Performs an "unsplit" on the current run. If a split has been made, it + * is canceled and the time that passed after said split is added back to + * the timer, as if the split had not taken place. + */ + private void unsplit() { + Run run = master.getRun(); + Run.State state = run.getState(); + if ( state == Run.State.ONGOING || state == Run.State.STOPPED ) { + long milli = System.nanoTime() / 1000000L; + if ( milli - lastUnsplit > GHOST_DELAY ) { + lastUnsplit = milli; + run.unsplit(); + } + } + } + + /** + * Skips the current split in the run. Skipping a split sets an undefined + * time for the current segment and merges the live time of the current + * segment with the following one. + */ + private void skip() { + Run run = master.getRun(); + if ( run.getState() == Run.State.ONGOING ) { + long milli = System.nanoTime() / 1000000L; + if ( milli - lastSkip > GHOST_DELAY ) { + lastSkip = milli; + run.skip(); + } + } + } + + /** + * Displays a dialog to let the user select a file. The user is able to + * cancel this action, which results in a {@code null} being returned. + * + * @return a file selected by the user or {@code null} if he canceled + */ + private File selectFile() { + int option = fileChooser.showDialog( master, + Language.action_accept.get() ); + + if ( option == JFileChooser.APPROVE_OPTION ) { + return fileChooser.getSelectedFile(); + } else { + return null; + } + } + + /** + * Asks the user to confirm the discard of the current run. The popup + * window will only trigger if the current run has not been saved after + * some editing or if the run presents better times. + * + * @return {@code true} if the user wants to discard the run + */ + private boolean confirmOverwrite() { + boolean before = master.ignoresNativeInputs(); + master.setIgnoreNativeInputs( true ); + + Run run = master.getRun(); + boolean betterRun = run.isPersonalBest(); + boolean betterSgt = run.hasSegmentsBest(); + + if ( betterRun || betterSgt ) { + String message = betterRun + ? Language.WARN_BETTER_RUN.get() + : Language.WARN_BETTER_TIMES.get(); + + int option = JOptionPane.showConfirmDialog( master, message, + Language.WARNING.get(), JOptionPane.YES_NO_CANCEL_OPTION, + JOptionPane.WARNING_MESSAGE ); + + if ( option == JOptionPane.CANCEL_OPTION ) { + master.setIgnoreNativeInputs( false ); + return false; + } else if ( option == JOptionPane.YES_OPTION ) { + run.saveLiveTimes( !betterRun ); + run.reset(); + save(); + } + } + master.setIgnoreNativeInputs( before ); + return true; + } + + /** + * Opens the given file. If the file is {@code null}, the user is asked + * to select one. Before anything is done, the user is also asked for + * a confirmation if the current run has not been saved. + * + * @param file the file to open + */ + void open( File file ) { + if ( !confirmOverwrite() ) { + return; + } + if ( file == null ) { + if ( ( file = selectFile() ) == null ) { + return; + } + } + this.file = file; + String name = file.getName(); + try { + open(); + } catch ( Exception ex ) { + master.showError( Language.error_read_file.get( name ) ); + this.file = null; + } + } + + /** + * Opens the currently selected file. This method will first try to open + * the file using the new method (XStream XML) and if it fails will try to + * use the legacy method (Java ObjectStream.) + * + * @throws Exception if the reading operation fails + */ + private void open() throws Exception { + BufferedInputStream in = null; + try { + in = new BufferedInputStream( new FileInputStream( file ) ); + try { + in.mark( Integer.MAX_VALUE ); + xmlRead( in ); + } catch ( Exception ex ) { + in.reset(); + legacyRead( new ObjectInputStream( in ) ); + } + MenuItem.recentlyOpened( "" + file ); + } catch ( Exception ex ) { + throw ex; + } finally { + try { + in.close(); + } catch ( Exception ex ) { + //$FALL-THROUGH$ + } + } + } + + /** + * Reads a stream on a run file using the new (since 1.5) XML method. This + * method will not be able to read a legacy run file and will throw an + * exception if confronted with such run file. + * + * @param in the input stream on the run file + */ + private void xmlRead( InputStream in ) { + XStream xml = new XStream( new DomDriver() ); + master.setRun( ( Run ) xml.fromXML( in ) ); + } + + /** + * Reads a stream on a run file using the legacy Java method. This method + * might fail if the given file is not a Llanfair run. + * + * @param in an input stream on the run file + * @throws Exception if the stream cannot be read + */ + private void legacyRead( ObjectInputStream in ) throws Exception { + master.setRun( ( Run ) in.readObject() ); + try { + Settings.GNR_SIZE.set( ( Dimension ) in.readObject(), true ); + } catch ( Exception ex ) { + // $FALL-THROUGH$ + } + } + + /** + * Saves the currently opened run to the currently selected file. If no + * file has been selected, the user is asked for one. + */ + private void save() { + if ( file == null ) { + if ( ( file = selectFile() ) == null ) { + return; + } + } + Settings.GNR_COOR.set( master.getLocationOnScreen(), true ); + Settings.GNR_SIZE.set( master.getSize(), true ); + + String name = file.getName(); + BufferedOutputStream out = null; + try { + XStream xml = new XStream( new DomDriver() ); + out = new BufferedOutputStream( new FileOutputStream( file ) ); + xml.toXML( master.getRun(), out ); + } catch ( Exception ex ) { + master.showError( Language.error_write_file.get( name ) ); + } finally { + try { + out.close(); + } catch ( Exception ex ) { + // $FALL-THROUGH$ + } + } + } + + /** + * Imports a run from another timer application. If no file has been + * selected, the user is asked for one. As of now, only WSplit run files + * are supported. + */ + private void imprt() { + if ( !confirmOverwrite() ) { + return; + } + if ( file == null ) { + if ( ( file = selectFile() ) == null ) { + return; + } + } + String name = file.getName(); + BufferedReader in = null; + try { + in = new BufferedReader( new FileReader( file ) ); + WSplit.parse( master, in ); + } catch ( Exception ex ) { + master.showError( Language.error_import_run.get( name ) ); + } finally { + try { + in.close(); + } catch ( Exception ex ) { + // $FALL-THROUGH$ + } + } + } + + /** + * Displays the "about" dialog. The dialog displays the version of Llanfair, + * the creative commons licence, the credits of development, a link to + * Llanfair's website and a link to donate. + */ + private void about() { + AboutDialog dialog = new AboutDialog( + master, Language.title_about.get() ); + dialog.setMessage( BUNDLE.getString( "about" )); + try { + dialog.setWebsite( new URL( BUNDLE.getString( "website" ) ) ); + } catch ( MalformedURLException ex ) { + // $FALL-THROUGH$ + } + try { + dialog.setDonateLink( new URL( BUNDLE.getString( "donate" ) ), + Llanfair.getResources().getIcon( "donate" ) ); + } catch ( MalformedURLException ex ) { + // $FALL-THROUGH$ + } + dialog.display(); + } } diff --git a/src/main/java/org/fenix/llanfair/Counters.java b/src/main/java/org/fenix/llanfair/Counters.java index 64e4c47..a301c23 100644 --- a/src/main/java/org/fenix/llanfair/Counters.java +++ b/src/main/java/org/fenix/llanfair/Counters.java @@ -1,171 +1,169 @@ package org.fenix.llanfair; +import org.fenix.utils.TableModelSupport; + +import javax.swing.*; +import javax.swing.event.TableModelListener; +import javax.swing.table.TableModel; import java.io.Serializable; import java.util.ArrayList; import java.util.List; -import javax.swing.Icon; -import javax.swing.event.TableModelListener; -import javax.swing.table.TableModel; -import org.fenix.llanfair.Language; - -import org.fenix.utils.TableModelSupport; - /** * * @author Xavier "Xunkar" Sencert */ public class Counters implements TableModel, Serializable { - - // -------------------------------------------------------------- CONSTANTES - - public static final long serialVersionUID = 1000L; - - public static final int COLUMN_ICON = 0; - - public static final int COLUMN_NAME = 1; - - public static final int COLUMN_START = 2; - - public static final int COLUMN_INCREMENT = 3; - - public static final int COLUMN_COUNT = 4; - - // -------------------------------------------------------------- ATTRIBUTS - - private List data; - - private TableModelSupport tmSupport; - - // ---------------------------------------------------------- CONSTRUCTEURS - - public Counters() { - data = new ArrayList(); - tmSupport = new TableModelSupport(this); - } - - public int getColumnCount() { - return COLUMN_COUNT; - } - - public int getRowCount() { - return data.size(); - } - - public Object getValueAt(int rowIndex, int columnIndex) { - if (rowIndex < 0 || rowIndex >= data.size()) { - throw new IllegalArgumentException("illegal counter id " + rowIndex); - } - return data.get(rowIndex).get(columnIndex); - } - - public String getColumnName(int columnIndex) { - switch (columnIndex) { - case COLUMN_ICON: return "" + Language.ICON; - case COLUMN_INCREMENT: return "" + Language.INCREMENT; - case COLUMN_NAME: return "" + Language.NAME; - case COLUMN_START: return "" + Language.START_VALUE; - } - return null; - } - - public Class getColumnClass(int columnIndex) { - switch (columnIndex) { - case COLUMN_ICON: return Icon.class; - case COLUMN_INCREMENT: return Integer.class; - case COLUMN_NAME: return String.class; - case COLUMN_START: return Integer.class; - } - return null; - } - - public boolean isCellEditable(int rowIndex, int columnIndex) { - return true; - } - - public void setValueAt(Object aValue, int rowIndex, int columnIndex) { - if (rowIndex < 0 || rowIndex >= data.size()) { - throw new IllegalArgumentException("illegal counter id " + rowIndex); - } - data.get(rowIndex).set(columnIndex, aValue); - } - - public void addTableModelListener(TableModelListener l) { - tmSupport.addTableModelListener(l); - } - - public void removeTableModelListener(TableModelListener l) { - tmSupport.removeTableModelListener(l); - } - - // ----------------------------------------------------------- CLASSES - - public static class Counter implements Serializable { - - public static final long serialVersionUID = 1000L; - - private String name; - - private Icon icon; - - private int increment; - - private int start; - - private int saved; - - private int live; - - public Counter() { - name = "" + Language.UNTITLED; - icon = null; - start = 0; - live = 0; - increment = 1; - } - - public Object get(int columnIndex) { - switch (columnIndex) { - case COLUMN_ICON: return icon; - case COLUMN_INCREMENT: return increment; - case COLUMN_NAME: return name; - case COLUMN_START: return start; - default: return name; - } - } - - public void set(int columnIndex, Object value) { - switch (columnIndex) { - case COLUMN_ICON: icon = (Icon) value; break; - case COLUMN_INCREMENT: increment = (Integer) value; break; - case COLUMN_NAME: name = (String) value; break; - case COLUMN_START: start = (Integer) value; break; - } - } - - public Icon getIcon() { - return icon; - } - - public String getName() { - return name; - } - - public int getLive() { - return live; - } - - public int getStart() { - return start; - } - - public int getSaved() { - return saved; - } - - public void nextStep() { - live += increment; - } - } + + // -------------------------------------------------------------- CONSTANTES + + public static final long serialVersionUID = 1000L; + + public static final int COLUMN_ICON = 0; + + public static final int COLUMN_NAME = 1; + + public static final int COLUMN_START = 2; + + public static final int COLUMN_INCREMENT = 3; + + public static final int COLUMN_COUNT = 4; + + // -------------------------------------------------------------- ATTRIBUTS + + private List data; + + private TableModelSupport tmSupport; + + // ---------------------------------------------------------- CONSTRUCTEURS + + public Counters() { + data = new ArrayList(); + tmSupport = new TableModelSupport(this); + } + + public int getColumnCount() { + return COLUMN_COUNT; + } + + public int getRowCount() { + return data.size(); + } + + public Object getValueAt(int rowIndex, int columnIndex) { + if (rowIndex < 0 || rowIndex >= data.size()) { + throw new IllegalArgumentException("illegal counter id " + rowIndex); + } + return data.get(rowIndex).get(columnIndex); + } + + public String getColumnName(int columnIndex) { + switch (columnIndex) { + case COLUMN_ICON: return "" + Language.ICON; + case COLUMN_INCREMENT: return "" + Language.INCREMENT; + case COLUMN_NAME: return "" + Language.NAME; + case COLUMN_START: return "" + Language.START_VALUE; + } + return null; + } + + public Class getColumnClass(int columnIndex) { + switch (columnIndex) { + case COLUMN_ICON: return Icon.class; + case COLUMN_INCREMENT: return Integer.class; + case COLUMN_NAME: return String.class; + case COLUMN_START: return Integer.class; + } + return null; + } + + public boolean isCellEditable(int rowIndex, int columnIndex) { + return true; + } + + public void setValueAt(Object aValue, int rowIndex, int columnIndex) { + if (rowIndex < 0 || rowIndex >= data.size()) { + throw new IllegalArgumentException("illegal counter id " + rowIndex); + } + data.get(rowIndex).set(columnIndex, aValue); + } + + public void addTableModelListener(TableModelListener l) { + tmSupport.addTableModelListener(l); + } + + public void removeTableModelListener(TableModelListener l) { + tmSupport.removeTableModelListener(l); + } + + // ----------------------------------------------------------- CLASSES + + public static class Counter implements Serializable { + + public static final long serialVersionUID = 1000L; + + private String name; + + private Icon icon; + + private int increment; + + private int start; + + private int saved; + + private int live; + + public Counter() { + name = "" + Language.UNTITLED; + icon = null; + start = 0; + live = 0; + increment = 1; + } + + public Object get(int columnIndex) { + switch (columnIndex) { + case COLUMN_ICON: return icon; + case COLUMN_INCREMENT: return increment; + case COLUMN_NAME: return name; + case COLUMN_START: return start; + default: return name; + } + } + + public void set(int columnIndex, Object value) { + switch (columnIndex) { + case COLUMN_ICON: icon = (Icon) value; break; + case COLUMN_INCREMENT: increment = (Integer) value; break; + case COLUMN_NAME: name = (String) value; break; + case COLUMN_START: start = (Integer) value; break; + } + } + + public Icon getIcon() { + return icon; + } + + public String getName() { + return name; + } + + public int getLive() { + return live; + } + + public int getStart() { + return start; + } + + public int getSaved() { + return saved; + } + + public void nextStep() { + live += increment; + } + } } diff --git a/src/main/java/org/fenix/llanfair/Language.java b/src/main/java/org/fenix/llanfair/Language.java index d68f549..dd3d2e1 100644 --- a/src/main/java/org/fenix/llanfair/Language.java +++ b/src/main/java/org/fenix/llanfair/Language.java @@ -1,9 +1,10 @@ package org.fenix.llanfair; +import org.fenix.utils.Resources; + import java.util.HashMap; import java.util.Locale; import java.util.Map; -import org.fenix.utils.Resources; /** * Enumeration of all externalized strings used by {@code Llanfair}. While it is @@ -15,278 +16,278 @@ import org.fenix.utils.Resources; * @see Resources */ public enum Language { - - // Settings > Generic - setting_alwaysOnTop, - setting_language, - setting_viewerLanguage, - setting_recentFiles, - setting_coordinates, - setting_dimension, - setting_compareMethod, - setting_accuracy, - setting_locked, - setting_warnOnReset, - - // Settings > Color - setting_color_background, - setting_color_foreground, - setting_color_time, - setting_color_timer, - setting_color_timeGained, - setting_color_timeLost, - setting_color_newRecord, - setting_color_title, - setting_color_highlight, - setting_color_separators, - - // Settings > Hotkey - setting_hotkey_split, - setting_hotkey_unsplit, - setting_hotkey_skip, - setting_hotkey_reset, - setting_hotkey_stop, - setting_hotkey_pause, - setting_hotkey_lock, - - // Settings > Header - setting_header_goal, - setting_header_title, - - // Settings > History - setting_history_rowCount, - setting_history_tabular, - setting_history_blankRows, - setting_history_multiline, - setting_history_merge, - setting_history_liveTimes, - setting_history_deltas, - setting_history_icons, - setting_history_iconSize, - setting_history_offset, - setting_history_alwaysShowLast, - setting_history_segmentFont, - setting_history_timeFont, - - // Settings > Core - setting_core_accuracy, - setting_core_icons, - setting_core_iconSize, - setting_core_segmentName, - setting_core_splitTime, - setting_core_segmentTime, - setting_core_bestTime, - setting_core_segmentTimer, - setting_core_timerFont, - setting_core_segmentTimerFont, - - // Settings > Graph - setting_graph_display, - setting_graph_scale, - - // Settings > Footer - setting_footer_display, - setting_footer_useSplitData, - setting_footer_verbose, - setting_footer_bestTime, - setting_footer_multiline, - setting_footer_deltaLabels, - - // Accuracy - accuracy_seconds, - accuracy_tenth, - accuracy_hundredth, - - // Compare - compare_best_overall_run, - compare_sum_of_best_segments, - - // Merge - merge_none, - merge_live, - merge_delta, - - // MenuItem - menuItem_edit, - menuItem_new, - menuItem_open, - menuItem_open_recent, - menuItem_import, - menuItem_save, - menuItem_save_as, - menuItem_reset, - menuItem_lock, - menuItem_unlock, - menuItem_resize_default, - menuItem_resize_preferred, - menuItem_settings, - menuItem_about, - menuItem_exit, - - // Errors - error_read_file, - error_write_file, - error_import_run, - - // Actions - action_accept, - - // Titles - title_about, - - GENERAL, - TIMER, - FOOTER, - MISC, - USE_MAIN_FONT, - LB_GOAL, - ICON, - COLORS, - - // Edit Dialog - ED_SEGMENTED, - TT_ED_SEGMENTED, - - // Panels Title - PN_DIMENSION, - PN_DISPLAY, - PN_FONTS, - PN_SCROLLING, - - // History - HISTORY, - MERGE_DELTA, - MERGE_LIVE, - MERGE_NONE, - TT_HS_OFFSET, - - // Core - LB_CR_BEST, - LB_CR_SEGMENT, - LB_CR_SPLIT, - - // Footer - LB_FT_BEST, - LB_FT_DELTA, - LB_FT_DELTA_BEST, - LB_FT_LIVE, - LB_FT_SEGMENT, - LB_FT_SPLIT, - - /* - * Messages. - */ - ICON_TOO_BIG, - ILLEGAL_TIME, - ILLEGAL_SEGMENT_TIME, - INPUT_NAN, - INPUT_NEGATIVE, - INVALID_TIME_STAMP, - WARN_BETTER_RUN, - WARN_BETTER_TIMES, - WARN_RESET_SETTINGS, - - /* - * Tooltips. - */ - TT_ADD_SEGMENT, - TT_COLOR_PICK, - TT_COLUMN_BEST, - TT_COLUMN_SEGMENT, - TT_COLUMN_TIME, - TT_REMOVE_SEGMENT, - TT_MOVE_SEGMENT_UP, - TT_MOVE_SEGMENT_DOWN, - /* - * Run.State enumeration. - */ - RUN_NULL, - RUN_OVER, - RUN_READY, - RUN_STOPPED, + // Settings > Generic + setting_alwaysOnTop, + setting_language, + setting_viewerLanguage, + setting_recentFiles, + setting_coordinates, + setting_dimension, + setting_compareMethod, + setting_accuracy, + setting_locked, + setting_warnOnReset, - /* - * Time.Accuracy enumeration. - */ - ACCURACY, + // Settings > Color + setting_color_background, + setting_color_foreground, + setting_color_time, + setting_color_timer, + setting_color_timeGained, + setting_color_timeLost, + setting_color_newRecord, + setting_color_title, + setting_color_highlight, + setting_color_separators, + + // Settings > Hotkey + setting_hotkey_split, + setting_hotkey_unsplit, + setting_hotkey_skip, + setting_hotkey_reset, + setting_hotkey_stop, + setting_hotkey_pause, + setting_hotkey_lock, + + // Settings > Header + setting_header_goal, + setting_header_title, + + // Settings > History + setting_history_rowCount, + setting_history_tabular, + setting_history_blankRows, + setting_history_multiline, + setting_history_merge, + setting_history_liveTimes, + setting_history_deltas, + setting_history_icons, + setting_history_iconSize, + setting_history_offset, + setting_history_alwaysShowLast, + setting_history_segmentFont, + setting_history_timeFont, + + // Settings > Core + setting_core_accuracy, + setting_core_icons, + setting_core_iconSize, + setting_core_segmentName, + setting_core_splitTime, + setting_core_segmentTime, + setting_core_bestTime, + setting_core_segmentTimer, + setting_core_timerFont, + setting_core_segmentTimerFont, + + // Settings > Graph + setting_graph_display, + setting_graph_scale, + + // Settings > Footer + setting_footer_display, + setting_footer_useSplitData, + setting_footer_verbose, + setting_footer_bestTime, + setting_footer_multiline, + setting_footer_deltaLabels, + + // Accuracy + accuracy_seconds, + accuracy_tenth, + accuracy_hundredth, + + // Compare + compare_best_overall_run, + compare_sum_of_best_segments, + + // Merge + merge_none, + merge_live, + merge_delta, + + // MenuItem + menuItem_edit, + menuItem_new, + menuItem_open, + menuItem_open_recent, + menuItem_import, + menuItem_save, + menuItem_save_as, + menuItem_reset, + menuItem_lock, + menuItem_unlock, + menuItem_resize_default, + menuItem_resize_preferred, + menuItem_settings, + menuItem_about, + menuItem_exit, + + // Errors + error_read_file, + error_write_file, + error_import_run, + + // Actions + action_accept, + + // Titles + title_about, + + GENERAL, + TIMER, + FOOTER, + MISC, + USE_MAIN_FONT, + LB_GOAL, + ICON, + COLORS, + + // Edit Dialog + ED_SEGMENTED, + TT_ED_SEGMENTED, + + // Panels Title + PN_DIMENSION, + PN_DISPLAY, + PN_FONTS, + PN_SCROLLING, + + // History + HISTORY, + MERGE_DELTA, + MERGE_LIVE, + MERGE_NONE, + TT_HS_OFFSET, + + // Core + LB_CR_BEST, + LB_CR_SEGMENT, + LB_CR_SPLIT, + + // Footer + LB_FT_BEST, + LB_FT_DELTA, + LB_FT_DELTA_BEST, + LB_FT_LIVE, + LB_FT_SEGMENT, + LB_FT_SPLIT, + + /* + * Messages. + */ + ICON_TOO_BIG, + ILLEGAL_TIME, + ILLEGAL_SEGMENT_TIME, + INPUT_NAN, + INPUT_NEGATIVE, + INVALID_TIME_STAMP, + WARN_BETTER_RUN, + WARN_BETTER_TIMES, + WARN_RESET_SETTINGS, + + /* + * Tooltips. + */ + TT_ADD_SEGMENT, + TT_COLOR_PICK, + TT_COLUMN_BEST, + TT_COLUMN_SEGMENT, + TT_COLUMN_TIME, + TT_REMOVE_SEGMENT, + TT_MOVE_SEGMENT_UP, + TT_MOVE_SEGMENT_DOWN, + + /* + * Run.State enumeration. + */ + RUN_NULL, + RUN_OVER, + RUN_READY, + RUN_STOPPED, + + /* + * Time.Accuracy enumeration. + */ + ACCURACY, - /* - * Miscellaneous tokens. - */ - ACCEPT, - APPLICATION, - BEST, - CANCEL, - COMPARE_METHOD, - COMPONENTS, - DISABLED, - EDITING, - ERROR, - GOAL, - IMAGE, - INPUTS, - MAX_ORDINATE, - NAME, - RUN_TITLE, - SEGMENT, - SEGMENTS, - SPLIT, - TIME, - UNTITLED, - WARNING, - - /* - * 1.4 - */ - INCREMENT, - START_VALUE; - - public static final Locale[] LANGUAGES = new Locale[] { - Locale.ENGLISH, - Locale.FRENCH, - Locale.GERMAN, - new Locale("nl"), - new Locale("sv") - }; - - public static final Map LOCALE_NAMES = - new HashMap(); - static { - LOCALE_NAMES.put("de", "Deutsch"); - LOCALE_NAMES.put("en", "English"); - LOCALE_NAMES.put("fr", "Français"); - LOCALE_NAMES.put("nl", "Nederlands"); - LOCALE_NAMES.put("sv", "Svenska"); - } + /* + * Miscellaneous tokens. + */ + ACCEPT, + APPLICATION, + BEST, + CANCEL, + COMPARE_METHOD, + COMPONENTS, + DISABLED, + EDITING, + ERROR, + GOAL, + IMAGE, + INPUTS, + MAX_ORDINATE, + NAME, + RUN_TITLE, + SEGMENT, + SEGMENTS, + SPLIT, + TIME, + UNTITLED, + WARNING, - // -------------------------------------------------------------- INTERFACE + /* + * 1.4 + */ + INCREMENT, + START_VALUE; - /** - * Returns the localized string get of this language element. - * - * @return the localized string for this element. - */ - public String get() { - return Llanfair.getResources().getString(name()); - } - - /** - * Returns the localized string get of this language element. This method - * also passes down an array of parameters to replace the tokens with. - * - * @param parameters - the array of values for each token of the string. - * @return the localized string filled with the given parameters. - */ - public String get(Object... parameters) { - return Llanfair.getResources().getString(name(), parameters); - } + public static final Locale[] LANGUAGES = new Locale[] { + Locale.ENGLISH, + Locale.FRENCH, + Locale.GERMAN, + new Locale("nl"), + new Locale("sv") + }; - /** - * The string representation of an enumerate is the localized string - * corresponding to its name. - */ - @Override public String toString() { - return get(); - } + public static final Map LOCALE_NAMES = + new HashMap(); + static { + LOCALE_NAMES.put("de", "Deutsch"); + LOCALE_NAMES.put("en", "English"); + LOCALE_NAMES.put("fr", "Français"); + LOCALE_NAMES.put("nl", "Nederlands"); + LOCALE_NAMES.put("sv", "Svenska"); + } + + // -------------------------------------------------------------- INTERFACE + + /** + * Returns the localized string get of this language element. + * + * @return the localized string for this element. + */ + public String get() { + return Llanfair.getResources().getString(name()); + } + + /** + * Returns the localized string get of this language element. This method + * also passes down an array of parameters to replace the tokens with. + * + * @param parameters - the array of values for each token of the string. + * @return the localized string filled with the given parameters. + */ + public String get(Object... parameters) { + return Llanfair.getResources().getString(name(), parameters); + } + + /** + * The string representation of an enumerate is the localized string + * corresponding to its name. + */ + @Override public String toString() { + return get(); + } } diff --git a/src/main/java/org/fenix/llanfair/Llanfair.java b/src/main/java/org/fenix/llanfair/Llanfair.java index 6301b45..8589443 100644 --- a/src/main/java/org/fenix/llanfair/Llanfair.java +++ b/src/main/java/org/fenix/llanfair/Llanfair.java @@ -31,427 +31,427 @@ import java.util.Locale; * @version 1.5 */ public class Llanfair extends BorderlessFrame implements TableModelListener, - LocaleListener, MouseWheelListener, ActionListener, NativeKeyListener, - PropertyChangeListener, WindowListener { + LocaleListener, MouseWheelListener, ActionListener, NativeKeyListener, + PropertyChangeListener, WindowListener { - private static Resources RESOURCES = null; - - - static { - ToolTipManager.sharedInstance().setInitialDelay( 1000 ); - ToolTipManager.sharedInstance().setDismissDelay( 7000 ); - ToolTipManager.sharedInstance().setReshowDelay( 0 ); - } + private static Resources RESOURCES = null; - private Run run; - private RunPane runPane; - - private Actions actions; - private JPopupMenu popupMenu; + static { + ToolTipManager.sharedInstance().setInitialDelay( 1000 ); + ToolTipManager.sharedInstance().setDismissDelay( 7000 ); + ToolTipManager.sharedInstance().setReshowDelay( 0 ); + } - private volatile boolean ignoreNativeInputs; - - private Dimension preferredSize; + private Run run; + private RunPane runPane; - /** - * Creates and initializes the application. As with any Swing application - * this constructor should be called from within a thread to avoid - * dead-lock. - */ - private Llanfair() { - super( "Llanfair" ); - LocaleDelegate.setDefault( Settings.GNR_LANG.get() ); - LocaleDelegate.addLocaleListener( this ); + private Actions actions; - //ResourceBundle b = ResourceBundle.getBundle("language"); - - RESOURCES = new Resources(); - registerFonts(); - setLookAndFeel(); + private JPopupMenu popupMenu; - run = new Run(); - runPane = null; - ignoreNativeInputs = false; - preferredSize = null; - actions = new Actions( this ); - - setMenu(); - setBehavior(); - setRun( run ); - - setVisible( true ); - } - - /** - * Main entry point of the application. This is the method called by Java - * when a user executes the JAR. Simply instantiantes a new Llanfair object. - * If an argument is passed, the program will not launch but will instead - * enter localization mode, dumping all language variables for the specified - * locale. - * - * @param args array of command line parameters supplied at launch - */ - public static void main( String[] args ) { - if ( args.length > 0 ) { - String locale = args[0]; - LocaleDelegate.setDefault( new Locale( locale ) ); - RESOURCES = new Resources(); - dumpLocalization(); - System.exit( 0 ); - } - SwingUtilities.invokeLater( new Runnable() { - @Override public void run() { - new Llanfair(); - } - } ); - } - - /** - * Grabs the resources of Llanfair. The Resources object is a front-end - * for every classpath resources associated with the application, including - * localization strings, icons, and properties. - * - * @return the resources object - */ - public static Resources getResources() { - return RESOURCES; - } + private volatile boolean ignoreNativeInputs; - /** - * Returns the run currently associated with this instance of Llanfair. - * While the run can be empty, it cannot be {@code null}. - * - * @return the current run - */ - Run getRun() { - return run; - } - - /** - * Sets the run to represent in this application to the given run. If the - * GUI does not exist (in other words, we are registering the first run) it - * is created on the fly. - * - * @param run the run to represent, cannot be {@code null} - */ - public final void setRun( Run run ) { - if ( run == null ) { - throw new NullPointerException( "Null run" ); - } - this.run = run; - // If we have a GUI, set the new model; else, create the GUI - if ( runPane != null ) { - runPane.setRun( run ); - } else { - runPane = new RunPane( run ); - add( runPane ); - } - Settings.setRun( run ); - run.addTableModelListener( this ); - run.addPropertyChangeListener( this ); - MenuItem.setActiveState( run.getState() ); - - setPreferredSize( preferredSize ); - pack(); - - // Replace the window to the run preferred location; center if none - Point location = Settings.GNR_COOR.get(); - if ( location == null ) { - setLocationRelativeTo( null ); - } else { - setLocation( location ); - } - } - - /** - * Indicates whether or not Llanfair currently ignores all native inputs. - * Since native inputs can be caught even when the application does not have - * the focus, it is necessary to be able to lock the application when the - * user needs to do something else whilst not interfering with the behavior - * of Llanfair. - * - * @return {@code true} if the current instance ignores native inputs - */ - public synchronized boolean ignoresNativeInputs() { - return ignoreNativeInputs; - } - - /** - * Tells Llanfair whether to ignore native input events or not. Since native - * inputs can be caught even when the application does not have the focus, - * it is necessary to be able to lock the application when the user needs - * to do something else whilst not interfering with the behavior of - * Llanfair. - * - * @param ignore if Llanfair must ignore the native inputs or not - */ - public synchronized void setIgnoreNativeInputs( boolean ignore ) { - ignoreNativeInputs = ignore; - } - - /** - * Outputs the given error in a dialog box. Only errors that are made for - * and useful to the user need to be displayed that way. - * - * @param message the localized error message - */ - void showError( String message ) { - JOptionPane.showMessageDialog( - this, message, Language.ERROR.get(), JOptionPane.ERROR_MESSAGE - ); - } - - /** - * Sets the look and feel of the application. Provides a task bar icon and - * a general system dependent theme. - */ - private void setLookAndFeel() { - setIconImage( RESOURCES.getImage( "Llanfair" ) ); - try { - UIManager.setLookAndFeel( - UIManager.getSystemLookAndFeelClassName() - ); - } catch ( Exception ex ) { - // $FALL-THROUGH$ - } - } - - /** - * Register the fonts provided with Llanfair with its environment. - */ - private void registerFonts() { - InputStream fontFile = RESOURCES.getStream( "digitalism.ttf" ); - try { - Font digitalism = Font.createFont( Font.TRUETYPE_FONT, fontFile ); - GraphicsEnvironment.getLocalGraphicsEnvironment().registerFont( - digitalism - ); - } catch ( Exception ex ) { - // $FALL-THROUGH$ - } - } - - /** - * Writes all values from the {@code Language} enum in a property file. - * This method will append all the newly defined entries to the list of - * already existing values. - */ - private static void dumpLocalization() { - try { - String iso = Locale.getDefault().getLanguage(); - FileWriter fw = new FileWriter( "language_" + iso + ".properties" ); - for ( Language lang : Language.values() ) { - String old = RESOURCES.getString( lang.name() ); - fw.write( lang.name() + " = " ); - if ( old != null ) { - fw.write( old ); - } - fw.write( "\n" ); - } - fw.close(); - } catch ( IOException ex ) { - // $FALL-THROUGH$ - } - } + private Dimension preferredSize; - /** - * When the locale changes, we first ask the resources to reload the locale - * dependent resources and pass the event to the GUI. - */ - @Override public void localeChanged( LocaleEvent event ) { - RESOURCES.defaultLocaleChanged(); - if ( runPane != null ) { - runPane.processLocaleEvent( event ); - } - MenuItem.localeChanged( event ); - } - - /** - * If we do not ignore the native inputs, register the input and invokes - * a new thread to treat the input whenever possible without hogging the - * main thread. - */ - @Override public void nativeKeyPressed( final NativeKeyEvent event ) { - int keyCode = event.getKeyCode(); - boolean hotkeysEnabler = ( keyCode == Settings.KEY_LOCK.get() ); - - if ( !ignoresNativeInputs() || hotkeysEnabler ) { - SwingUtilities.invokeLater( new Runnable() { - @Override public void run() { - actions.process( event ); - } - } ); - } - } + /** + * Creates and initializes the application. As with any Swing application + * this constructor should be called from within a thread to avoid + * dead-lock. + */ + private Llanfair() { + super( "Llanfair" ); + LocaleDelegate.setDefault( Settings.GNR_LANG.get() ); + LocaleDelegate.addLocaleListener( this ); - @Override public void nativeKeyReleased( NativeKeyEvent event ) {} + //ResourceBundle b = ResourceBundle.getBundle("language"); - @Override public void nativeKeyTyped( NativeKeyEvent event ) {} - - /** - * A property change event might be fired from either the settings - * singleton or the run itself. In either case, we propagate the event to - * our children and update ourself with the new value of the given property. - */ - @Override public void propertyChange( PropertyChangeEvent event ) { - runPane.processPropertyChangeEvent( event ); - String property = event.getPropertyName(); + RESOURCES = new Resources(); + registerFonts(); + setLookAndFeel(); - if ( Run.STATE_PROPERTY.equals( property ) ) { - MenuItem.setActiveState( run.getState() ); - } else if ( Settings.GNR_ATOP.equals( property ) ) { - setAlwaysOnTop( Settings.GNR_ATOP.get() ); - } else if (Settings.HST_ROWS.equals(property) - || Settings.GPH_SHOW.equals(property) - || Settings.FOO_SHOW.equals(property) - || Settings.FOO_SPLT.equals(property) - || Settings.COR_ICSZ.equals(property) - || Settings.GNR_ACCY.equals(property) - || Settings.HDR_TTLE.equals(property) - || Settings.HDR_GOAL.equals(property) - || Settings.HST_DLTA.equals(property) - || Settings.HST_SFNT.equals(property) - || Settings.HST_TFNT.equals(property) - || Settings.HST_LIVE.equals(property) - || Settings.HST_MERG.equals(property) - || Settings.HST_BLNK.equals(property) - || Settings.HST_ICON.equals(property) - || Settings.HST_ICSZ.equals(property) - || Settings.HST_LINE.equals(property) - || Settings.COR_NAME.equals(property) - || Settings.COR_SPLT.equals(property) - || Settings.COR_SEGM.equals(property) - || Settings.COR_BEST.equals(property) - || Settings.COR_ICON.equals(property) - || Settings.COR_TFNT.equals(property) - || Settings.COR_SFNT.equals(property) - || Settings.COR_STMR.equals(property) - || Settings.FOO_BEST.equals(property) - || Settings.FOO_DLBL.equals(property) - || Settings.FOO_VERB.equals(property) - || Settings.FOO_LINE.equals(property) - || Run.NAME_PROPERTY.equals(property)) { - setPreferredSize(null); - pack(); - } - } - - /** - * When the run's table of segments is updated, we ask the main panel to - * update itself accordingly and repack the frame as its dimensions may - * have changed. - */ - @Override public void tableChanged( TableModelEvent event ) { - runPane.processTableModelEvent( event ); - // No need to recompute the size if we receive a HEADER_ROW UPDATE - // as we only use them when a segment is moved up or down and when - // the user cancel any changes made to his run. - if ( event.getType() == TableModelEvent.UPDATE - && event.getFirstRow() == TableModelEvent.HEADER_ROW ) { - setPreferredSize( preferredSize ); - } else { - setPreferredSize( null ); - } - pack(); - } - - /** - * When the user scrolls the mouse wheel, we update the graph's scale to - * zoom in or out depending on the direction of the scroll. - */ - @Override public void mouseWheelMoved( MouseWheelEvent event ) { - int rotations = event.getWheelRotation(); - float percent = Settings.GPH_SCAL.get(); - if ( percent == 0.5F ) { - percent = 1.0F; - rotations--; - } - float newValue = Math.max( 0.5F, percent + rotations ); - Settings.GPH_SCAL.set( newValue ); - } + run = new Run(); + runPane = null; + ignoreNativeInputs = false; + preferredSize = null; + actions = new Actions( this ); - /** - * When the user clicks on the mouse's right-button, we bring up the - * context menu at the click's location. - */ - @Override public void mousePressed( MouseEvent event ) { - super.mousePressed( event ); - if ( SwingUtilities.isRightMouseButton( event ) ) { - popupMenu.show( this, event.getX(), event.getY() ); - } - } - - /** - * Whenever the frame is being disposed of, we save the settings and - * unregister the native hook of {@code JNativeHook}. - */ - @Override public void windowClosed( WindowEvent event ) { - Settings.save(); - GlobalScreen.unregisterNativeHook(); - } + setMenu(); + setBehavior(); + setRun( run ); - @Override public void windowClosing(WindowEvent event) {} + setVisible( true ); + } - @Override public void windowOpened(WindowEvent event) {} + /** + * Main entry point of the application. This is the method called by Java + * when a user executes the JAR. Simply instantiantes a new Llanfair object. + * If an argument is passed, the program will not launch but will instead + * enter localization mode, dumping all language variables for the specified + * locale. + * + * @param args array of command line parameters supplied at launch + */ + public static void main( String[] args ) { + if ( args.length > 0 ) { + String locale = args[0]; + LocaleDelegate.setDefault( new Locale( locale ) ); + RESOURCES = new Resources(); + dumpLocalization(); + System.exit( 0 ); + } + SwingUtilities.invokeLater( new Runnable() { + @Override public void run() { + new Llanfair(); + } + } ); + } - @Override public void windowActivated(WindowEvent event) {} + /** + * Grabs the resources of Llanfair. The Resources object is a front-end + * for every classpath resources associated with the application, including + * localization strings, icons, and properties. + * + * @return the resources object + */ + public static Resources getResources() { + return RESOURCES; + } - @Override public void windowDeactivated(WindowEvent event) {} + /** + * Returns the run currently associated with this instance of Llanfair. + * While the run can be empty, it cannot be {@code null}. + * + * @return the current run + */ + Run getRun() { + return run; + } - @Override public void windowIconified(WindowEvent event) {} + /** + * Sets the run to represent in this application to the given run. If the + * GUI does not exist (in other words, we are registering the first run) it + * is created on the fly. + * + * @param run the run to represent, cannot be {@code null} + */ + public final void setRun( Run run ) { + if ( run == null ) { + throw new NullPointerException( "Null run" ); + } + this.run = run; + // If we have a GUI, set the new model; else, create the GUI + if ( runPane != null ) { + runPane.setRun( run ); + } else { + runPane = new RunPane( run ); + add( runPane ); + } + Settings.setRun( run ); + run.addTableModelListener( this ); + run.addPropertyChangeListener( this ); + MenuItem.setActiveState( run.getState() ); - @Override public void windowDeiconified(WindowEvent event) {} - - /** - * Action events are fired by clicking on the entries of the context menu. - */ - @Override public synchronized void actionPerformed( final ActionEvent ev ) { - MenuItem source = ( MenuItem ) ev.getSource(); + setPreferredSize( preferredSize ); + pack(); - SwingUtilities.invokeLater( new Runnable() { - @Override public void run() { - actions.process( ev ); - } - } ); + // Replace the window to the run preferred location; center if none + Point location = Settings.GNR_COOR.get(); + if ( location == null ) { + setLocationRelativeTo( null ); + } else { + setLocation( location ); + } + } - if (source.equals(MenuItem.EDIT)) { + /** + * Indicates whether or not Llanfair currently ignores all native inputs. + * Since native inputs can be caught even when the application does not have + * the focus, it is necessary to be able to lock the application when the + * user needs to do something else whilst not interfering with the behavior + * of Llanfair. + * + * @return {@code true} if the current instance ignores native inputs + */ + public synchronized boolean ignoresNativeInputs() { + return ignoreNativeInputs; + } - } else if (source.equals(MenuItem.RESIZE_DEFAULT)) { - setPreferredSize(null); - pack(); - - } else if (source.equals(MenuItem.RESIZE_PREFERRED)) { - setPreferredSize(preferredSize); - pack(); - } - } + /** + * Tells Llanfair whether to ignore native input events or not. Since native + * inputs can be caught even when the application does not have the focus, + * it is necessary to be able to lock the application when the user needs + * to do something else whilst not interfering with the behavior of + * Llanfair. + * + * @param ignore if Llanfair must ignore the native inputs or not + */ + public synchronized void setIgnoreNativeInputs( boolean ignore ) { + ignoreNativeInputs = ignore; + } - /** - * Sets the persistent behavior of the application and its components. - * - * @throws IllegalStateException if JNativeHook cannot be registered. - */ - private void setBehavior() { - try { - GlobalScreen.registerNativeHook(); - } catch (NativeHookException e) { - throw new IllegalStateException("cannot register native hook"); - } - setAlwaysOnTop(Settings.GNR_ATOP.get()); - addWindowListener(this); - addMouseWheelListener(this); - Settings.addPropertyChangeListener(this); - GlobalScreen.getInstance().addNativeKeyListener(this); - } + /** + * Outputs the given error in a dialog box. Only errors that are made for + * and useful to the user need to be displayed that way. + * + * @param message the localized error message + */ + void showError( String message ) { + JOptionPane.showMessageDialog( + this, message, Language.ERROR.get(), JOptionPane.ERROR_MESSAGE + ); + } - /** - * Initializes the right-click context menu. - */ - private void setMenu() { - popupMenu = MenuItem.getPopupMenu(); - MenuItem.addActionListener( this ); - MenuItem.populateRecentlyOpened(); - } + /** + * Sets the look and feel of the application. Provides a task bar icon and + * a general system dependent theme. + */ + private void setLookAndFeel() { + setIconImage( RESOURCES.getImage( "Llanfair" ) ); + try { + UIManager.setLookAndFeel( + UIManager.getSystemLookAndFeelClassName() + ); + } catch ( Exception ex ) { + // $FALL-THROUGH$ + } + } + + /** + * Register the fonts provided with Llanfair with its environment. + */ + private void registerFonts() { + InputStream fontFile = RESOURCES.getStream( "digitalism.ttf" ); + try { + Font digitalism = Font.createFont( Font.TRUETYPE_FONT, fontFile ); + GraphicsEnvironment.getLocalGraphicsEnvironment().registerFont( + digitalism + ); + } catch ( Exception ex ) { + // $FALL-THROUGH$ + } + } + + /** + * Writes all values from the {@code Language} enum in a property file. + * This method will append all the newly defined entries to the list of + * already existing values. + */ + private static void dumpLocalization() { + try { + String iso = Locale.getDefault().getLanguage(); + FileWriter fw = new FileWriter( "language_" + iso + ".properties" ); + for ( Language lang : Language.values() ) { + String old = RESOURCES.getString( lang.name() ); + fw.write( lang.name() + " = " ); + if ( old != null ) { + fw.write( old ); + } + fw.write( "\n" ); + } + fw.close(); + } catch ( IOException ex ) { + // $FALL-THROUGH$ + } + } + + /** + * When the locale changes, we first ask the resources to reload the locale + * dependent resources and pass the event to the GUI. + */ + @Override public void localeChanged( LocaleEvent event ) { + RESOURCES.defaultLocaleChanged(); + if ( runPane != null ) { + runPane.processLocaleEvent( event ); + } + MenuItem.localeChanged( event ); + } + + /** + * If we do not ignore the native inputs, register the input and invokes + * a new thread to treat the input whenever possible without hogging the + * main thread. + */ + @Override public void nativeKeyPressed( final NativeKeyEvent event ) { + int keyCode = event.getKeyCode(); + boolean hotkeysEnabler = ( keyCode == Settings.KEY_LOCK.get() ); + + if ( !ignoresNativeInputs() || hotkeysEnabler ) { + SwingUtilities.invokeLater( new Runnable() { + @Override public void run() { + actions.process( event ); + } + } ); + } + } + + @Override public void nativeKeyReleased( NativeKeyEvent event ) {} + + @Override public void nativeKeyTyped( NativeKeyEvent event ) {} + + /** + * A property change event might be fired from either the settings + * singleton or the run itself. In either case, we propagate the event to + * our children and update ourself with the new value of the given property. + */ + @Override public void propertyChange( PropertyChangeEvent event ) { + runPane.processPropertyChangeEvent( event ); + String property = event.getPropertyName(); + + if ( Run.STATE_PROPERTY.equals( property ) ) { + MenuItem.setActiveState( run.getState() ); + } else if ( Settings.GNR_ATOP.equals( property ) ) { + setAlwaysOnTop( Settings.GNR_ATOP.get() ); + } else if (Settings.HST_ROWS.equals(property) + || Settings.GPH_SHOW.equals(property) + || Settings.FOO_SHOW.equals(property) + || Settings.FOO_SPLT.equals(property) + || Settings.COR_ICSZ.equals(property) + || Settings.GNR_ACCY.equals(property) + || Settings.HDR_TTLE.equals(property) + || Settings.HDR_GOAL.equals(property) + || Settings.HST_DLTA.equals(property) + || Settings.HST_SFNT.equals(property) + || Settings.HST_TFNT.equals(property) + || Settings.HST_LIVE.equals(property) + || Settings.HST_MERG.equals(property) + || Settings.HST_BLNK.equals(property) + || Settings.HST_ICON.equals(property) + || Settings.HST_ICSZ.equals(property) + || Settings.HST_LINE.equals(property) + || Settings.COR_NAME.equals(property) + || Settings.COR_SPLT.equals(property) + || Settings.COR_SEGM.equals(property) + || Settings.COR_BEST.equals(property) + || Settings.COR_ICON.equals(property) + || Settings.COR_TFNT.equals(property) + || Settings.COR_SFNT.equals(property) + || Settings.COR_STMR.equals(property) + || Settings.FOO_BEST.equals(property) + || Settings.FOO_DLBL.equals(property) + || Settings.FOO_VERB.equals(property) + || Settings.FOO_LINE.equals(property) + || Run.NAME_PROPERTY.equals(property)) { + setPreferredSize(null); + pack(); + } + } + + /** + * When the run's table of segments is updated, we ask the main panel to + * update itself accordingly and repack the frame as its dimensions may + * have changed. + */ + @Override public void tableChanged( TableModelEvent event ) { + runPane.processTableModelEvent( event ); + // No need to recompute the size if we receive a HEADER_ROW UPDATE + // as we only use them when a segment is moved up or down and when + // the user cancel any changes made to his run. + if ( event.getType() == TableModelEvent.UPDATE + && event.getFirstRow() == TableModelEvent.HEADER_ROW ) { + setPreferredSize( preferredSize ); + } else { + setPreferredSize( null ); + } + pack(); + } + + /** + * When the user scrolls the mouse wheel, we update the graph's scale to + * zoom in or out depending on the direction of the scroll. + */ + @Override public void mouseWheelMoved( MouseWheelEvent event ) { + int rotations = event.getWheelRotation(); + float percent = Settings.GPH_SCAL.get(); + if ( percent == 0.5F ) { + percent = 1.0F; + rotations--; + } + float newValue = Math.max( 0.5F, percent + rotations ); + Settings.GPH_SCAL.set( newValue ); + } + + /** + * When the user clicks on the mouse's right-button, we bring up the + * context menu at the click's location. + */ + @Override public void mousePressed( MouseEvent event ) { + super.mousePressed( event ); + if ( SwingUtilities.isRightMouseButton( event ) ) { + popupMenu.show( this, event.getX(), event.getY() ); + } + } + + /** + * Whenever the frame is being disposed of, we save the settings and + * unregister the native hook of {@code JNativeHook}. + */ + @Override public void windowClosed( WindowEvent event ) { + Settings.save(); + GlobalScreen.unregisterNativeHook(); + } + + @Override public void windowClosing(WindowEvent event) {} + + @Override public void windowOpened(WindowEvent event) {} + + @Override public void windowActivated(WindowEvent event) {} + + @Override public void windowDeactivated(WindowEvent event) {} + + @Override public void windowIconified(WindowEvent event) {} + + @Override public void windowDeiconified(WindowEvent event) {} + + /** + * Action events are fired by clicking on the entries of the context menu. + */ + @Override public synchronized void actionPerformed( final ActionEvent ev ) { + MenuItem source = ( MenuItem ) ev.getSource(); + + SwingUtilities.invokeLater( new Runnable() { + @Override public void run() { + actions.process( ev ); + } + } ); + + if (source.equals(MenuItem.EDIT)) { + + } else if (source.equals(MenuItem.RESIZE_DEFAULT)) { + setPreferredSize(null); + pack(); + + } else if (source.equals(MenuItem.RESIZE_PREFERRED)) { + setPreferredSize(preferredSize); + pack(); + } + } + + /** + * Sets the persistent behavior of the application and its components. + * + * @throws IllegalStateException if JNativeHook cannot be registered. + */ + private void setBehavior() { + try { + GlobalScreen.registerNativeHook(); + } catch (NativeHookException e) { + throw new IllegalStateException("cannot register native hook"); + } + setAlwaysOnTop(Settings.GNR_ATOP.get()); + addWindowListener(this); + addMouseWheelListener(this); + Settings.addPropertyChangeListener(this); + GlobalScreen.getInstance().addNativeKeyListener(this); + } + + /** + * Initializes the right-click context menu. + */ + private void setMenu() { + popupMenu = MenuItem.getPopupMenu(); + MenuItem.addActionListener( this ); + MenuItem.populateRecentlyOpened(); + } } diff --git a/src/main/java/org/fenix/llanfair/MenuItem.java b/src/main/java/org/fenix/llanfair/MenuItem.java index e53e858..b8457d7 100644 --- a/src/main/java/org/fenix/llanfair/MenuItem.java +++ b/src/main/java/org/fenix/llanfair/MenuItem.java @@ -1,19 +1,16 @@ package org.fenix.llanfair; +import org.fenix.llanfair.Run.State; +import org.fenix.llanfair.config.Settings; +import org.fenix.utils.locale.LocaleEvent; + +import javax.swing.*; +import javax.swing.event.EventListenerList; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.File; import java.util.Arrays; import java.util.List; -import javax.swing.Icon; -import javax.swing.JMenu; -import javax.swing.JMenuItem; -import javax.swing.JPopupMenu; -import javax.swing.JSeparator; -import javax.swing.event.EventListenerList; -import org.fenix.utils.locale.LocaleEvent; -import org.fenix.llanfair.Run.State; -import org.fenix.llanfair.config.Settings; /** * Enumerates the menu items available in the right-click context menu of @@ -25,207 +22,207 @@ import org.fenix.llanfair.config.Settings; */ enum MenuItem implements ActionListener { - EDIT( false, State.NULL, State.READY ), - NEW( true, State.NULL, State.READY, State.STOPPED ), - OPEN( false, State.NULL, State.READY, State.STOPPED ), - OPEN_RECENT( false, State.NULL, State.READY, State.STOPPED ), - IMPORT( false, State.NULL, State.READY, State.STOPPED ), - SAVE( false, State.READY, State.STOPPED ), - SAVE_AS( true, State.READY ), - RESET( true, State.ONGOING, State.STOPPED, State.PAUSED ), - LOCK( false, State.NULL, State.READY, State.STOPPED, State.ONGOING ), - UNLOCK( false, State.NULL, State.READY, State.STOPPED, State.ONGOING ), - RESIZE_DEFAULT( false, State.NULL, State.READY ), - RESIZE_PREFERRED( true, State.NULL, State.READY ), - SETTINGS( true, State.NULL, State.READY, State.STOPPED ), - ABOUT( true, State.NULL, State.READY, State.STOPPED, State.ONGOING ), - EXIT( false, State.NULL, State.READY, State.STOPPED, State.ONGOING ); + EDIT( false, State.NULL, State.READY ), + NEW( true, State.NULL, State.READY, State.STOPPED ), + OPEN( false, State.NULL, State.READY, State.STOPPED ), + OPEN_RECENT( false, State.NULL, State.READY, State.STOPPED ), + IMPORT( false, State.NULL, State.READY, State.STOPPED ), + SAVE( false, State.READY, State.STOPPED ), + SAVE_AS( true, State.READY ), + RESET( true, State.ONGOING, State.STOPPED, State.PAUSED ), + LOCK( false, State.NULL, State.READY, State.STOPPED, State.ONGOING ), + UNLOCK( false, State.NULL, State.READY, State.STOPPED, State.ONGOING ), + RESIZE_DEFAULT( false, State.NULL, State.READY ), + RESIZE_PREFERRED( true, State.NULL, State.READY ), + SETTINGS( true, State.NULL, State.READY, State.STOPPED ), + ABOUT( true, State.NULL, State.READY, State.STOPPED, State.ONGOING ), + EXIT( false, State.NULL, State.READY, State.STOPPED, State.ONGOING ); - /** - * Static list of listeners that listen to all the menu items. Whenever - * one item fires an {@code ActionEvent}, the type statically fires an - * other {@code ActionEvent} with the {@code MenuItem} enumerate as the - * source component. - */ - private static EventListenerList listeners = new EventListenerList(); + /** + * Static list of listeners that listen to all the menu items. Whenever + * one item fires an {@code ActionEvent}, the type statically fires an + * other {@code ActionEvent} with the {@code MenuItem} enumerate as the + * source component. + */ + private static EventListenerList listeners = new EventListenerList(); - private static final int MAX_FILES = 5; - private static final int TRUNCATE = 30; - - private boolean isEndOfGroup; - private List activeStates; - private JMenuItem menuItem; + private static final int MAX_FILES = 5; + private static final int TRUNCATE = 30; - /** - * Internal constructor used to set the attributes. Only called by the - * enum type itself. - * - * @param isEndOfGroup indicates if this item is a group ender - * @param activeStates list of active run states of this item - */ - private MenuItem( boolean isEndOfGroup, Run.State... activeStates ) { - this.isEndOfGroup = isEndOfGroup; - this.activeStates = Arrays.asList( activeStates ); + private boolean isEndOfGroup; + private List activeStates; + private JMenuItem menuItem; - if ( name().equals( "OPEN_RECENT" ) ) { - menuItem = new JMenu( toString() ); - } else { - menuItem = new JMenuItem( toString() ); - } - Icon icon = Llanfair.getResources().getIcon( "jmi/" + name() + ".png" ); - menuItem.setIcon( icon ); - menuItem.addActionListener( this ); - // LOCK & UNLOCK mask each other and Llanfair always start unlocked - if ( name().equals( "UNLOCK" ) ) { - menuItem.setVisible( false ); - } - } - - /** - * Returns a popup menu composed of all the menu items. A separator is - * inserted after each item which are indicated as end of their group. - * - * @return a popup menu containing every menu item - */ - static JPopupMenu getPopupMenu() { - JPopupMenu menu = new JPopupMenu(); - for ( MenuItem item : values() ) { - menu.add( item.menuItem ); - if ( item.isEndOfGroup ) { - menu.add( new JSeparator() ); - } - } - return menu; - } + /** + * Internal constructor used to set the attributes. Only called by the + * enum type itself. + * + * @param isEndOfGroup indicates if this item is a group ender + * @param activeStates list of active run states of this item + */ + MenuItem(boolean isEndOfGroup, Run.State... activeStates) { + this.isEndOfGroup = isEndOfGroup; + this.activeStates = Arrays.asList( activeStates ); - /** - * Enables or disables every menu items depending on the given run state. - * If this state is present in the list of active states for an item, then - * its GUI component is enabled, else it is disabled. - * - * @param state the current run state, cannot be {@code null} - */ - static void setActiveState( Run.State state ) { - if ( state == null ) { - throw new IllegalArgumentException( "Null run state" ); - } - for ( MenuItem item : values() ) { - item.menuItem.setEnabled( item.activeStates.contains( state ) ); - } - } + if ( name().equals( "OPEN_RECENT" ) ) { + menuItem = new JMenu( toString() ); + } else { + menuItem = new JMenuItem( toString() ); + } + Icon icon = Llanfair.getResources().getIcon( "jmi/" + name() + ".png" ); + menuItem.setIcon( icon ); + menuItem.addActionListener( this ); + // LOCK & UNLOCK mask each other and Llanfair always start unlocked + if ( name().equals( "UNLOCK" ) ) { + menuItem.setVisible( false ); + } + } - /** - * Registers the given {@code ActionListener} to the list of listeners - * interested in capturing events from every menu items. - * - * @param listener the action listener to register - */ - static void addActionListener( ActionListener listener ) { - listeners.add( ActionListener.class, listener ); - } - - /** - * Callback to invoke whenever a file is opened. This method will sort the - * recent files menu to put the recently opened file at the top. - * - * @param path the name of the recently opened file - */ - static void recentlyOpened( String path ) { - assert ( path != null ); - List recentFiles = Settings.GNR_RCNT.get(); - - if ( recentFiles.contains( path ) ) { - recentFiles.remove( path ); - } - recentFiles.add( 0, path ); + /** + * Returns a popup menu composed of all the menu items. A separator is + * inserted after each item which are indicated as end of their group. + * + * @return a popup menu containing every menu item + */ + static JPopupMenu getPopupMenu() { + JPopupMenu menu = new JPopupMenu(); + for ( MenuItem item : values() ) { + menu.add( item.menuItem ); + if ( item.isEndOfGroup ) { + menu.add( new JSeparator() ); + } + } + return menu; + } - if ( recentFiles.size() > MAX_FILES ) { - recentFiles.remove( MAX_FILES ); - } - Settings.GNR_RCNT.set( recentFiles ); - populateRecentlyOpened(); - } - - /** - * Fills the {@code OPEN_RECENT} item with the list of recently opened - * files. If {@code MAX_FILES} is somehow lower than the recent files list - * length, the overflowing files are removed. - */ - static void populateRecentlyOpened() { - List recentFiles = Settings.GNR_RCNT.get(); - for ( int i = MAX_FILES; i < recentFiles.size(); i++ ) { - recentFiles.remove( i - 1 ); - } - OPEN_RECENT.menuItem.removeAll(); - for ( String fileName : Settings.GNR_RCNT.get() ) { - String text = fileName; - int index = text.lastIndexOf( File.separatorChar ); - if ( index == -1 ) { - int length = text.length(); - int start = length - Math.min( length, TRUNCATE ); - text = text.substring( start ); - if ( start == 0 ) { - text = "[...]" + text; - } - } else { - text = text.substring( index + 1 ); - } - JMenuItem jmi = new JMenuItem( text ); - jmi.setName( "RECENT" + fileName ); - jmi.addActionListener( OPEN_RECENT ); - OPEN_RECENT.menuItem.add( jmi ); - } - } - - /** - * Returns the localized name of this menu item. - */ - @Override public String toString() { - return Language.valueOf( "menuItem_" + name().toLowerCase() ).get(); - } + /** + * Enables or disables every menu items depending on the given run state. + * If this state is present in the list of active states for an item, then + * its GUI component is enabled, else it is disabled. + * + * @param state the current run state, cannot be {@code null} + */ + static void setActiveState( Run.State state ) { + if ( state == null ) { + throw new IllegalArgumentException( "Null run state" ); + } + for ( MenuItem item : values() ) { + item.menuItem.setEnabled( item.activeStates.contains( state ) ); + } + } - /** - * When a GUI component fires an action event, capture it and fire it - * back, setting the source as the enumerate value instead of the GUI - * component. - */ - @Override public void actionPerformed( ActionEvent event ) { - Object source = event.getSource(); - - if ( source.equals( LOCK.menuItem ) ) { - LOCK.menuItem.setVisible( false ); - UNLOCK.menuItem.setVisible( true ); - } else if ( source.equals( UNLOCK.menuItem ) ) { - LOCK.menuItem.setVisible( true ); - UNLOCK.menuItem.setVisible( false ); - } + /** + * Registers the given {@code ActionListener} to the list of listeners + * interested in capturing events from every menu items. + * + * @param listener the action listener to register + */ + static void addActionListener( ActionListener listener ) { + listeners.add( ActionListener.class, listener ); + } - JMenuItem jmi = ( JMenuItem ) source; - String name = jmi.getName(); - ActionListener[] als = listeners.getListeners( ActionListener.class ); - - if ( name != null && name.startsWith( "RECENT" ) ) { - event = new ActionEvent( - this, ActionEvent.ACTION_PERFORMED, name.substring( 6 ) - ); - } else { - event = new ActionEvent( - this, ActionEvent.ACTION_PERFORMED, jmi.getText() - ); - } - for ( ActionListener al : als ) { - al.actionPerformed( event ); - } - } + /** + * Callback to invoke whenever a file is opened. This method will sort the + * recent files menu to put the recently opened file at the top. + * + * @param path the name of the recently opened file + */ + static void recentlyOpened( String path ) { + assert ( path != null ); + List recentFiles = Settings.GNR_RCNT.get(); + + if ( recentFiles.contains( path ) ) { + recentFiles.remove( path ); + } + recentFiles.add( 0, path ); + + if ( recentFiles.size() > MAX_FILES ) { + recentFiles.remove( MAX_FILES ); + } + Settings.GNR_RCNT.set( recentFiles ); + populateRecentlyOpened(); + } + + /** + * Fills the {@code OPEN_RECENT} item with the list of recently opened + * files. If {@code MAX_FILES} is somehow lower than the recent files list + * length, the overflowing files are removed. + */ + static void populateRecentlyOpened() { + List recentFiles = Settings.GNR_RCNT.get(); + for ( int i = MAX_FILES; i < recentFiles.size(); i++ ) { + recentFiles.remove( i - 1 ); + } + OPEN_RECENT.menuItem.removeAll(); + for ( String fileName : Settings.GNR_RCNT.get() ) { + String text = fileName; + int index = text.lastIndexOf( File.separatorChar ); + if ( index == -1 ) { + int length = text.length(); + int start = length - Math.min( length, TRUNCATE ); + text = text.substring( start ); + if ( start == 0 ) { + text = "[...]" + text; + } + } else { + text = text.substring( index + 1 ); + } + JMenuItem jmi = new JMenuItem( text ); + jmi.setName( "RECENT" + fileName ); + jmi.addActionListener( OPEN_RECENT ); + OPEN_RECENT.menuItem.add( jmi ); + } + } + + /** + * Returns the localized name of this menu item. + */ + @Override public String toString() { + return Language.valueOf( "menuItem_" + name().toLowerCase() ).get(); + } + + /** + * When a GUI component fires an action event, capture it and fire it + * back, setting the source as the enumerate value instead of the GUI + * component. + */ + @Override public void actionPerformed( ActionEvent event ) { + Object source = event.getSource(); + + if ( source.equals( LOCK.menuItem ) ) { + LOCK.menuItem.setVisible( false ); + UNLOCK.menuItem.setVisible( true ); + } else if ( source.equals( UNLOCK.menuItem ) ) { + LOCK.menuItem.setVisible( true ); + UNLOCK.menuItem.setVisible( false ); + } + + JMenuItem jmi = ( JMenuItem ) source; + String name = jmi.getName(); + ActionListener[] als = listeners.getListeners( ActionListener.class ); + + if ( name != null && name.startsWith( "RECENT" ) ) { + event = new ActionEvent( + this, ActionEvent.ACTION_PERFORMED, name.substring( 6 ) + ); + } else { + event = new ActionEvent( + this, ActionEvent.ACTION_PERFORMED, jmi.getText() + ); + } + for ( ActionListener al : als ) { + al.actionPerformed( event ); + } + } + + /** + * When the locale changes, every menu item must be updated to enforce the + * new locale setting. + */ + public static void localeChanged( LocaleEvent event ) { + for ( MenuItem item : values() ) { + item.menuItem.setText( "" + item ); + } + } - /** - * When the locale changes, every menu item must be updated to enforce the - * new locale setting. - */ - public static void localeChanged( LocaleEvent event ) { - for ( MenuItem item : values() ) { - item.menuItem.setText( "" + item ); - } - } - } diff --git a/src/main/java/org/fenix/llanfair/Run.java b/src/main/java/org/fenix/llanfair/Run.java index 1cd7d2b..a9b5bac 100644 --- a/src/main/java/org/fenix/llanfair/Run.java +++ b/src/main/java/org/fenix/llanfair/Run.java @@ -1,5 +1,12 @@ package org.fenix.llanfair; +import org.fenix.llanfair.config.Settings; +import org.fenix.utils.TableModelSupport; +import org.fenix.utils.config.Configuration; + +import javax.swing.*; +import javax.swing.event.TableModelListener; +import javax.swing.table.TableModel; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.io.IOException; @@ -8,15 +15,6 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.List; -import javax.swing.Icon; -import javax.swing.event.TableModelListener; -import javax.swing.table.TableModel; -import org.fenix.llanfair.Language; -import org.fenix.llanfair.config.Settings; - -import org.fenix.utils.TableModelSupport; -import org.fenix.utils.config.Configuration; - /** * Modélise une course comportant un certain nombre de segments. Une fois une @@ -33,1119 +31,1119 @@ import org.fenix.utils.config.Configuration; */ public class Run implements TableModel, Serializable { - // -------------------------------------------------------------- CONSTANTS - - /** - * The serial version identifier used to determine the compatibility of the - * different serialized versions of this type. This identifier must change - * when modifications that break backward-compatibility are made to the - * type. - */ - public static final long serialVersionUID = 1010L; - - public static final int MAX_COUNTERS = 4; - - /** - * Identifier for the column containing the segment’s icon. - */ - public static final int COLUMN_ICON = 0; - - /** - * Identifier for the column containing the segment’s name. - */ - public static final int COLUMN_NAME = 1; - - /** - * Identifier for the column containing the segment’s run time. - */ - public static final int COLUMN_TIME = 2; - - /** - * Identifier for the column containing the segment’s segment time. - */ - public static final int COLUMN_SEGMENT = 3; - - /** - * Identifier for the column containing the segment’s best segment time. - */ - public static final int COLUMN_BEST = 4; - - /** - * Number of columns displayed in the table of segments. - */ - private static final int COLUMN_COUNT = 5; - - /** - * Identifier for the bean property name of the run. - */ - public static final String NAME_PROPERTY = "run.name"; - - /** - * Identifier for the bean property state of the run. - */ - public static final String STATE_PROPERTY = "run.state"; - - /** - * Identifier for the bean property current segment of the run. - */ - public static final String CURRENT_SEGMENT_PROPERTY = "run.currentSegment"; - - public static final String GOAL_PROPERTY = "run.goal"; - - public static final String COUNTER_VALUE_PROPERTY = "run.counters.value"; - - public static final String COUNTER_ADD_PROPERTY = "run.counters.add"; - - public static final String COUNTER_REMOVE_PROPERTY = "run.counters.remove"; - - public static final String COUNTER_EDIT_PROPERTY = "run.counters.edit"; - - // ------------------------------------------------------------- ATTRIBUTES - - /** - * The name of the run. - */ - private String name; - - /** - * Current state of the run. Transient as a deserialized run will always - * start in {@link State#READY}. - */ - private transient State state; - - /** - * Backup copy of the run’s state. Transient as it is only used during - * editing of the run. - */ - private transient State stateBackup; - - /** - * List of all the segments contained within this run. - */ - private List segments; - - /** - * Backup copy of the segments’ list. Necessary to buffer edits from - * {@code JTable}s and revert to the original state in case the user - * cancels. - */ - private transient List segmentsBackup; - - /** - * Index of the segment being currently run. Only represents a segment when - * the run is {@link State#ONGOING}. - */ - private transient int current; - - /** - * Number of milliseconds on the clock when the run started. - */ - private transient long startTime; - - /** - * Delegate handling {@code PropertyChangeEvent}s. - */ - private transient PropertyChangeSupport pcSupport; - - /** - * Delegate handling {@code TableModelEvent}s. - */ - private transient TableModelSupport tmSupport; - - private String goal; - - private boolean segmented; - - private List counters; - - private Configuration configuration; - - // ----------------------------------------------------------- CONSTRUCTORS - - /** - * Creates a new run of given name. This run has no segments and so begins - * in the state {@link State#NULL}. - * - * @param name - the name of the run - */ - public Run(String name) { - if (name == null) { - throw new NullPointerException("null run name"); - } - this.name = name; - segments = new ArrayList(); - goal = ""; - segmented = false; - counters = new ArrayList(); - initializeTransients(); - } - - /** - * Creates a default run without any segments and a default placeholder - * run name. This new run starts in the state {@code State.NULL}. - */ - public Run() { - this("" + Language.UNTITLED); - } - - // ---------------------------------------------------------------- GETTERS - - /** - * Returns the name of the run. - * - * @return the name of the run. - */ - public String getName() { - return name; - } - - public String getGoal() { - return goal; - } - - public boolean isSegmented() { - return segmented; - } - - /** - * Returns the current state of the run - * - * @return the current state of the run. - */ - public State getState() { - return state; - } - - /** - * Returns the number of milliseconds on the clock when the run started. - * - * @return the run’s start time stamp. - */ - public long getStartTime() { - return startTime; - } - - public Counters getCounter(int index) { - if (index < 0 || index >= MAX_COUNTERS) { - throw new IllegalArgumentException("illegal counter id " + index); - } - return counters.get(index); - } - - /** - * Returns the index of the segment being currently run. Only represents a - * segment when the run is {@link State#ONGOING}. - * - * @return the current segment index. - */ - public int getCurrent() { - return current; - } - - /** - * Returns the index of the previously runned segment. Only represents a - * segment if {@link #hasPreviousSegment()}. - * - * @return the previous segment index. - */ - public int getPrevious() { - return current - 1; - } - - /** - * Indicates wether or not a previous segment is available. This is the case - * if and only if the run is {@link State#ONGOING} or {@link State#STOPPED} - * and we currently are on the second segment of farther down the run. - * - * @return wether a previous segment is available or not. - */ - public boolean hasPreviousSegment() { - return current > 0; - } - - /** - * Returns the segment of given index. The index must be within the range - * {@code [0..getSegmentCount()[}. - * - * @param segmentIndex - the index of the segment to return. - * @return the segment of given index. - */ - public Segment getSegment(int segmentIndex) { - return segments.get(segmentIndex); - } - - /** - * Returns the run time of given type up to the given segment. Such a time - * can be {@code null} if the last segment has an undefined time. - * - * @param segmentIndex - index of the segment up to which get the time. - * @param type - one of the identifier. - * @return the run time up to the given segment - * @see Segment#getTime(int) - */ - public Time getTime(int segmentIndex, int type) { - return getTime(segmentIndex, type, true); - } - - /** - * Returns the run time of given type. Such a time can be {@code null} if - * the last segment has an undefined time. - * - * @param type - one of the identifier. - * @return the run time of given type. - * @see Segment#getTime(int) - */ - public Time getTime(int type) { - return getTime(getRowCount() - 1, type); - } - - public Time getTime(int segmentIndex, int type, boolean allowNull) { - if (segmentIndex < 0 || segmentIndex >= getRowCount()) { - return null; - } - if (type == Segment.DELTA) { - Time set = getTime(segmentIndex, Segment.SET); - Time live = getTime(segmentIndex, Segment.LIVE); - return (set == null ? null : Time.getDelta(live, set)); - } - Time runTime = new Time(); - for (int i = 0; i <= segmentIndex; i++) { - runTime.add(segments.get(i).getTime(type)); - } - Time lastSegmentTime = segments.get(segmentIndex).getTime(type); - return allowNull ? (lastSegmentTime == null ? null : runTime) : runTime; - } - - /** - * Returns a portion of the registered run time to which live times should - * be compared. This is equal to {@link Settings#COMPARE_PERCENT} percent of - * the whole run time. This allows for more visually detailed comparison. - * - * @return {@code P%} of {@code getTime(Segment.SET)} where {@code P} is - * the configured compare percent. - */ - public Time getCompareTime() { - int i = 2; - Time time = getTime(Segment.SET); - while (time == null) { - if (getRowCount() - i < 0) { - return Time.ZERO; - } - time = getTime(getRowCount() - i, Segment.SET); - i++; - } - long ms = time.getMilliseconds(); - float pc = Settings.GPH_SCAL.get(); - - return new Time((long) (ms * pc) / 100L); - } - - /** - * Returns the maximum height in pixels of the icons assigned to the - * segments of this run. Since icons are scaled down proportionnaly, the - * exact height can be lower than the configured icon size (if they are - * wider than high.) - * - * @return the exact maximum height of the icons of this run's segments. - */ - public int getMaxIconHeight() { - int max = 0; - for (Segment segment : segments) { - Icon icon = segment.getIcon(); - if (icon != null) { - max = Math.max(max, icon.getIconHeight()); - } - } - return max; - } - - /** - * Indicates wether the live run is better than the registered one or not, - * i.e. if we've established a new personal best. Cannot be {@code true} if - * the run hasn't reached the last segment. - * - * @return wether or not this run is a new personal best. - */ - public boolean isPersonalBest() { - if (current < getRowCount()) { - return false; - } - Time live = getTime(Segment.LIVE); - Time run = getTime(Segment.RUN); - return (live.compareTo(run) < 0); - } - - /** - * Indicates wehter or not any live segments time are better than their best - * registered time. Any time is always better than an undefined time. - * - * @return wether or not this run has new segments' best. - */ - public boolean hasSegmentsBest() { - for (int i = 0; i < current; i++) { - Segment segment = segments.get(i); - Time live = segment.getTime(Segment.LIVE); - Time best = segment.getTime(Segment.BEST); - - if (live != null && live.compareTo(best) < 0) { - return true; - } - } - return false; - } - - /** - * Returns wether of not the given segment live time is better than its - * registered time. If that time is undefined, we check using the sum of - * the live time from the previous non-null segment. - * - * @param index - the index of the segment. - * @return wether or not this segment live time is better. - */ - public boolean isBetterSegment(int index) { - Segment segment = getSegment(index); - Time set = segment.getTime(Segment.SET); - Time live = segment.getTime(Segment.LIVE); - - if (live == null) { - return false; - } - live = live.clone(); - - if (set != null) { - set = set.clone(); - } - - if (index > 0) { - for (int i = index - 1; i >= 0; i--) { - Segment prev = getSegment(i); - if (prev.getTime(Segment.SET) == null) { - live.add(prev.getTime(Segment.LIVE)); - } else { - break; - } - } - for (int i = index - 1; i >= 0; i--) { - Segment prev = getSegment(i); - if (prev.getTime(Segment.LIVE) == null) { - set.add(prev.getTime(Segment.SET)); - } else { - break; - } - } - } - return live.compareTo(set) < 0; - } - - public boolean isBestSegment(int index) { - Segment segment = getSegment(index); - Time set = segment.getTime(Segment.BEST); - Time live = segment.getTime(Segment.LIVE); - - if (live == null) { - return false; - } - live = live.clone(); - - if (set != null) { - set = set.clone(); - } - - if (index > 0) { - for (int i = index - 1; i >= 0; i--) { - Segment prev = getSegment(i); - if (prev.getTime(Segment.BEST) == null) { - live.add(prev.getTime(Segment.LIVE)); - } else { - break; - } - } - for (int i = index - 1; i >= 0; i--) { - Segment prev = getSegment(i); - if (prev.getTime(Segment.LIVE) == null) { - set.add(prev.getTime(Segment.BEST)); - } else { - break; - } - } - } - return live.compareTo(set) < 0; - } - - // ------------------------------------------------------ INHERITED GETTERS - - /** - * As specified by {@code TableModel}. - */ - public int getColumnCount() { - return COLUMN_COUNT; - } - - /** - * As specified by {@code TableModel}. - * Equivalent to the number of segments in the run. - */ - public int getRowCount() { - return segments.size(); - } - - /** - * As specified by {@code TableModel}. Always yield {@code true} as every - * information of every segment is editable. - */ - public boolean isCellEditable(int rowIndex, int columnIndex) { - return true; - } - - /** - * As specified by {@code TableModel}. - */ - public Class getColumnClass(int columnIndex) { - switch (columnIndex) { - case COLUMN_ICON: return Icon.class; - case COLUMN_NAME: return String.class; - case COLUMN_TIME: return Time.class; - case COLUMN_BEST: return Time.class; - case COLUMN_SEGMENT: return Time.class; - } - // Should not be reached. - return null; - } - - /** - * As specified by {@code TableModel}. - */ - public String getColumnName(int column) { - switch (column) { - case COLUMN_ICON: return "" + Language.ICON; - case COLUMN_NAME: return "" + Language.NAME; - case COLUMN_TIME: return "" + Language.TIME; - case COLUMN_BEST: return "" + Language.BEST; - case COLUMN_SEGMENT: return "" + Language.SEGMENT; - } - // Should not be reached. - return null; - } - - /** - * As specified by {@code TableModel}. - */ - public Object getValueAt(int row, int column) { - switch (column) { - case COLUMN_ICON: return getSegment(row).getIcon(); - case COLUMN_NAME: return getSegment(row).getName(); - case COLUMN_TIME: return getTime(row, Segment.RUN); - case COLUMN_BEST: return getSegment(row).getTime(Segment.BEST); - case COLUMN_SEGMENT: return getSegment(row).getTime(Segment.RUN); - } - // Should not be reached. - return null; - } - - // ---------------------------------------------------------------- SETTERS - - public T getSetting( String key ) { - return configuration.get( key ); - } - - public void putSetting( String key, Object value ) { - configuration.put( key, value ); - } - - public void addSettingChangeListener( PropertyChangeListener pcl ) { - configuration.addPropertyChangeListener( pcl ); - } - - public boolean containsSetting( String key ) { - return configuration.contains( key ); - } - - /** - * Sets the name of this run to the given string. - * - * @param name - the new name of the run. - */ - public void setName(String name) { - if (name == null) { - throw new NullPointerException("null run name"); - } - String old = this.name; - this.name = name; - pcSupport.firePropertyChange(NAME_PROPERTY, old, name); - } - - public void setSegmented(boolean segmented) { - this.segmented = segmented; - } - - public void setGoal(String goal) { - if (goal == null) { - throw new NullPointerException("null goal string"); - } - String old = this.goal; - this.goal = goal; - pcSupport.firePropertyChange(GOAL_PROPERTY, old, goal); - } - - /** - * 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. - * - * @param segment - the segment to add to this run. - */ - public void addSegment(Segment segment) { - if (segment == null) { - throw new NullPointerException("null segment"); - } - int oldCount = getRowCount(); - segments.add(segment); - tmSupport.fireTableRowsInserted(oldCount, oldCount); - - if (oldCount == 0) { - state = State.READY; - pcSupport.firePropertyChange(STATE_PROPERTY, State.NULL, state); - } - } - - /** - * Removes the segment of given index from the this run table of segments. - * If we remove the last segment, the run becomes {@link State#NULL}. - * - * @param segmentIndex - the index of the segment to remove. - */ - public void removeSegment(int segmentIndex) { - setValueAt(null, segmentIndex, 2); - segments.remove(segmentIndex); - tmSupport.fireTableRowsDeleted(segmentIndex, segmentIndex); - - if (getRowCount() == 0) { - State old = state; - state = State.NULL; - pcSupport.firePropertyChange(STATE_PROPERTY, old, state); - } - } - - /** - * Removes the segment of given index and inserts it back one position - * lower. The segment effectively moves one position upward. - * - * @param segmentIndex - the index of the segment to move. - */ - public void moveSegmentUp(int segmentIndex) { - if (segmentIndex > 0) { - Segment segment = segments.get(segmentIndex); - segments.remove(segmentIndex); - segments.add(segmentIndex - 1, segment); - tmSupport.fireTableStructureChanged(); - } - } - - /** - * Removes the segment of given index and inserts it back one position - * higher. The segment effectively moves one position downward. - * - * @param segmentIndex - the index of the segment to move. - */ - public void moveSegmentDown(int segmentIndex) { - if (segmentIndex < getRowCount() - 1) { - Segment segment = segments.get(segmentIndex); - segments.remove(segmentIndex); - segments.add(segmentIndex + 1, segment); - tmSupport.fireTableStructureChanged(); - } - } - - /** - * Starts the race. The clock time is saved as the run start time and the - * current segment becomes the first segment of the run. - * - * @throws IllegalStateException if the run is on-going or null. - */ - public void start() { - if (state == null || state == State.ONGOING) { - throw new IllegalStateException("illegal state to start"); - } - startTime = System.nanoTime() / 1000000L; - current = 0; - state = State.ONGOING; - segments.get(current).setStartTime(startTime); - - pcSupport.firePropertyChange(STATE_PROPERTY, State.READY, state); - pcSupport.firePropertyChange(CURRENT_SEGMENT_PROPERTY, -1, 0); - } - - /** - * Makes a split, saving the elapsed time for the current segment and - * setting the next segment as current. If we were at the last segment, - * the run becomes {@link State#STOPPED}. - * - * @throws IllegalStateException if the run is not on-going. - */ - public void split() { - if (state != State.ONGOING) { - throw new IllegalStateException("run is not on-going"); - } - long stopTime = System.nanoTime() / 1000000L; - long segmentTime = stopTime - getSegment(current).getStartTime(); - current = current + 1; - - Time time = new Time(segmentTime); - segments.get(current - 1).setTime(time, Segment.LIVE); - - if (current == getRowCount()) { - stop(); - } else { - segments.get(current).setStartTime(stopTime); - } - pcSupport.firePropertyChange( - CURRENT_SEGMENT_PROPERTY, current - 1, current); - if (segmented && state == State.ONGOING && current > -1) { - pause(); - } - } - - /** - * If a run is on-going and a split has been made, this method will cancel - * it, reverting to the state before said split and discarding the - * established live time. - * - * @throws IllegalStateException if the run is not on-going. - */ - public void unsplit() { - if (state == State.NULL || state == State.READY) { - throw new IllegalStateException("illegal run state"); - } - if (current > 0) { - current = current - 1; - getSegment(current).setTime(null, Segment.LIVE); - - pcSupport.firePropertyChange( - CURRENT_SEGMENT_PROPERTY, current + 1, current); - - if (state == State.STOPPED) { - state = State.ONGOING; - pcSupport.firePropertyChange( - STATE_PROPERTY, State.STOPPED, state); - } - } - } - - public void pause() { - if (state != State.ONGOING) { - throw new IllegalStateException("run is not on-going"); - } - state = State.PAUSED; - long stopTime = System.nanoTime() / 1000000L; - long segmentTime = stopTime - getSegment(current).getStartTime(); - Time time = new Time(segmentTime); - segments.get(current).setTime(time, Segment.LIVE, true); - pcSupport.firePropertyChange(STATE_PROPERTY, State.ONGOING, state); - } - - public void resume() { - if (state != State.PAUSED) { - throw new IllegalStateException("run is not paused"); - } - state = State.ONGOING; - long stop = System.nanoTime() / 1000000L; - startTime = stop - getTime(current, Segment.LIVE, false).getMilliseconds(); - - Segment crt = getSegment(current); - crt.setStartTime(stop - crt.getTime(Segment.LIVE).getMilliseconds()); - - long cumulative = 0L; - for (int i = 0; i < current; i++) { - Segment iSeg = getSegment(i); - iSeg.setStartTime(startTime + cumulative); - cumulative += iSeg.getTime(Segment.LIVE).getMilliseconds(); - } - pcSupport.firePropertyChange(STATE_PROPERTY, State.PAUSED, state); - } - - /** - * Stops the current on-going run. - * - * @throws IllegalStateException if the is not on-going. - */ - public void stop() { - if (state != State.ONGOING) { - throw new IllegalStateException("run is not on-going"); - } - state = State.STOPPED; - pcSupport.firePropertyChange(STATE_PROPERTY, State.ONGOING, state); - } - - /** - * Resets the current run, discarding any live times and becoming once - * again {@link State#READY}. - */ - public void reset() { - for (Segment segment : segments) { - segment.setTime(null, Segment.LIVE); - } - current = -1; - startTime = 0L; - - State old = state; - state = State.READY; - pcSupport.firePropertyChange(STATE_PROPERTY, old, state); - } - - /** - * Skips the current segment, setting its live segment time as undefined - * and moving to the next segment. Since it is not possible to skip the - * last segment, a next one always exist. The next segment time will be - * defined as the delta between the start of the previous non-null segment - * and the split time of this segment. - */ - public void skip() { - if (current > - 1 && current < getRowCount() - 1) { - Segment crtSegment = getSegment(current); - long segmentStart = crtSegment.getStartTime(); - crtSegment.setTime(null, Segment.LIVE); - - current = current + 1; - getSegment(current).setStartTime(segmentStart); - - pcSupport.firePropertyChange( - CURRENT_SEGMENT_PROPERTY, current - 1, current); - } - } - - /** - * Overwrites the registered with the live times, using the following - * algorithm: if a live segment time is better than its registered best - * time, the best time is overwritten. If we are not doing a {@code partial} - * save and the run is complete (its end was naturally reached), the - * registered segment time is overwritten with the live segment time. - * - * @param partial - wether to only save best times or the whole run. - */ - public void saveLiveTimes(boolean partial) { - boolean over = (current == getRowCount()); - for (Segment segment : segments) { - Time live = segment.getTime(Segment.LIVE); - if (live == null) { - if (!partial && over) { - segment.setTime(null, Segment.RUN); - } - } else { - if (live.compareTo(segment.getTime(Segment.BEST)) < 0) { - segment.setTime(live, Segment.BEST); - } - if (!partial && over) { - segment.setTime(live, Segment.RUN); - } - } - } - } - - /** - * Creates a backup copy of this run table of segments in order to be able - * to revert any changes made by the user using direct-editing components. - */ - public void saveBackup() { - stateBackup = state; - segmentsBackup = new ArrayList(); - for (Segment segment : segments) { - segmentsBackup.add(segment.clone()); - } - } - - /** - * Reverts this run table of segments to its original state prior to the - * call to {@link #saveBackup()}. Does nothing if no backup has been - * created. After this call, the existing backup will be discarded. - */ - public void loadBackup() { - if (segmentsBackup != null) { - State old = state; - state = stateBackup; - segments = new ArrayList(); - for (Segment segment : segmentsBackup) { - segments.add(segment); - } - segmentsBackup = null; - tmSupport.fireTableStructureChanged(); - pcSupport.firePropertyChange(STATE_PROPERTY, old, state); - } - } - - /** - * Sets the split time of the given segment. If the new value is - * {@code null} (meaning the time is undefined,) the segment time and best - * time also becomes {@code null} and the previously set segment time is - * added to the next non-null segment time. If it is not, the delta between - * the new and the old value is added to the next non-null segment and the - * new segment time is computed using the previous non-null segment time. - * - * @param index - the index of the segment to set a split time to. - * @param time - the new split time. - */ - public void setSplitTime(int index, Time time) { - Time oldTime = getTime(index, Segment.RUN); - - if (time == null) { - setSegmentTime(index, null); - } else { - // Compute the time added to/removed from the segment. - if (oldTime == null) { - if (index > 0) { - oldTime = getTime(index - 1, Segment.RUN); - } - } - Time delta = Time.getDelta(oldTime, time); - Time pTime = new Time(); - - // Find the first previous non-null segment. - for (int i = index - 1; i >= 0; i--) { - if (getSegment(i).getTime(Segment.RUN) != null) { - pTime = getTime(i, Segment.RUN); - break; - } - } - // If a next non-null segment exist possess a split time - // inferior to the split time we are defining, we add - // the delta to preserve consistency. - for (int i = index + 1; i < getRowCount(); i++) { - Segment nSegment = getSegment(i); - if (nSegment.getTime(Segment.RUN) != null) { - Time nTime = getTime(i, Segment.RUN); - if (time.compareTo(nTime) < 0 - && time.compareTo(pTime) > 0) { - nSegment.getTime(Segment.RUN).add(delta); - } - break; - } - } - time = Time.getDelta(time, pTime); - Segment segment = getSegment(index); - segment.setTime(time, Segment.RUN); - - if (time.compareTo(segment.getTime(Segment.BEST)) < 0) { - segment.setTime(time, Segment.BEST); - } - } - } - - /** - * Registers the given listener as a new {@code PropertyChangeListener} for - * this run. - * - * @param pcl - the listener to register. - */ - public void addPropertyChangeListener(PropertyChangeListener pcl) { - pcSupport.addPropertyChangeListener(pcl); - } - - /** - * Removes the given listener from the list of registered - * {@code PropertyChangeListener} of this run. - * - * @param pcl - the listener to register. - */ - public void removePropertyChangeListener(PropertyChangeListener pcl) { - pcSupport.removePropertyChangeListener(pcl); - } - - // ------------------------------------------------------ INHERITED SETTERS - - /** - * Registers the given listener as a new {@code TableModelListener} for - * this run table of segments. - * - * @param tml - the listener to register. - */ - public void addTableModelListener(TableModelListener tml) { - tmSupport.addTableModelListener(tml); - } - - /** - * Removes the given listener from the list of registered - * {@code TableModelListener} of this run. - * - * @param tml - the listener to register. - */ - public void removeTableModelListener(TableModelListener tml) { - tmSupport.removeTableModelListener(tml); - } - - /** - * As specified by {@code TableModel}. Sets the value of given column for - * the segment of given row index. - */ - public void setValueAt(Object value, int row, int column) { - Segment segment = getSegment(row); - switch (column) { - case COLUMN_ICON: - segment.setIcon((Icon) value); - tmSupport.fireTableCellUpdated(row, column); - break; - - case COLUMN_NAME: - segment.setName((String) value); - tmSupport.fireTableCellUpdated(row, column); - break; - - case COLUMN_TIME: - Time newTime = (Time) value; - setSplitTime(row, newTime); - tmSupport.fireTableDataChanged(); - break; - - case COLUMN_SEGMENT: - setSegmentTime(row, (Time) value); - tmSupport.fireTableDataChanged(); - break; - - case COLUMN_BEST: - newTime = (Time) value; - Time runTime = segment.getTime(Segment.RUN); - if (newTime == null || newTime.compareTo(runTime) > 0) { - segment.setTime(runTime, Segment.BEST); - } else { - segment.setTime(newTime, Segment.BEST); - } - tmSupport.fireTableDataChanged(); - break; - } - } - - // -------------------------------------------------------------- UTILITIES - - /** - * Sets the segment time of the given segment. If the segment time is - * better than the best time, the best time is updated accordingly. - * - * @param index - the index of the segment to set a split time to. - * @param time - the new split time. - */ - private void setSegmentTime(int index, Time time) { - if (time != null && time.compareTo(Time.ZERO) <= 0) { - throw new IllegalArgumentException("" + Language.ILLEGAL_TIME); - } - Segment segment = segments.get(index); - Time best = segment.getTime(Segment.BEST); - Time old = segment.getTime(Segment.RUN); - int rowCount = getRowCount(); - - if (time == null) { - // The old segment time of the newly undefined segment goes - // to the next non-null segment to preserve split times - // consistency. - for (int i = index + 1; i < rowCount; i++) { - Time nTime = getSegment(i).getTime(Segment.RUN); - if (nTime != null) { - nTime.add(old); - break; - } - } - - } else if (old == null) { - for (int i = index + 1; i < rowCount; i++) { - Segment nSeg = getSegment(i); - Time nTime = nSeg.getTime(Segment.RUN); - if (nTime != null) { - if (nTime.compareTo(time) <= 0) { - throw new IllegalArgumentException( - "" + Language.ILLEGAL_SEGMENT_TIME); - } - nSeg.setTime(Time.getDelta(nTime, time), Segment.RUN); - break; - } - } - - } - segment.setTime(time, Segment.RUN); - - if (time != null) { - if (time.compareTo(best) < 0) { - segment.setTime(time, Segment.BEST); - } - } - } - - /** - * Initialize all transient fields. - */ - private void initializeTransients() { - pcSupport = new PropertyChangeSupport(this); - tmSupport = new TableModelSupport(this); - segmentsBackup = null; - stateBackup = null; - state = getRowCount() > 0 ? State.READY : State.NULL; - current = -1; - startTime = 0L; - - if (goal == null) { - goal = ""; - } - if (counters == null) { - counters = new ArrayList(); - } - if ( configuration == null ) { - configuration = new Configuration(); - } - } - - /** - * Deserialization process. Redefined to initialize transients fields upon - * deserialization. - */ - private void readObject(ObjectInputStream in) - throws IOException, ClassNotFoundException { - in.defaultReadObject(); - initializeTransients(); - } - - // ---------------------------------------------------------- INTERNAL TYPE - - /** - * Enumeration of a run possible state. - * - * @author Xavier "Xunkar" Sencert - */ - public static enum State { - - // ---------------------------------------------------- ENUMERATES - - /** - * The run cannot start, probably because there are no segments. - */ - NULL, - - /** - * The run is ready to start. This state should be achieved upon - * opening a run and resetting it. - */ - READY, - - /** - * The run is being runned. A current segment now exists. - */ - ONGOING, - - /** - * The run was stopped, either naturally by making the last split or - * voluntarily by the user. - */ - STOPPED, - - /** - * The run was paused by the user. The timer should not be running - * while a run is paused. - */ - PAUSED; - - // ----------------------------------------------------- CONSTANTS - - /** - * The serial version identifier used to determine the compatibility of - * the different serialized versions of this type. This identifier must - * change when modifications that break backward-compatibility are made - * to the type. - */ - private static final long serialVersionUID = 1000L; - } + // -------------------------------------------------------------- CONSTANTS + + /** + * The serial version identifier used to determine the compatibility of the + * different serialized versions of this type. This identifier must change + * when modifications that break backward-compatibility are made to the + * type. + */ + public static final long serialVersionUID = 1010L; + + public static final int MAX_COUNTERS = 4; + + /** + * Identifier for the column containing the segment’s icon. + */ + public static final int COLUMN_ICON = 0; + + /** + * Identifier for the column containing the segment’s name. + */ + public static final int COLUMN_NAME = 1; + + /** + * Identifier for the column containing the segment’s run time. + */ + public static final int COLUMN_TIME = 2; + + /** + * Identifier for the column containing the segment’s segment time. + */ + public static final int COLUMN_SEGMENT = 3; + + /** + * Identifier for the column containing the segment’s best segment time. + */ + public static final int COLUMN_BEST = 4; + + /** + * Number of columns displayed in the table of segments. + */ + private static final int COLUMN_COUNT = 5; + + /** + * Identifier for the bean property name of the run. + */ + public static final String NAME_PROPERTY = "run.name"; + + /** + * Identifier for the bean property state of the run. + */ + public static final String STATE_PROPERTY = "run.state"; + + /** + * Identifier for the bean property current segment of the run. + */ + public static final String CURRENT_SEGMENT_PROPERTY = "run.currentSegment"; + + public static final String GOAL_PROPERTY = "run.goal"; + + public static final String COUNTER_VALUE_PROPERTY = "run.counters.value"; + + public static final String COUNTER_ADD_PROPERTY = "run.counters.add"; + + public static final String COUNTER_REMOVE_PROPERTY = "run.counters.remove"; + + public static final String COUNTER_EDIT_PROPERTY = "run.counters.edit"; + + // ------------------------------------------------------------- ATTRIBUTES + + /** + * The name of the run. + */ + private String name; + + /** + * Current state of the run. Transient as a deserialized run will always + * start in {@link State#READY}. + */ + private transient State state; + + /** + * Backup copy of the run’s state. Transient as it is only used during + * editing of the run. + */ + private transient State stateBackup; + + /** + * List of all the segments contained within this run. + */ + private List segments; + + /** + * Backup copy of the segments’ list. Necessary to buffer edits from + * {@code JTable}s and revert to the original state in case the user + * cancels. + */ + private transient List segmentsBackup; + + /** + * Index of the segment being currently run. Only represents a segment when + * the run is {@link State#ONGOING}. + */ + private transient int current; + + /** + * Number of milliseconds on the clock when the run started. + */ + private transient long startTime; + + /** + * Delegate handling {@code PropertyChangeEvent}s. + */ + private transient PropertyChangeSupport pcSupport; + + /** + * Delegate handling {@code TableModelEvent}s. + */ + private transient TableModelSupport tmSupport; + + private String goal; + + private boolean segmented; + + private List counters; + + private Configuration configuration; + + // ----------------------------------------------------------- CONSTRUCTORS + + /** + * Creates a new run of given name. This run has no segments and so begins + * in the state {@link State#NULL}. + * + * @param name - the name of the run + */ + public Run(String name) { + if (name == null) { + throw new NullPointerException("null run name"); + } + this.name = name; + segments = new ArrayList(); + goal = ""; + segmented = false; + counters = new ArrayList(); + initializeTransients(); + } + + /** + * Creates a default run without any segments and a default placeholder + * run name. This new run starts in the state {@code State.NULL}. + */ + public Run() { + this("" + Language.UNTITLED); + } + + // ---------------------------------------------------------------- GETTERS + + /** + * Returns the name of the run. + * + * @return the name of the run. + */ + public String getName() { + return name; + } + + public String getGoal() { + return goal; + } + + public boolean isSegmented() { + return segmented; + } + + /** + * Returns the current state of the run + * + * @return the current state of the run. + */ + public State getState() { + return state; + } + + /** + * Returns the number of milliseconds on the clock when the run started. + * + * @return the run’s start time stamp. + */ + public long getStartTime() { + return startTime; + } + + public Counters getCounter(int index) { + if (index < 0 || index >= MAX_COUNTERS) { + throw new IllegalArgumentException("illegal counter id " + index); + } + return counters.get(index); + } + + /** + * Returns the index of the segment being currently run. Only represents a + * segment when the run is {@link State#ONGOING}. + * + * @return the current segment index. + */ + public int getCurrent() { + return current; + } + + /** + * Returns the index of the previously runned segment. Only represents a + * segment if {@link #hasPreviousSegment()}. + * + * @return the previous segment index. + */ + public int getPrevious() { + return current - 1; + } + + /** + * Indicates wether or not a previous segment is available. This is the case + * if and only if the run is {@link State#ONGOING} or {@link State#STOPPED} + * and we currently are on the second segment of farther down the run. + * + * @return wether a previous segment is available or not. + */ + public boolean hasPreviousSegment() { + return current > 0; + } + + /** + * Returns the segment of given index. The index must be within the range + * {@code [0..getSegmentCount()[}. + * + * @param segmentIndex - the index of the segment to return. + * @return the segment of given index. + */ + public Segment getSegment(int segmentIndex) { + return segments.get(segmentIndex); + } + + /** + * Returns the run time of given type up to the given segment. Such a time + * can be {@code null} if the last segment has an undefined time. + * + * @param segmentIndex - index of the segment up to which get the time. + * @param type - one of the identifier. + * @return the run time up to the given segment + * @see Segment#getTime(int) + */ + public Time getTime(int segmentIndex, int type) { + return getTime(segmentIndex, type, true); + } + + /** + * Returns the run time of given type. Such a time can be {@code null} if + * the last segment has an undefined time. + * + * @param type - one of the identifier. + * @return the run time of given type. + * @see Segment#getTime(int) + */ + public Time getTime(int type) { + return getTime(getRowCount() - 1, type); + } + + public Time getTime(int segmentIndex, int type, boolean allowNull) { + if (segmentIndex < 0 || segmentIndex >= getRowCount()) { + return null; + } + if (type == Segment.DELTA) { + Time set = getTime(segmentIndex, Segment.SET); + Time live = getTime(segmentIndex, Segment.LIVE); + return (set == null ? null : Time.getDelta(live, set)); + } + Time runTime = new Time(); + for (int i = 0; i <= segmentIndex; i++) { + runTime.add(segments.get(i).getTime(type)); + } + Time lastSegmentTime = segments.get(segmentIndex).getTime(type); + return allowNull ? (lastSegmentTime == null ? null : runTime) : runTime; + } + + /** + * Returns a portion of the registered run time to which live times should + * be compared. This is equal to {@link Settings#COMPARE_PERCENT} percent of + * the whole run time. This allows for more visually detailed comparison. + * + * @return {@code P%} of {@code getTime(Segment.SET)} where {@code P} is + * the configured compare percent. + */ + public Time getCompareTime() { + int i = 2; + Time time = getTime(Segment.SET); + while (time == null) { + if (getRowCount() - i < 0) { + return Time.ZERO; + } + time = getTime(getRowCount() - i, Segment.SET); + i++; + } + long ms = time.getMilliseconds(); + float pc = Settings.GPH_SCAL.get(); + + return new Time((long) (ms * pc) / 100L); + } + + /** + * Returns the maximum height in pixels of the icons assigned to the + * segments of this run. Since icons are scaled down proportionnaly, the + * exact height can be lower than the configured icon size (if they are + * wider than high.) + * + * @return the exact maximum height of the icons of this run's segments. + */ + public int getMaxIconHeight() { + int max = 0; + for (Segment segment : segments) { + Icon icon = segment.getIcon(); + if (icon != null) { + max = Math.max(max, icon.getIconHeight()); + } + } + return max; + } + + /** + * Indicates wether the live run is better than the registered one or not, + * i.e. if we've established a new personal best. Cannot be {@code true} if + * the run hasn't reached the last segment. + * + * @return wether or not this run is a new personal best. + */ + public boolean isPersonalBest() { + if (current < getRowCount()) { + return false; + } + Time live = getTime(Segment.LIVE); + Time run = getTime(Segment.RUN); + return (live.compareTo(run) < 0); + } + + /** + * Indicates wehter or not any live segments time are better than their best + * registered time. Any time is always better than an undefined time. + * + * @return wether or not this run has new segments' best. + */ + public boolean hasSegmentsBest() { + for (int i = 0; i < current; i++) { + Segment segment = segments.get(i); + Time live = segment.getTime(Segment.LIVE); + Time best = segment.getTime(Segment.BEST); + + if (live != null && live.compareTo(best) < 0) { + return true; + } + } + return false; + } + + /** + * Returns wether of not the given segment live time is better than its + * registered time. If that time is undefined, we check using the sum of + * the live time from the previous non-null segment. + * + * @param index - the index of the segment. + * @return wether or not this segment live time is better. + */ + public boolean isBetterSegment(int index) { + Segment segment = getSegment(index); + Time set = segment.getTime(Segment.SET); + Time live = segment.getTime(Segment.LIVE); + + if (live == null) { + return false; + } + live = live.clone(); + + if (set != null) { + set = set.clone(); + } + + if (index > 0) { + for (int i = index - 1; i >= 0; i--) { + Segment prev = getSegment(i); + if (prev.getTime(Segment.SET) == null) { + live.add(prev.getTime(Segment.LIVE)); + } else { + break; + } + } + for (int i = index - 1; i >= 0; i--) { + Segment prev = getSegment(i); + if (prev.getTime(Segment.LIVE) == null) { + set.add(prev.getTime(Segment.SET)); + } else { + break; + } + } + } + return live.compareTo(set) < 0; + } + + public boolean isBestSegment(int index) { + Segment segment = getSegment(index); + Time set = segment.getTime(Segment.BEST); + Time live = segment.getTime(Segment.LIVE); + + if (live == null) { + return false; + } + live = live.clone(); + + if (set != null) { + set = set.clone(); + } + + if (index > 0) { + for (int i = index - 1; i >= 0; i--) { + Segment prev = getSegment(i); + if (prev.getTime(Segment.BEST) == null) { + live.add(prev.getTime(Segment.LIVE)); + } else { + break; + } + } + for (int i = index - 1; i >= 0; i--) { + Segment prev = getSegment(i); + if (prev.getTime(Segment.LIVE) == null) { + set.add(prev.getTime(Segment.BEST)); + } else { + break; + } + } + } + return live.compareTo(set) < 0; + } + + // ------------------------------------------------------ INHERITED GETTERS + + /** + * As specified by {@code TableModel}. + */ + public int getColumnCount() { + return COLUMN_COUNT; + } + + /** + * As specified by {@code TableModel}. + * Equivalent to the number of segments in the run. + */ + public int getRowCount() { + return segments.size(); + } + + /** + * As specified by {@code TableModel}. Always yield {@code true} as every + * information of every segment is editable. + */ + public boolean isCellEditable(int rowIndex, int columnIndex) { + return true; + } + + /** + * As specified by {@code TableModel}. + */ + public Class getColumnClass(int columnIndex) { + switch (columnIndex) { + case COLUMN_ICON: return Icon.class; + case COLUMN_NAME: return String.class; + case COLUMN_TIME: return Time.class; + case COLUMN_BEST: return Time.class; + case COLUMN_SEGMENT: return Time.class; + } + // Should not be reached. + return null; + } + + /** + * As specified by {@code TableModel}. + */ + public String getColumnName(int column) { + switch (column) { + case COLUMN_ICON: return "" + Language.ICON; + case COLUMN_NAME: return "" + Language.NAME; + case COLUMN_TIME: return "" + Language.TIME; + case COLUMN_BEST: return "" + Language.BEST; + case COLUMN_SEGMENT: return "" + Language.SEGMENT; + } + // Should not be reached. + return null; + } + + /** + * As specified by {@code TableModel}. + */ + public Object getValueAt(int row, int column) { + switch (column) { + case COLUMN_ICON: return getSegment(row).getIcon(); + case COLUMN_NAME: return getSegment(row).getName(); + case COLUMN_TIME: return getTime(row, Segment.RUN); + case COLUMN_BEST: return getSegment(row).getTime(Segment.BEST); + case COLUMN_SEGMENT: return getSegment(row).getTime(Segment.RUN); + } + // Should not be reached. + return null; + } + + // ---------------------------------------------------------------- SETTERS + + public T getSetting( String key ) { + return configuration.get(key); + } + + public void putSetting( String key, Object value ) { + configuration.put( key, value ); + } + + public void addSettingChangeListener( PropertyChangeListener pcl ) { + configuration.addPropertyChangeListener( pcl ); + } + + public boolean containsSetting( String key ) { + return configuration.contains( key ); + } + + /** + * Sets the name of this run to the given string. + * + * @param name - the new name of the run. + */ + public void setName(String name) { + if (name == null) { + throw new NullPointerException("null run name"); + } + String old = this.name; + this.name = name; + pcSupport.firePropertyChange(NAME_PROPERTY, old, name); + } + + public void setSegmented(boolean segmented) { + this.segmented = segmented; + } + + public void setGoal(String goal) { + if (goal == null) { + throw new NullPointerException("null goal string"); + } + String old = this.goal; + this.goal = goal; + pcSupport.firePropertyChange(GOAL_PROPERTY, old, goal); + } + + /** + * 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. + * + * @param segment - the segment to add to this run. + */ + public void addSegment(Segment segment) { + if (segment == null) { + throw new NullPointerException("null segment"); + } + int oldCount = getRowCount(); + segments.add(segment); + tmSupport.fireTableRowsInserted(oldCount, oldCount); + + if (oldCount == 0) { + state = State.READY; + pcSupport.firePropertyChange(STATE_PROPERTY, State.NULL, state); + } + } + + /** + * Removes the segment of given index from the this run table of segments. + * If we remove the last segment, the run becomes {@link State#NULL}. + * + * @param segmentIndex - the index of the segment to remove. + */ + public void removeSegment(int segmentIndex) { + setValueAt(null, segmentIndex, 2); + segments.remove(segmentIndex); + tmSupport.fireTableRowsDeleted(segmentIndex, segmentIndex); + + if (getRowCount() == 0) { + State old = state; + state = State.NULL; + pcSupport.firePropertyChange(STATE_PROPERTY, old, state); + } + } + + /** + * Removes the segment of given index and inserts it back one position + * lower. The segment effectively moves one position upward. + * + * @param segmentIndex - the index of the segment to move. + */ + public void moveSegmentUp(int segmentIndex) { + if (segmentIndex > 0) { + Segment segment = segments.get(segmentIndex); + segments.remove(segmentIndex); + segments.add(segmentIndex - 1, segment); + tmSupport.fireTableStructureChanged(); + } + } + + /** + * Removes the segment of given index and inserts it back one position + * higher. The segment effectively moves one position downward. + * + * @param segmentIndex - the index of the segment to move. + */ + public void moveSegmentDown(int segmentIndex) { + if (segmentIndex < getRowCount() - 1) { + Segment segment = segments.get(segmentIndex); + segments.remove(segmentIndex); + segments.add(segmentIndex + 1, segment); + tmSupport.fireTableStructureChanged(); + } + } + + /** + * Starts the race. The clock time is saved as the run start time and the + * current segment becomes the first segment of the run. + * + * @throws IllegalStateException if the run is on-going or null. + */ + public void start() { + if (state == null || state == State.ONGOING) { + throw new IllegalStateException("illegal state to start"); + } + startTime = System.nanoTime() / 1000000L; + current = 0; + state = State.ONGOING; + segments.get(current).setStartTime(startTime); + + pcSupport.firePropertyChange(STATE_PROPERTY, State.READY, state); + pcSupport.firePropertyChange(CURRENT_SEGMENT_PROPERTY, -1, 0); + } + + /** + * Makes a split, saving the elapsed time for the current segment and + * setting the next segment as current. If we were at the last segment, + * the run becomes {@link State#STOPPED}. + * + * @throws IllegalStateException if the run is not on-going. + */ + public void split() { + if (state != State.ONGOING) { + throw new IllegalStateException("run is not on-going"); + } + long stopTime = System.nanoTime() / 1000000L; + long segmentTime = stopTime - getSegment(current).getStartTime(); + current = current + 1; + + Time time = new Time(segmentTime); + segments.get(current - 1).setTime(time, Segment.LIVE); + + if (current == getRowCount()) { + stop(); + } else { + segments.get(current).setStartTime(stopTime); + } + pcSupport.firePropertyChange( + CURRENT_SEGMENT_PROPERTY, current - 1, current); + if (segmented && state == State.ONGOING && current > -1) { + pause(); + } + } + + /** + * If a run is on-going and a split has been made, this method will cancel + * it, reverting to the state before said split and discarding the + * established live time. + * + * @throws IllegalStateException if the run is not on-going. + */ + public void unsplit() { + if (state == State.NULL || state == State.READY) { + throw new IllegalStateException("illegal run state"); + } + if (current > 0) { + current = current - 1; + getSegment(current).setTime(null, Segment.LIVE); + + pcSupport.firePropertyChange( + CURRENT_SEGMENT_PROPERTY, current + 1, current); + + if (state == State.STOPPED) { + state = State.ONGOING; + pcSupport.firePropertyChange( + STATE_PROPERTY, State.STOPPED, state); + } + } + } + + public void pause() { + if (state != State.ONGOING) { + throw new IllegalStateException("run is not on-going"); + } + state = State.PAUSED; + long stopTime = System.nanoTime() / 1000000L; + long segmentTime = stopTime - getSegment(current).getStartTime(); + Time time = new Time(segmentTime); + segments.get(current).setTime(time, Segment.LIVE, true); + pcSupport.firePropertyChange(STATE_PROPERTY, State.ONGOING, state); + } + + public void resume() { + if (state != State.PAUSED) { + throw new IllegalStateException("run is not paused"); + } + state = State.ONGOING; + long stop = System.nanoTime() / 1000000L; + startTime = stop - getTime(current, Segment.LIVE, false).getMilliseconds(); + + Segment crt = getSegment(current); + crt.setStartTime(stop - crt.getTime(Segment.LIVE).getMilliseconds()); + + long cumulative = 0L; + for (int i = 0; i < current; i++) { + Segment iSeg = getSegment(i); + iSeg.setStartTime(startTime + cumulative); + cumulative += iSeg.getTime(Segment.LIVE).getMilliseconds(); + } + pcSupport.firePropertyChange(STATE_PROPERTY, State.PAUSED, state); + } + + /** + * Stops the current on-going run. + * + * @throws IllegalStateException if the is not on-going. + */ + public void stop() { + if (state != State.ONGOING) { + throw new IllegalStateException("run is not on-going"); + } + state = State.STOPPED; + pcSupport.firePropertyChange(STATE_PROPERTY, State.ONGOING, state); + } + + /** + * Resets the current run, discarding any live times and becoming once + * again {@link State#READY}. + */ + public void reset() { + for (Segment segment : segments) { + segment.setTime(null, Segment.LIVE); + } + current = -1; + startTime = 0L; + + State old = state; + state = State.READY; + pcSupport.firePropertyChange(STATE_PROPERTY, old, state); + } + + /** + * Skips the current segment, setting its live segment time as undefined + * and moving to the next segment. Since it is not possible to skip the + * last segment, a next one always exist. The next segment time will be + * defined as the delta between the start of the previous non-null segment + * and the split time of this segment. + */ + public void skip() { + if (current > - 1 && current < getRowCount() - 1) { + Segment crtSegment = getSegment(current); + long segmentStart = crtSegment.getStartTime(); + crtSegment.setTime(null, Segment.LIVE); + + current = current + 1; + getSegment(current).setStartTime(segmentStart); + + pcSupport.firePropertyChange( + CURRENT_SEGMENT_PROPERTY, current - 1, current); + } + } + + /** + * Overwrites the registered with the live times, using the following + * algorithm: if a live segment time is better than its registered best + * time, the best time is overwritten. If we are not doing a {@code partial} + * save and the run is complete (its end was naturally reached), the + * registered segment time is overwritten with the live segment time. + * + * @param partial - wether to only save best times or the whole run. + */ + public void saveLiveTimes(boolean partial) { + boolean over = (current == getRowCount()); + for (Segment segment : segments) { + Time live = segment.getTime(Segment.LIVE); + if (live == null) { + if (!partial && over) { + segment.setTime(null, Segment.RUN); + } + } else { + if (live.compareTo(segment.getTime(Segment.BEST)) < 0) { + segment.setTime(live, Segment.BEST); + } + if (!partial && over) { + segment.setTime(live, Segment.RUN); + } + } + } + } + + /** + * Creates a backup copy of this run table of segments in order to be able + * to revert any changes made by the user using direct-editing components. + */ + public void saveBackup() { + stateBackup = state; + segmentsBackup = new ArrayList(); + for (Segment segment : segments) { + segmentsBackup.add(segment.clone()); + } + } + + /** + * Reverts this run table of segments to its original state prior to the + * call to {@link #saveBackup()}. Does nothing if no backup has been + * created. After this call, the existing backup will be discarded. + */ + public void loadBackup() { + if (segmentsBackup != null) { + State old = state; + state = stateBackup; + segments = new ArrayList(); + for (Segment segment : segmentsBackup) { + segments.add(segment); + } + segmentsBackup = null; + tmSupport.fireTableStructureChanged(); + pcSupport.firePropertyChange(STATE_PROPERTY, old, state); + } + } + + /** + * Sets the split time of the given segment. If the new value is + * {@code null} (meaning the time is undefined,) the segment time and best + * time also becomes {@code null} and the previously set segment time is + * added to the next non-null segment time. If it is not, the delta between + * the new and the old value is added to the next non-null segment and the + * new segment time is computed using the previous non-null segment time. + * + * @param index - the index of the segment to set a split time to. + * @param time - the new split time. + */ + public void setSplitTime(int index, Time time) { + Time oldTime = getTime(index, Segment.RUN); + + if (time == null) { + setSegmentTime(index, null); + } else { + // Compute the time added to/removed from the segment. + if (oldTime == null) { + if (index > 0) { + oldTime = getTime(index - 1, Segment.RUN); + } + } + Time delta = Time.getDelta(oldTime, time); + Time pTime = new Time(); + + // Find the first previous non-null segment. + for (int i = index - 1; i >= 0; i--) { + if (getSegment(i).getTime(Segment.RUN) != null) { + pTime = getTime(i, Segment.RUN); + break; + } + } + // If a next non-null segment exist possess a split time + // inferior to the split time we are defining, we add + // the delta to preserve consistency. + for (int i = index + 1; i < getRowCount(); i++) { + Segment nSegment = getSegment(i); + if (nSegment.getTime(Segment.RUN) != null) { + Time nTime = getTime(i, Segment.RUN); + if (time.compareTo(nTime) < 0 + && time.compareTo(pTime) > 0) { + nSegment.getTime(Segment.RUN).add(delta); + } + break; + } + } + time = Time.getDelta(time, pTime); + Segment segment = getSegment(index); + segment.setTime(time, Segment.RUN); + + if (time.compareTo(segment.getTime(Segment.BEST)) < 0) { + segment.setTime(time, Segment.BEST); + } + } + } + + /** + * Registers the given listener as a new {@code PropertyChangeListener} for + * this run. + * + * @param pcl - the listener to register. + */ + public void addPropertyChangeListener(PropertyChangeListener pcl) { + pcSupport.addPropertyChangeListener(pcl); + } + + /** + * Removes the given listener from the list of registered + * {@code PropertyChangeListener} of this run. + * + * @param pcl - the listener to register. + */ + public void removePropertyChangeListener(PropertyChangeListener pcl) { + pcSupport.removePropertyChangeListener(pcl); + } + + // ------------------------------------------------------ INHERITED SETTERS + + /** + * Registers the given listener as a new {@code TableModelListener} for + * this run table of segments. + * + * @param tml - the listener to register. + */ + public void addTableModelListener(TableModelListener tml) { + tmSupport.addTableModelListener(tml); + } + + /** + * Removes the given listener from the list of registered + * {@code TableModelListener} of this run. + * + * @param tml - the listener to register. + */ + public void removeTableModelListener(TableModelListener tml) { + tmSupport.removeTableModelListener(tml); + } + + /** + * As specified by {@code TableModel}. Sets the value of given column for + * the segment of given row index. + */ + public void setValueAt(Object value, int row, int column) { + Segment segment = getSegment(row); + switch (column) { + case COLUMN_ICON: + segment.setIcon((Icon) value); + tmSupport.fireTableCellUpdated(row, column); + break; + + case COLUMN_NAME: + segment.setName((String) value); + tmSupport.fireTableCellUpdated(row, column); + break; + + case COLUMN_TIME: + Time newTime = (Time) value; + setSplitTime(row, newTime); + tmSupport.fireTableDataChanged(); + break; + + case COLUMN_SEGMENT: + setSegmentTime(row, (Time) value); + tmSupport.fireTableDataChanged(); + break; + + case COLUMN_BEST: + newTime = (Time) value; + Time runTime = segment.getTime(Segment.RUN); + if (newTime == null || newTime.compareTo(runTime) > 0) { + segment.setTime(runTime, Segment.BEST); + } else { + segment.setTime(newTime, Segment.BEST); + } + tmSupport.fireTableDataChanged(); + break; + } + } + + // -------------------------------------------------------------- UTILITIES + + /** + * Sets the segment time of the given segment. If the segment time is + * better than the best time, the best time is updated accordingly. + * + * @param index - the index of the segment to set a split time to. + * @param time - the new split time. + */ + private void setSegmentTime(int index, Time time) { + if (time != null && time.compareTo(Time.ZERO) <= 0) { + throw new IllegalArgumentException("" + Language.ILLEGAL_TIME); + } + Segment segment = segments.get(index); + Time best = segment.getTime(Segment.BEST); + Time old = segment.getTime(Segment.RUN); + int rowCount = getRowCount(); + + if (time == null) { + // The old segment time of the newly undefined segment goes + // to the next non-null segment to preserve split times + // consistency. + for (int i = index + 1; i < rowCount; i++) { + Time nTime = getSegment(i).getTime(Segment.RUN); + if (nTime != null) { + nTime.add(old); + break; + } + } + + } else if (old == null) { + for (int i = index + 1; i < rowCount; i++) { + Segment nSeg = getSegment(i); + Time nTime = nSeg.getTime(Segment.RUN); + if (nTime != null) { + if (nTime.compareTo(time) <= 0) { + throw new IllegalArgumentException( + "" + Language.ILLEGAL_SEGMENT_TIME); + } + nSeg.setTime(Time.getDelta(nTime, time), Segment.RUN); + break; + } + } + + } + segment.setTime(time, Segment.RUN); + + if (time != null) { + if (time.compareTo(best) < 0) { + segment.setTime(time, Segment.BEST); + } + } + } + + /** + * Initialize all transient fields. + */ + private void initializeTransients() { + pcSupport = new PropertyChangeSupport(this); + tmSupport = new TableModelSupport(this); + segmentsBackup = null; + stateBackup = null; + state = getRowCount() > 0 ? State.READY : State.NULL; + current = -1; + startTime = 0L; + + if (goal == null) { + goal = ""; + } + if (counters == null) { + counters = new ArrayList(); + } + if ( configuration == null ) { + configuration = new Configuration(); + } + } + + /** + * Deserialization process. Redefined to initialize transients fields upon + * deserialization. + */ + private void readObject(ObjectInputStream in) + throws IOException, ClassNotFoundException { + in.defaultReadObject(); + initializeTransients(); + } + + // ---------------------------------------------------------- INTERNAL TYPE + + /** + * Enumeration of a run possible state. + * + * @author Xavier "Xunkar" Sencert + */ + public enum State { + + // ---------------------------------------------------- ENUMERATES + + /** + * The run cannot start, probably because there are no segments. + */ + NULL, + + /** + * The run is ready to start. This state should be achieved upon + * opening a run and resetting it. + */ + READY, + + /** + * The run is being runned. A current segment now exists. + */ + ONGOING, + + /** + * The run was stopped, either naturally by making the last split or + * voluntarily by the user. + */ + STOPPED, + + /** + * The run was paused by the user. The timer should not be running + * while a run is paused. + */ + PAUSED; + + // ----------------------------------------------------- CONSTANTS + + /** + * The serial version identifier used to determine the compatibility of + * the different serialized versions of this type. This identifier must + * change when modifications that break backward-compatibility are made + * to the type. + */ + private static final long serialVersionUID = 1000L; + } } diff --git a/src/main/java/org/fenix/llanfair/Segment.java b/src/main/java/org/fenix/llanfair/Segment.java index aa9760c..9dff420 100644 --- a/src/main/java/org/fenix/llanfair/Segment.java +++ b/src/main/java/org/fenix/llanfair/Segment.java @@ -17,311 +17,311 @@ import java.io.Serializable; */ public class Segment implements Cloneable, Serializable { - // -------------------------------------------------------------- CONSTANTS + // -------------------------------------------------------------- CONSTANTS - /** - * The serial version identifier used to determine the compatibility of the - * different serialized versions of this type. This identifier must change - * when modifications that break backward-compatibility are made to the - * type. - */ - private static final long serialVersionUID = 1001L; + /** + * The serial version identifier used to determine the compatibility of the + * different serialized versions of this type. This identifier must change + * when modifications that break backward-compatibility are made to the + * type. + */ + private static final long serialVersionUID = 1001L; - /** - * Array of legit display sizes for the segments’ icons. - */ - public static final Integer[] ICON_SIZES = new Integer[] { - 16, 24, 32, 40, 48, 56, 64 - }; + /** + * Array of legit display sizes for the segments’ icons. + */ + public static final Integer[] ICON_SIZES = new Integer[] { + 16, 24, 32, 40, 48, 56, 64 + }; - /** - * Maximum size allowed for the segment’s icons. When setting an icon for - * a segment, it will be scaled down to that size if it’s bigger, but will - * not be scaled up if it’s smaller. - */ - public static final int ICON_MAX_SIZE = ICON_SIZES[ICON_SIZES.length - 1]; + /** + * Maximum size allowed for the segment’s icons. When setting an icon for + * a segment, it will be scaled down to that size if it’s bigger, but will + * not be scaled up if it’s smaller. + */ + public static final int ICON_MAX_SIZE = ICON_SIZES[ICON_SIZES.length - 1]; - /** - * Identifier for the time of the segment as defined by the currently set - * compare method. - */ - public static final int SET = 0; + /** + * Identifier for the time of the segment as defined by the currently set + * compare method. + */ + public static final int SET = 0; - /** - * Identifier for the registered time of the segment. - */ - public static final int RUN = 1; + /** + * Identifier for the registered time of the segment. + */ + public static final int RUN = 1; - /** - * Identifier for the best ever registered time of the segment. - */ - public static final int BEST = 2; + /** + * Identifier for the best ever registered time of the segment. + */ + public static final int BEST = 2; - /** - * Identifier for the live time realized on this segment. - */ - public static final int LIVE = 3; + /** + * Identifier for the live time realized on this segment. + */ + public static final int LIVE = 3; - /** - * Identifier for the delta between the live time and the time as defined - * by the currently set compare method, i.e. {@code LIVE - SET}. - */ - public static final int DELTA = 4; + /** + * Identifier for the delta between the live time and the time as defined + * by the currently set compare method, i.e. {@code LIVE - SET}. + */ + public static final int DELTA = 4; - /** - * Identifier for the delta between the live time and the registered time - * of the segment, i.e. {@code LIVE - RUN}. - */ - public static final int DELTA_RUN = 5; + /** + * Identifier for the delta between the live time and the registered time + * of the segment, i.e. {@code LIVE - RUN}. + */ + public static final int DELTA_RUN = 5; - /** - * Identifier for the delta between the live time and the best ever - * registered time of the segment, i.e. {@code LIVE - BEST}. - */ - public static final int DELTA_BEST = 6; + /** + * Identifier for the delta between the live time and the best ever + * registered time of the segment, i.e. {@code LIVE - BEST}. + */ + public static final int DELTA_BEST = 6; - // ------------------------------------------------------------- ATTRIBUTES + // ------------------------------------------------------------- ATTRIBUTES - /** - * Name of the segment. - */ - private String name; + /** + * Name of the segment. + */ + private String name; - /** - * Icon associated with this segment. Can be {@code null} if no icon is to - * be displayed. - */ - private Icon icon; + /** + * Icon associated with this segment. Can be {@code null} if no icon is to + * be displayed. + */ + private Icon icon; - /** - * Registered time for this segment during the best run. - */ - private Time runTime; + /** + * Registered time for this segment during the best run. + */ + private Time runTime; - /** - * Best time ever registered for this segment. - */ - private Time bestTime; + /** + * Best time ever registered for this segment. + */ + private Time bestTime; - /** - * Live time realized on this segment during a run. This value is never - * saved as is but can overwrite {@code runTime} or {@code bestTime}. - */ - private transient Time liveTime; - - /** - * Number of milliseconds on the clock when the segment started. - */ - private transient long startTime; + /** + * Live time realized on this segment during a run. This value is never + * saved as is but can overwrite {@code runTime} or {@code bestTime}. + */ + private transient Time liveTime; - // ----------------------------------------------------------- CONSTRUCTORS + /** + * Number of milliseconds on the clock when the segment started. + */ + private transient long startTime; - /** - * Creates a new segment of given name and undefined times. - * - * @param name - the name of the segment. - */ - public Segment(String name) { - if (name == null) { - throw new NullPointerException("null segment name"); - } - this.name = name; - icon = null; - runTime = null; - bestTime = null; - initializeTransients(); - } - - /** - * Creates a default segment with a default name and undefined times. - */ - public Segment() { - this("" + Language.UNTITLED); - } + // ----------------------------------------------------------- CONSTRUCTORS - // ---------------------------------------------------------------- GETTERS + /** + * Creates a new segment of given name and undefined times. + * + * @param name - the name of the segment. + */ + public Segment(String name) { + if (name == null) { + throw new NullPointerException("null segment name"); + } + this.name = name; + icon = null; + runTime = null; + bestTime = null; + initializeTransients(); + } - /** - * Returns the name of the segment. - * - * @return the name of the segment. - */ - public String getName() { - return name; - } - - /** - * Returns the icon associated with this segment. Can be {@code null}. - * - * @return the icon of the segment or {@code null}. - */ - public Icon getIcon() { - return icon; - } - - /** - * Returns the number of milliseconds on the clock when the segment started. - * - * @return the start time of this segment. - */ - public long getStartTime() { - return startTime; - } - - /** - * Returns the given type of time for this segment. - * - * @param type - one of the type identifier. - * @return the segment time of given type. - */ - public Time getTime(int type) { - switch (type) { - case BEST: - return bestTime; - - case LIVE: - return liveTime; - - case RUN: - return runTime; - - case DELTA_RUN: - if (runTime == null) { - return null; - } - return Time.getDelta(liveTime, runTime); - - case DELTA_BEST: - if (bestTime == null) { - return null; - } - return Time.getDelta(liveTime, bestTime); - - case DELTA: - Time time = getTime(); - return (time == null ? null : Time.getDelta(liveTime, time)); - - default: - return getTime(); - } - } - - /** - * As specified by {@code Cloneable}, returns a deep copy of the segment. - */ - public Segment clone() { - Segment segment = new Segment(name); - segment.icon = icon; - segment.runTime = (runTime == null ? null : runTime.clone()); - segment.bestTime = (bestTime == null ? null : bestTime.clone()); - segment.liveTime = (liveTime == null ? null : liveTime.clone()); - segment.startTime = startTime; - return segment; - } - - // ---------------------------------------------------------------- SETTERS - - /** - * Sets the name of the segment to the given string. - * - * @param name - the new name of the segment. - */ - public void setName(String name) { - if (name == null) { - throw new NullPointerException("null name"); - } - this.name = name; - } - - /** - * Sets the current icon of this segment. Can be {@code null} to remove - * the current icon or indicate that no icon should be used. The icon wil - * be scale down to {@code ICON_MAX_SIZE} if it’s bigger. - * - * @param icon - the new icon for this segment. - */ - public void setIcon(Icon icon) { - if (icon == null) { - this.icon = null; - } else { - this.icon = Images.rescale(icon, ICON_MAX_SIZE); - } - } - - /** - * Sets the number of milliseconds on the clock when the segment started. - * Should only be called by the run owning this segment. - * - * @param startTime - the starting time of this segment. - * @throws IllegalArgumentException if the time is negative. - */ - void setStartTime(long startTime) { - if (startTime < 0L) { - throw new IllegalArgumentException("negative start time"); - } - this.startTime = startTime; - } - - /** - * Sets the given type of time to the new value. Note that some type of - * times cannot be set (such as {@code DELTA}s.) The new value can be - * {@code null} to indicate undefined times. - * - * @param time - the new time value for the given type. - * @param type - one of the type identifier. - * @throws IllegalArgumentException if the new time value is lower than or - * equal to zero. - */ - public void setTime(Time time, int type, boolean bypass) { - if (!bypass) { - if (time != null && time.compareTo(Time.ZERO) <= 0) { - throw new IllegalArgumentException("" + Language.ILLEGAL_TIME); - } - } - switch (type) { - case BEST: bestTime = time; break; - case LIVE: liveTime = time; break; - case RUN: runTime = time; break; - } - } - - public void setTime(Time time, int type) { - setTime(time, type, false); - } + /** + * Creates a default segment with a default name and undefined times. + */ + public Segment() { + this("" + Language.UNTITLED); + } + + // ---------------------------------------------------------------- GETTERS + + /** + * Returns the name of the segment. + * + * @return the name of the segment. + */ + public String getName() { + return name; + } + + /** + * Returns the icon associated with this segment. Can be {@code null}. + * + * @return the icon of the segment or {@code null}. + */ + public Icon getIcon() { + return icon; + } + + /** + * Returns the number of milliseconds on the clock when the segment started. + * + * @return the start time of this segment. + */ + public long getStartTime() { + return startTime; + } + + /** + * Returns the given type of time for this segment. + * + * @param type - one of the type identifier. + * @return the segment time of given type. + */ + public Time getTime(int type) { + switch (type) { + case BEST: + return bestTime; + + case LIVE: + return liveTime; + + case RUN: + return runTime; + + case DELTA_RUN: + if (runTime == null) { + return null; + } + return Time.getDelta(liveTime, runTime); + + case DELTA_BEST: + if (bestTime == null) { + return null; + } + return Time.getDelta(liveTime, bestTime); + + case DELTA: + Time time = getTime(); + return (time == null ? null : Time.getDelta(liveTime, time)); + + default: + return getTime(); + } + } + + /** + * As specified by {@code Cloneable}, returns a deep copy of the segment. + */ + public Segment clone() { + Segment segment = new Segment(name); + segment.icon = icon; + segment.runTime = (runTime == null ? null : runTime.clone()); + segment.bestTime = (bestTime == null ? null : bestTime.clone()); + segment.liveTime = (liveTime == null ? null : liveTime.clone()); + segment.startTime = startTime; + return segment; + } + + // ---------------------------------------------------------------- SETTERS + + /** + * Sets the name of the segment to the given string. + * + * @param name - the new name of the segment. + */ + public void setName(String name) { + if (name == null) { + throw new NullPointerException("null name"); + } + this.name = name; + } + + /** + * Sets the current icon of this segment. Can be {@code null} to remove + * the current icon or indicate that no icon should be used. The icon wil + * be scale down to {@code ICON_MAX_SIZE} if it’s bigger. + * + * @param icon - the new icon for this segment. + */ + public void setIcon(Icon icon) { + if (icon == null) { + this.icon = null; + } else { + this.icon = Images.rescale(icon, ICON_MAX_SIZE); + } + } + + /** + * Sets the number of milliseconds on the clock when the segment started. + * Should only be called by the run owning this segment. + * + * @param startTime - the starting time of this segment. + * @throws IllegalArgumentException if the time is negative. + */ + void setStartTime(long startTime) { + if (startTime < 0L) { + throw new IllegalArgumentException("negative start time"); + } + this.startTime = startTime; + } + + /** + * Sets the given type of time to the new value. Note that some type of + * times cannot be set (such as {@code DELTA}s.) The new value can be + * {@code null} to indicate undefined times. + * + * @param time - the new time value for the given type. + * @param type - one of the type identifier. + * @throws IllegalArgumentException if the new time value is lower than or + * equal to zero. + */ + public void setTime(Time time, int type, boolean bypass) { + if (!bypass) { + if (time != null && time.compareTo(Time.ZERO) <= 0) { + throw new IllegalArgumentException("" + Language.ILLEGAL_TIME); + } + } + switch (type) { + case BEST: bestTime = time; break; + case LIVE: liveTime = time; break; + case RUN: runTime = time; break; + } + } + + public void setTime(Time time, int type) { + setTime(time, type, false); + } + + // -------------------------------------------------------------- UTILITIES + + /** + * Initialize all transient fields. + */ + private void initializeTransients() { + liveTime = null; + startTime = 0L; + } + + /** + * Returns the time of this segment as specified by the currently set + * compare method. + * + * @return the time as defined by the current compare method. + */ + private Time getTime() { + switch (Settings.GNR_COMP.get()) { + case BEST_OVERALL_RUN: return runTime; + case SUM_OF_BEST_SEGMENTS: return bestTime; + } + // Should not be reached. + return null; + } - // -------------------------------------------------------------- UTILITIES - - /** - * Initialize all transient fields. - */ - private void initializeTransients() { - liveTime = null; - startTime = 0L; - } - - /** - * Returns the time of this segment as specified by the currently set - * compare method. - * - * @return the time as defined by the current compare method. - */ - private Time getTime() { - switch (Settings.GNR_COMP.get()) { - case BEST_OVERALL_RUN: return runTime; - case SUM_OF_BEST_SEGMENTS: return bestTime; - } - // Should not be reached. - return null; - } - /** - * Deserialization process. Redefined to initialize transients fields upon - * deserialization. - */ + * Deserialization process. Redefined to initialize transients fields upon + * deserialization. + */ private void readObject(ObjectInputStream in) - throws IOException, ClassNotFoundException { - in.defaultReadObject(); - initializeTransients(); + throws IOException, ClassNotFoundException { + in.defaultReadObject(); + initializeTransients(); } } diff --git a/src/main/java/org/fenix/llanfair/Time.java b/src/main/java/org/fenix/llanfair/Time.java index 20f6c9a..261a715 100644 --- a/src/main/java/org/fenix/llanfair/Time.java +++ b/src/main/java/org/fenix/llanfair/Time.java @@ -23,276 +23,276 @@ import org.fenix.llanfair.config.Settings; * @version 1.1 */ public class Time implements Cloneable, Comparable