/*
* Copyright (c) 1998-2017 by Richard A. Wilkes. All rights reserved.
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, version 2.0. If a copy of the MPL was not distributed with
* this file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* This Source Code Form is "Incompatible With Secondary Licenses", as
* defined by the Mozilla Public License, version 2.0.
*/
package com.trollworks.gcs.page;
import com.trollworks.gcs.app.GCSFonts;
import com.trollworks.gcs.character.Armor;
import com.trollworks.gcs.character.CharacterSheet;
import com.trollworks.gcs.character.Encumbrance;
import com.trollworks.gcs.character.GURPSCharacter;
import com.trollworks.gcs.character.Profile;
import com.trollworks.toolkit.ui.GraphicsUtilities;
import com.trollworks.toolkit.ui.widget.Commitable;
import com.trollworks.toolkit.utility.Platform;
import com.trollworks.toolkit.utility.notification.NotifierTarget;
import com.trollworks.toolkit.utility.text.DateFormatter;
import com.trollworks.toolkit.utility.text.DiceFormatter;
import com.trollworks.toolkit.utility.text.DoubleFormatter;
import com.trollworks.toolkit.utility.text.HeightFormatter;
import com.trollworks.toolkit.utility.text.IntegerFormatter;
import com.trollworks.toolkit.utility.text.Numbers;
import com.trollworks.toolkit.utility.text.Text;
import com.trollworks.toolkit.utility.text.WeightFormatter;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.text.DateFormat;
import java.text.MessageFormat;
import java.text.ParseException;
import java.util.HashMap;
import javax.swing.JFormattedTextField;
import javax.swing.SwingConstants;
import javax.swing.UIManager;
import javax.swing.plaf.basic.BasicTextFieldUI;
import javax.swing.text.DefaultFormatter;
import javax.swing.text.DefaultFormatterFactory;
/** A generic field for a page. */
public class PageField extends JFormattedTextField implements NotifierTarget, PropertyChangeListener, ActionListener, Commitable {
private CharacterSheet mSheet;
private String mConsumedType;
private String mCustomToolTip;
/**
* Creates a new, left-aligned, text input field.
*
* @param sheet The sheet to listen to.
* @param consumedType The field to listen to.
*/
public PageField(CharacterSheet sheet, String consumedType) {
this(sheet, consumedType, SwingConstants.LEFT, true, null);
}
/**
* Creates a new, left-aligned, text input field.
*
* @param sheet The sheet to listen to.
* @param consumedType The field to listen to.
* @param tooltip The tooltip to set.
*/
public PageField(CharacterSheet sheet, String consumedType, String tooltip) {
this(sheet, consumedType, SwingConstants.LEFT, true, tooltip);
}
/**
* Creates a new text input field.
*
* @param sheet The sheet to listen to.
* @param consumedType The field to listen to.
* @param alignment The alignment of the field.
* @param tooltip The tooltip to set.
*/
public PageField(CharacterSheet sheet, String consumedType, int alignment, String tooltip) {
this(sheet, consumedType, alignment, true, tooltip);
}
/**
* Creates a new text input field.
*
* @param sheet The sheet to listen to.
* @param consumedType The field to listen to.
* @param alignment The alignment of the field.
* @param editable Whether or not the user can edit this field.
* @param tooltip The tooltip to set.
*/
public PageField(CharacterSheet sheet, String consumedType, int alignment, boolean editable, String tooltip) {
super(getFormatterFactoryForType(consumedType), sheet.getCharacter().getValueForID(consumedType));
if (Platform.isLinux()) {
// I override the UI here since the GTK UI on Linux has no way to turn off the border
// around text fields.
setUI(new BasicTextFieldUI());
}
mSheet = sheet;
mConsumedType = consumedType;
setFont(sheet.getScale().scale(UIManager.getFont(GCSFonts.KEY_FIELD)));
setBorder(null);
setOpaque(false);
// Just setting opaque to false isn't enough for some reason, so I'm also setting the
// background color to a 100% transparent value.
setBackground(new Color(255, 255, 255, 0));
setHorizontalAlignment(alignment);
setEditable(editable);
setEnabled(editable);
setForeground(editable ? Color.BLACK : Color.GRAY);
if (tooltip != null) {
setToolTipText(Text.wrapPlainTextForToolTip(tooltip));
if (tooltip.indexOf("{") != -1) { //$NON-NLS-1$
mCustomToolTip = tooltip;
}
}
mSheet.getCharacter().addTarget(this, mConsumedType);
addPropertyChangeListener("value", this); //$NON-NLS-1$
addActionListener(this);
setFocusLostBehavior(COMMIT_OR_REVERT);
// Reset the selection colors back to what is standard for text fields.
// This is necessary, since (at least on the Mac) JFormattedTextField
// has the wrong values by default.
setCaretColor(UIManager.getColor("TextField.caretForeground")); //$NON-NLS-1$
setSelectionColor(UIManager.getColor("TextField.selectionBackground")); //$NON-NLS-1$
setSelectedTextColor(UIManager.getColor("TextField.selectionForeground")); //$NON-NLS-1$
setDisabledTextColor(UIManager.getColor("TextField.inactiveForeground")); //$NON-NLS-1$
}
@Override
public Dimension getPreferredSize() {
Dimension size = super.getPreferredSize();
// Don't know why this is needed, but it seems to be. Without it, text is being truncated by
// about 2 pixels.
size.width += mSheet.getScale().scale(2);
return size;
}
@Override
public String getToolTipText() {
if (mCustomToolTip != null) {
return Text.wrapPlainTextForToolTip(MessageFormat.format(mCustomToolTip, Numbers.format(((Integer) mSheet.getCharacter().getValueForID(GURPSCharacter.POINTS_PREFIX + mConsumedType)).intValue())));
}
return super.getToolTipText();
}
@Override
public void handleNotification(Object producer, String name, Object data) {
setValue(data);
invalidate();
repaint();
}
@Override
protected void processFocusEvent(FocusEvent event) {
super.processFocusEvent(event);
if (event.getID() == FocusEvent.FOCUS_GAINED) {
selectAll();
}
}
@Override
protected void paintComponent(Graphics gc) {
if (isEditable()) {
Rectangle bounds = getBounds();
bounds.x = 0;
bounds.y = 0;
gc.setColor(Color.lightGray);
int height = mSheet.getScale().scale(1);
gc.fillRect(bounds.x, bounds.y + bounds.height - height, bounds.width, height);
gc.setColor(getForeground());
}
super.paintComponent(GraphicsUtilities.prepare(gc));
}
/** @return The consumed type. */
public String getConsumedType() {
return mConsumedType;
}
@Override
public void propertyChange(PropertyChangeEvent event) {
if (isEditable()) {
mSheet.getCharacter().setValueForID(mConsumedType, getValue());
}
}
private static final HashMap<String, AbstractFormatterFactory> FACTORY_MAP = new HashMap<>();
private static final AbstractFormatterFactory DEFAULT_FACTORY;
static {
DefaultFormatterFactory factory = new DefaultFormatterFactory(new WeightFormatter(true));
FACTORY_MAP.put(Profile.ID_WEIGHT, factory);
FACTORY_MAP.put(GURPSCharacter.ID_BASIC_LIFT, factory);
FACTORY_MAP.put(GURPSCharacter.ID_ONE_HANDED_LIFT, factory);
FACTORY_MAP.put(GURPSCharacter.ID_TWO_HANDED_LIFT, factory);
FACTORY_MAP.put(GURPSCharacter.ID_SHOVE_AND_KNOCK_OVER, factory);
FACTORY_MAP.put(GURPSCharacter.ID_RUNNING_SHOVE_AND_KNOCK_OVER, factory);
FACTORY_MAP.put(GURPSCharacter.ID_CARRY_ON_BACK, factory);
FACTORY_MAP.put(GURPSCharacter.ID_SHIFT_SLIGHTLY, factory);
for (Encumbrance encumbrance : Encumbrance.values()) {
FACTORY_MAP.put(GURPSCharacter.MAXIMUM_CARRY_PREFIX + encumbrance.ordinal(), factory);
}
factory = new DefaultFormatterFactory(new IntegerFormatter(0, 9999, false));
FACTORY_MAP.put(GURPSCharacter.ID_STRENGTH, factory);
FACTORY_MAP.put(GURPSCharacter.ID_DEXTERITY, factory);
FACTORY_MAP.put(GURPSCharacter.ID_INTELLIGENCE, factory);
FACTORY_MAP.put(GURPSCharacter.ID_HEALTH, factory);
FACTORY_MAP.put(GURPSCharacter.ID_WILL, factory);
FACTORY_MAP.put(GURPSCharacter.ID_FRIGHT_CHECK, factory);
FACTORY_MAP.put(GURPSCharacter.ID_BASIC_MOVE, factory);
FACTORY_MAP.put(GURPSCharacter.ID_PERCEPTION, factory);
FACTORY_MAP.put(GURPSCharacter.ID_VISION, factory);
FACTORY_MAP.put(GURPSCharacter.ID_HEARING, factory);
FACTORY_MAP.put(GURPSCharacter.ID_TASTE_AND_SMELL, factory);
FACTORY_MAP.put(GURPSCharacter.ID_TOUCH, factory);
FACTORY_MAP.put(Armor.ID_EYES_DR, factory);
FACTORY_MAP.put(Armor.ID_SKULL_DR, factory);
FACTORY_MAP.put(Armor.ID_FACE_DR, factory);
FACTORY_MAP.put(Armor.ID_LEG_DR, factory);
FACTORY_MAP.put(Armor.ID_ARM_DR, factory);
FACTORY_MAP.put(Armor.ID_TORSO_DR, factory);
FACTORY_MAP.put(Armor.ID_GROIN_DR, factory);
FACTORY_MAP.put(Armor.ID_HAND_DR, factory);
FACTORY_MAP.put(Armor.ID_FOOT_DR, factory);
FACTORY_MAP.put(Armor.ID_NECK_DR, factory);
for (Encumbrance encumbrance : Encumbrance.values()) {
int index = encumbrance.ordinal();
FACTORY_MAP.put(GURPSCharacter.MOVE_PREFIX + index, factory);
FACTORY_MAP.put(GURPSCharacter.DODGE_PREFIX + index, factory);
}
factory = new DefaultFormatterFactory(new IntegerFormatter(-99999, 99999, false));
FACTORY_MAP.put(GURPSCharacter.ID_ATTRIBUTE_POINTS, factory);
FACTORY_MAP.put(GURPSCharacter.ID_ADVANTAGE_POINTS, factory);
FACTORY_MAP.put(GURPSCharacter.ID_DISADVANTAGE_POINTS, factory);
FACTORY_MAP.put(GURPSCharacter.ID_QUIRK_POINTS, factory);
FACTORY_MAP.put(GURPSCharacter.ID_SKILL_POINTS, factory);
FACTORY_MAP.put(GURPSCharacter.ID_SPELL_POINTS, factory);
FACTORY_MAP.put(GURPSCharacter.ID_RACE_POINTS, factory);
FACTORY_MAP.put(GURPSCharacter.ID_EARNED_POINTS, factory);
FACTORY_MAP.put(GURPSCharacter.ID_TIRED_FATIGUE_POINTS, factory);
FACTORY_MAP.put(GURPSCharacter.ID_UNCONSCIOUS_CHECKS_FATIGUE_POINTS, factory);
FACTORY_MAP.put(GURPSCharacter.ID_UNCONSCIOUS_FATIGUE_POINTS, factory);
FACTORY_MAP.put(GURPSCharacter.ID_REELING_HIT_POINTS, factory);
FACTORY_MAP.put(GURPSCharacter.ID_UNCONSCIOUS_CHECKS_HIT_POINTS, factory);
FACTORY_MAP.put(GURPSCharacter.ID_DEATH_CHECK_1_HIT_POINTS, factory);
FACTORY_MAP.put(GURPSCharacter.ID_DEATH_CHECK_2_HIT_POINTS, factory);
FACTORY_MAP.put(GURPSCharacter.ID_DEATH_CHECK_3_HIT_POINTS, factory);
FACTORY_MAP.put(GURPSCharacter.ID_DEATH_CHECK_4_HIT_POINTS, factory);
FACTORY_MAP.put(GURPSCharacter.ID_DEAD_HIT_POINTS, factory);
factory = new DefaultFormatterFactory(new IntegerFormatter(0, 999999, false));
FACTORY_MAP.put(GURPSCharacter.ID_FATIGUE_POINTS, factory);
FACTORY_MAP.put(GURPSCharacter.ID_HIT_POINTS, factory);
factory = new DefaultFormatterFactory(new DiceFormatter());
FACTORY_MAP.put(GURPSCharacter.ID_BASIC_THRUST, factory);
FACTORY_MAP.put(GURPSCharacter.ID_BASIC_SWING, factory);
FACTORY_MAP.put(Profile.ID_SIZE_MODIFIER, new DefaultFormatterFactory(new IntegerFormatter(-99, 9999, true)));
FACTORY_MAP.put(Profile.ID_AGE, new DefaultFormatterFactory(new IntegerFormatter(0, Integer.MAX_VALUE, false, true)));
FACTORY_MAP.put(Profile.ID_HEIGHT, new DefaultFormatterFactory(new HeightFormatter(true)));
FACTORY_MAP.put(GURPSCharacter.ID_CREATED_ON, new DefaultFormatterFactory(new DateFormatter(DateFormat.MEDIUM)));
FACTORY_MAP.put(GURPSCharacter.ID_BASIC_SPEED, new DefaultFormatterFactory(new DoubleFormatter(0, 9999, false)));
DefaultFormatter formatter = new DefaultFormatter();
formatter.setOverwriteMode(false);
DEFAULT_FACTORY = new DefaultFormatterFactory(formatter);
}
private static AbstractFormatterFactory getFormatterFactoryForType(String type) {
AbstractFormatterFactory factory = FACTORY_MAP.get(type);
return factory != null ? factory : DEFAULT_FACTORY;
}
@Override
public int getNotificationPriority() {
return 0;
}
@Override
public void actionPerformed(ActionEvent event) {
attemptCommit();
}
@Override
public void attemptCommit() {
try {
commitEdit();
} catch (ParseException exception) {
invalidEdit();
}
}
}