/**
* ThisTime 2.0 (2008-03-30)
* Copyright 2007 Zach Scrivena
* zachscrivena@gmail.com
* http://thistime.sourceforge.net/
*
* Simple clock and timer program.
*
* TERMS AND CONDITIONS:
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package thistime;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.image.BufferedImage;
import javax.imageio.ImageIO;
import javax.swing.JComponent;
/**
* Timer display.
*/
class TimerDisplay
extends JComponent
{
/** unscaled images for digits, colon, and negative-sign (first 10 elements are digits 0-9) */
private static final BufferedImage[] UNSCALED_IMAGES = new BufferedImage[12];
/** array index of the colon image in UNSCALED_IMAGES[] */
private static final int COLON_INDEX = 10;
/** array index of the negative-sign image in UNSCALED_IMAGES[] */
private static final int NEGATIVE_SIGN_INDEX = 11;
/** width of an unscaled digit image */
private static int UNSCALED_DIGIT_WIDTH;
/** height of an unscaled digit image */
private static int UNSCALED_DIGIT_HEIGHT;
/** width of the unscaled colon image */
private static int UNSCALED_COLON_WIDTH;
/** scaling ratio for a small digit (should be less than 1) */
private static final double SMALL_DIGIT_RATIO = 0.6;
/** horizontal and vertical padding between digits */
private static final int PADDING = 20;
/** "set" mode box color */
private static final Color SET_MODE_BOX_COLOR = new Color(255, 0, 0);
/** scaled images for digits, colon, and negative-sign (first 10 elements are digits 0-9) */
private final Image[] scaledImages = new Image[12];
/** scaled images for small digits */
private final Image[] smallScaledImages = new Image[10];
/** hash code for the unscaled total width and height corresponding to scaledImages[] and smallScaledImages[] */
private int unscaledSizeHashCode = -1;
/** hash code for the scaled total width and height corresponding to scaledImages[] and smallScaledImages[] */
private int scaledSizeHashCode = -1;
/** is the timer display in "set" mode? */
private volatile boolean setMode;
/** "normalized" timer value to be displayed */
private final TimerValue value;
/**
* Static initialization block for loading image resources.
* The following assumptions are made:
* 1. All digit images and the negative sign image have the same dimensions.
* 2. Digits and the colon images have the same height.
*/
static
{
try
{
for (int i = 0; i <= 9; i++)
{
UNSCALED_IMAGES[i] = ImageIO.read(TimerDisplay.class.getResource("/thistime/resources/digit" + i + ".png"));
}
UNSCALED_IMAGES[COLON_INDEX] = ImageIO.read(TimerDisplay.class.getResource("/thistime/resources/colon.png"));
UNSCALED_IMAGES[NEGATIVE_SIGN_INDEX] = ImageIO.read(TimerDisplay.class.getResource("/thistime/resources/neg.png"));
UNSCALED_DIGIT_WIDTH = UNSCALED_IMAGES[0].getWidth();
UNSCALED_DIGIT_HEIGHT = UNSCALED_IMAGES[0].getHeight();
UNSCALED_COLON_WIDTH = UNSCALED_IMAGES[COLON_INDEX].getWidth();
}
catch (Exception e)
{
throw new RuntimeException(e.getMessage(), e.getCause());
}
}
/**
* Constructor.
*
* @param value
* "normalized" timer value to be displayed
*/
TimerDisplay(
final TimerValue value)
{
this.value = value;
/* add mouse listener for "set" mode */
this.addMouseListener(new MouseAdapter()
{
@Override
public void mouseClicked(
MouseEvent e)
{
if (TimerDisplay.this.setMode)
{
synchronized (TimerDisplay.this.value)
{
doAction(Action.MOUSE_CLICKED, e);
}
}
}
});
/* add mouse wheel listener for "set" mode */
this.addMouseWheelListener(new MouseAdapter()
{
@Override
public void mouseWheelMoved(
MouseWheelEvent e)
{
if (TimerDisplay.this.setMode)
{
synchronized (TimerDisplay.this.value)
{
doAction(Action.MOUSE_WHEEL_MOVED, e);
}
}
}
});
}
/**
* Set the "set" mode of this timer display.
*
* @param setMode
* true if timer display is to be in "set" mode; false otherwise
*/
void setSetMode(
final boolean setMode)
{
this.setMode = setMode;
}
/**
* Paint the timer display. This method is invoked by Swing to draw components.
*
* @param g
* the Graphics context in which to paint
*/
@Override
public void paint(
Graphics g)
{
synchronized (value)
{
doAction(Action.PAINT, g);
}
}
/**
* Perform the specified action on the display.
* Assumes that this.value has been externally synchronized.
* This method must run on the EDT.
*
* @param action
* action to be performed
* @param arg
* argument value for the action
*/
private void doAction(
final Action action,
final Object arg)
{
/************************************************
* (1) PARSE AGRUMENTS FOR THE SPECIFIED ACTION *
************************************************/
Graphics g = null;
int mouseX = 0;
int mouseY = 0;
int mouseScroll = 0;
switch (action)
{
case PAINT:
g = (Graphics) arg;
break;
case MOUSE_CLICKED:
final MouseEvent me = (MouseEvent) arg;
mouseX = me.getX();
mouseY = me.getY();
break;
case MOUSE_WHEEL_MOVED:
final MouseWheelEvent mwe = (MouseWheelEvent) arg;
mouseX = mwe.getX();
mouseY = mwe.getY();
mouseScroll = (mwe.getWheelRotation() < 0) ? 1 : -1;
break;
}
/******************************************************
* (2) COMPUTE DIMENSIONS OF TIMER DISPLAY COMPONENTS *
******************************************************/
/* length of the hour-value */
final int hourValueLength = String.valueOf(value.h).length();
/* width and height of the canvas */
final Rectangle bounds = getBounds();
final int canvasWidth = (int) bounds.getWidth();
final int canvasHeight = (int) bounds.getHeight();
/* total width and height of timer display, in unscaled units */
final int unscaledTotalWidth = UNSCALED_COLON_WIDTH +
(int) ((SMALL_DIGIT_RATIO * 2 + hourValueLength + 2 + (value.negative ? 1 : 0)) * UNSCALED_DIGIT_WIDTH) +
(6 + hourValueLength + (value.negative ? 1 : 0)) * PADDING;
final int unscaledTotalHeight = UNSCALED_DIGIT_HEIGHT + 2 * PADDING;
/* compute scaled widths and heights */
final Dimension scaledDimension = GraphicsManipulator.scaleToFitKeepRatio(
unscaledTotalWidth, unscaledTotalHeight,
canvasWidth, canvasHeight);
final int scaledTotalWidth = (int) scaledDimension.getWidth();
final int scaledTotalHeight = (int) scaledDimension.getHeight();
final int digitWidth = scaledTotalWidth * UNSCALED_DIGIT_WIDTH / unscaledTotalWidth;
final int digitHeight = scaledTotalHeight * UNSCALED_DIGIT_HEIGHT / unscaledTotalHeight;
final int colonWidth = scaledTotalWidth * UNSCALED_COLON_WIDTH / unscaledTotalWidth;
final int smallDigitWidth = (int) (SMALL_DIGIT_RATIO * digitWidth);
final int smallDigitHeight = (int) (SMALL_DIGIT_RATIO * digitHeight);
final int padding = scaledTotalWidth * PADDING / unscaledTotalWidth;
/* initial horizontal and vertical offsets */
int offsetX = padding + (canvasWidth - scaledTotalWidth) / 2;
int offsetY = padding + (canvasHeight - scaledTotalHeight) / 2;
/***************************************************
* (3) CHECK IF CACHED SCALED IMAGES CAN BE REUSED *
***************************************************/
/* compute new hash codes */
final int newUnscaledSizeHashCode = unscaledTotalWidth * unscaledTotalHeight;
final int newScaledSizeHashCode = scaledTotalWidth * scaledTotalHeight;
if ((newUnscaledSizeHashCode != unscaledSizeHashCode) ||
(newScaledSizeHashCode != scaledSizeHashCode))
{
/* proceed to generate new scaled images */
for (int i = 0; i <= 9; i++)
{
scaledImages[i] = ((digitWidth == 0) || (digitHeight == 0)) ?
UNSCALED_IMAGES[i] :
UNSCALED_IMAGES[i].getScaledInstance(digitWidth, digitHeight, Image.SCALE_SMOOTH);
smallScaledImages[i] = ((smallDigitWidth == 0) || (smallDigitHeight == 0)) ?
UNSCALED_IMAGES[i] :
UNSCALED_IMAGES[i].getScaledInstance(smallDigitWidth, smallDigitHeight, Image.SCALE_SMOOTH);
}
scaledImages[COLON_INDEX] = ((colonWidth == 0) || (digitHeight == 0)) ?
UNSCALED_IMAGES[COLON_INDEX] :
UNSCALED_IMAGES[COLON_INDEX].getScaledInstance(colonWidth, digitHeight, Image.SCALE_SMOOTH);
scaledImages[NEGATIVE_SIGN_INDEX] = ((digitWidth == 0) || (digitHeight == 0)) ?
UNSCALED_IMAGES[NEGATIVE_SIGN_INDEX] :
UNSCALED_IMAGES[NEGATIVE_SIGN_INDEX].getScaledInstance(digitWidth, digitHeight, Image.SCALE_SMOOTH);
/* update hash codes */
unscaledSizeHashCode = newUnscaledSizeHashCode;
scaledSizeHashCode = newScaledSizeHashCode;
}
/***********************
* (4) DISPLAY "HOURS" *
***********************/
if (setMode)
{
/* size of the "set" mode box around all the hour digits and negative-sign, if any */
final int boxWidth = (digitWidth + padding) * (hourValueLength + (value.negative ? 1 : 0)) - padding;
final int boxHeight = digitHeight;
switch (action)
{
case PAINT:
/* set graphic color for rectangle */
g.setColor(SET_MODE_BOX_COLOR);
/* draw the set box */
g.drawRect(
offsetX,
offsetY,
boxWidth,
boxHeight);
/* draw dotted line through the middle of the box */
for (int i = 2; i < boxWidth; i += 5)
{
g.drawLine(
offsetX + i - 2,
offsetY + boxHeight / 2,
offsetX + i,
offsetY + boxHeight / 2);
}
break;
case MOUSE_CLICKED:
case MOUSE_WHEEL_MOVED:
/* check if mouse event occured inside the "set" mode box */
if ((mouseX >= offsetX) &&
(mouseX <= (offsetX + boxWidth)) &&
(mouseY >= offsetY) &&
(mouseY <= (offsetY + boxHeight)))
{
boolean increment;
if (action == Action.MOUSE_CLICKED)
{
increment = (mouseY <= (offsetY + boxHeight / 2));
}
else
{
increment = (mouseScroll > 0);
}
if (increment)
{
/* increment hour-value */
if (value.negative)
{
if (value.h == 0)
{
value.negative = false;
}
else
{
value.h--;
}
}
else
{
value.h++;
}
}
else
{
/* decrement hour-value */
if (value.negative)
{
value.h++;
}
else
{
if (value.h == 0)
{
if ((value.m == 0) && (value.s == 0))
{
value.h = 1;
}
value.negative = true;
}
else
{
value.h--;
}
}
}
if ((value.h == 0) &&
(value.m == 0) &&
(value.s == 0))
{
value.negative = false;
}
repaint();
return;
}
break;
}
}
/* draw the negative sign, if necessary */
if (value.negative)
{
if (action == Action.PAINT)
{
g.drawImage(scaledImages[TimerDisplay.NEGATIVE_SIGN_INDEX], offsetX, offsetY, null);
}
offsetX += (digitWidth + padding);
}
/* draw the digits in "hours", starting with the most significant digit */
long hourValueLeft = value.h;
for (int i = (hourValueLength - 1); i >= 0; i--)
{
final int divisor = (int) Math.pow(10, i);
final int digit = (int) (hourValueLeft / divisor);
hourValueLeft -= (digit * divisor);
if (action == Action.PAINT)
{
g.drawImage(scaledImages[digit], offsetX, offsetY, null);
}
offsetX += (digitWidth + padding);
}
/* draw the colon */
if (action == Action.PAINT)
{
g.drawImage(scaledImages[COLON_INDEX], offsetX, offsetY, null);
}
offsetX += (colonWidth + padding);
/*************************
* (5) DISPLAY "MINUTES" *
*************************/
/* draw the digits in "minutes", starting with the most significant digit */
int minuteValueLeft = value.m;
for (int i = 1; i >= 0; i--)
{
final int divisor = (int) Math.pow(10, i);
final int digit = minuteValueLeft / divisor;
minuteValueLeft -= (digit * divisor);
if (action == Action.PAINT)
{
g.drawImage(scaledImages[digit], offsetX, offsetY, null);
}
if (setMode)
{
switch (action)
{
case PAINT:
/* draw the "set" mode box */
g.drawRect(
offsetX,
offsetY,
digitWidth,
digitHeight);
/* draw dotted line through the middle of the box */
for (int j = 2; j < digitWidth; j += 5)
{
g.drawLine(
offsetX + j - 2,
offsetY + digitHeight / 2,
offsetX + j,
offsetY + digitHeight / 2);
}
break;
case MOUSE_CLICKED:
case MOUSE_WHEEL_MOVED:
/* check if mouse event occured inside the "set" mode box */
if ((mouseX >= offsetX) &&
(mouseX <= (offsetX + digitWidth)) &&
(mouseY >= offsetY) &&
(mouseY <= (offsetY + digitHeight)))
{
boolean increment;
if (action == Action.MOUSE_CLICKED)
{
increment = (mouseY <= (offsetY + digitHeight / 2));
}
else
{
increment = (mouseScroll > 0);
}
if (increment)
{
/* increment minute-value */
if ((value.m + divisor) <= 59)
{
value.m += divisor;
}
}
else
{
/* decrement minute-value */
if ((value.m - divisor) >= 0)
{
value.m -= divisor;
}
}
if ((value.h == 0) &&
(value.m == 0) &&
(value.s == 0))
{
value.negative = false;
}
repaint();
return;
}
break;
}
}
offsetX += (digitWidth + padding);
}
/*************************
* (6) DISPLAY "SECONDS" *
*************************/
/* draw the digits in "seconds", starting with the most significant digit */
int secondValueLeft = value.s;
for (int i = 1; i >= 0; i--)
{
final int divisor = (int) Math.pow(10, i);
final int digit = secondValueLeft / divisor;
secondValueLeft -= (digit * divisor);
if (action == Action.PAINT)
{
g.drawImage(smallScaledImages[digit], offsetX, offsetY + digitHeight - smallDigitHeight, null);
}
if (setMode)
{
switch (action)
{
case PAINT:
/* draw the "set" mode box */
g.drawRect(
offsetX,
offsetY + digitHeight - smallDigitHeight,
smallDigitWidth,
smallDigitHeight);
/* draw dotted line through the middle of the box */
for (int j = 2; j < smallDigitWidth; j += 5)
{
g.drawLine(
offsetX + j - 2,
offsetY + digitHeight - smallDigitHeight / 2,
offsetX + j,
offsetY + digitHeight - smallDigitHeight / 2);
}
break;
case MOUSE_CLICKED:
case MOUSE_WHEEL_MOVED:
/* check if mouse event occured inside the "set" mode box */
if ((mouseX >= offsetX) &&
(mouseX <= (offsetX + smallDigitWidth)) &&
(mouseY >= (offsetY + digitHeight - smallDigitHeight)) &&
(mouseY <= (offsetY + digitHeight)))
{
boolean increment;
if (action == Action.MOUSE_CLICKED)
{
increment = (mouseY <= (offsetY + digitHeight - smallDigitHeight / 2));
}
else
{
increment = (mouseScroll > 0);
}
if (increment)
{
/* increment second-value */
if ((value.s + divisor) <= 59)
{
value.s += divisor;
}
}
else
{
/* decrement second-value */
if ((value.s - divisor) >= 0)
{
value.s -= divisor;
}
}
if ((value.h == 0) &&
(value.m == 0) &&
(value.s == 0))
{
value.negative = false;
}
repaint();
return;
}
break;
}
}
offsetX += (smallDigitWidth + padding);
}
}
/*****************
* INNER CLASSES *
*****************/
/**
* Types of action for the doAction() method.
*/
private static enum Action
{
PAINT,
MOUSE_CLICKED,
MOUSE_WHEEL_MOVED
};
}