/*
* 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.skill;
import com.trollworks.gcs.character.GURPSCharacter;
import com.trollworks.gcs.feature.FeaturesPanel;
import com.trollworks.gcs.prereq.PrereqsPanel;
import com.trollworks.gcs.weapon.MeleeWeaponEditor;
import com.trollworks.gcs.weapon.RangedWeaponEditor;
import com.trollworks.gcs.weapon.WeaponStats;
import com.trollworks.gcs.widgets.outline.RowEditor;
import com.trollworks.toolkit.annotation.Localize;
import com.trollworks.toolkit.ui.UIUtilities;
import com.trollworks.toolkit.ui.layout.ColumnLayout;
import com.trollworks.toolkit.ui.widget.Commitable;
import com.trollworks.toolkit.ui.widget.LinkedLabel;
import com.trollworks.toolkit.utility.Localization;
import com.trollworks.toolkit.utility.text.NumberFilter;
import com.trollworks.toolkit.utility.text.Numbers;
import com.trollworks.toolkit.utility.text.Text;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTabbedPane;
import javax.swing.JTextField;
import javax.swing.SwingConstants;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.Document;
/** The detailed editor for {@link Technique}s. */
public class TechniqueEditor extends RowEditor<Technique> implements ActionListener, DocumentListener {
@Localize("Name")
@Localize(locale = "de", value = "Name")
@Localize(locale = "ru", value = "Название")
@Localize(locale = "es", value = "Nombre")
private static String NAME;
@Localize("Notes")
@Localize(locale = "de", value = "Anmerkungen")
@Localize(locale = "ru", value = "Заметка")
@Localize(locale = "es", value = "Notas")
private static String NOTES;
@Localize("Page Reference")
@Localize(locale = "de", value = "Seitenangabe")
@Localize(locale = "ru", value = "Ссылка на страницу")
@Localize(locale = "es", value = "Página de referencia")
private static String EDITOR_REFERENCE;
@Localize("Points")
@Localize(locale = "de", value = "Punkte")
@Localize(locale = "ru", value = "Очки")
@Localize(locale = "es", value = "Puntos")
private static String EDITOR_POINTS;
@Localize("Level")
@Localize(locale = "de", value = "Fertigkeitswert")
@Localize(locale = "ru", value = "Уровень")
@Localize(locale = "es", value = "Nivel")
private static String EDITOR_LEVEL;
@Localize("The skill level and relative skill level to roll against")
@Localize(locale = "de", value = "Der Fertigkeitswert und relativer Fertigkeitswert, gegen die gewürfelt werden muss")
@Localize(locale = "ru", value = "Уровень умения и относительный уровень умения для повторного броска")
@Localize(locale = "es", value = "Nivel y nivel relativo de la habilidad a superar con la tirada")
private static String EDITOR_LEVEL_TOOLTIP;
@Localize("Difficulty")
@Localize(locale = "de", value = "Schwierigkeit")
@Localize(locale = "ru", value = "Сложность")
@Localize(locale = "es", value = "Dificultad")
private static String EDITOR_DIFFICULTY;
@Localize("The base name of the technique, without any notes or specialty information")
@Localize(locale = "de", value = "Der Name der Technik ohne Anmerkungen oder Spezialisierung")
@Localize(locale = "ru", value = "Базовое название техники, без заметок или информации по специализации")
@Localize(locale = "es", value = "Nombre de la técnica, sin notas ni otra información")
private static String TECHNIQUE_NAME_TOOLTIP;
@Localize("The name field may not be empty")
@Localize(locale = "de", value = "Der Name darf nicht leer sein")
@Localize(locale = "ru", value = "Поле \"Название\" не может быть пустым")
@Localize(locale = "es", value = "El nombre no puede estar en blanco")
private static String TECHNIQUE_NAME_CANNOT_BE_EMPTY;
@Localize("Any notes that you would like to show up in the list along with this technique")
@Localize(locale = "de", value = "Anmerkungen, die in der Liste neben der Technik erscheinen sollen")
@Localize(locale = "ru", value = "Заметки, которые показываются в списке рядом с техникой")
@Localize(locale = "es", value = "Cualquier nota que te gustaría que se mostrara junto a la técnica")
private static String TECHNIQUE_NOTES_TOOLTIP;
@Localize("The difficulty of learning this technique")
@Localize(locale = "de", value = "Die Schwierigkeit dieser Fertikgeit")
@Localize(locale = "ru", value = "Сложность изучения техники")
@Localize(locale = "es", value = "Dificultad de aprendizaje de la técnica")
private static String TECHNIQUE_DIFFICULTY_TOOLTIP;
@Localize("The relative difficulty of learning this technique")
@Localize(locale = "de", value = "Die relative Schwierigkeit dieser Fertigkeit")
@Localize(locale = "ru", value = "Относительная сложность изучения техники")
@Localize(locale = "es", value = "Dificultad relativa de aprendizaje de la técnica")
private static String TECHNIQUE_DIFFICULTY_POPUP_TOOLTIP;
@Localize("The number of points spent on this technique")
@Localize(locale = "de", value = "Die Punkte, die für diese Fertigkeit aufgewendet wurden")
@Localize(locale = "ru", value = "Потрачено на технику количество очков")
@Localize(locale = "es", value = "Puntos consumidos en la técnica")
private static String TECHNIQUE_POINTS_TOOLTIP;
@Localize("A reference to the book and page this technique appears on (e.g. B22 would refer to \"Basic Set\", page 22)")
@Localize(locale = "de", value = "Eine Referenz auf das Buch und die Seite, auf der diese Technik beschrieben wird (z.B. B22 würde auf \"Basic Set\" Seite 22 verweisen)")
@Localize(locale = "ru", value = "Ссылка на страницу и книгу, описывающая технику (например B22 - книга \"Базовые правила\", страница 22)")
@Localize(locale = "es", value = "Referencia al libro y página en donde se menciona la técnica (p.e. B22 se refiere al \"Manual Básico\", página 22)")
private static String TECHNIQUE_REFERENCE_TOOLTIP;
@Localize("Defaults To")
@Localize(locale = "de", value = "Grundwert auf")
@Localize(locale = "ru", value = "По умолчанию к")
@Localize(locale = "es", value = "Por defecto")
private static String DEFAULTS_TO;
@Localize("The name of the skill this technique defaults from")
@Localize(locale = "de", value = "Der Name der Fertigkeit, von welcher diese Technik ihren Grundwert bezieht")
@Localize(locale = "ru", value = "Название умения, для которой предназначена техника")
@Localize(locale = "es", value = "Nombre de la habilidad por defecto para esta técnica ")
private static String DEFAULTS_TO_TOOLTIP;
@Localize("The default name field may not be empty")
@Localize(locale = "de", value = "Der Grundwert darf nicht leer sein")
@Localize(locale = "es", value = "El nombre por defecto no puede estar en blanco")
@Localize(locale = "ru", value = "Поле \"Название по умолчанию\" не может быть пустым")
private static String DEFAULT_NAME_CANNOT_BE_EMPTY;
@Localize("The specialization of the skill, if any, this technique defaults from")
@Localize(locale = "de", value = "Die Spezialisierung für diese Fertigkeit, wenn eine genommen wurde, von welcher diese Technik ihren Grundwert bezieht")
@Localize(locale = "ru", value = "Специализация умения, если есть, для которой предназначена техника")
@Localize(locale = "es", value = "Especialización de Habilidad, si la hay, esta técnica toma su valor por defecto de")
private static String DEFAULT_SPECIALIZATION_TOOLTIP;
@Localize("The amount to adjust the default skill level by")
@Localize(locale = "de", value = "Um wieviel der Fertigkeitswert des Grundwerts angepasst wird")
@Localize(locale = "ru", value = "Значение уровня умения по умолчанию")
@Localize(locale = "es", value = "Cantidad hasta la que puede ajustarse el nivel de la habilidad por defecto")
private static String DEFAULT_MODIFIER_TOOLTIP;
@Localize("Cannot exceed default skill level by more than")
@Localize(locale = "de", value = "Kann den Fertigkeitswert des Grundwerts nicht übersteigen um mehr als")
@Localize(locale = "ru", value = "Не может превышать уровень умения по умолчанию, больше чем на")
@Localize(locale = "es", value = "No puede exceder el nivel de la habilidad por defecto en más de")
private static String LIMIT;
@Localize("Whether to limit the maximum level that can be achieved or not")
@Localize(locale = "de", value = "Ob der Fertigkeitswert der Technik eine Obergrenze hat")
@Localize(locale = "ru", value = "Ограничить максимальный достижимый уровень")
@Localize(locale = "es", value = "Si se ha alcanzado o no el máximo nivel al que puede llegar")
private static String LIMIT_TOOLTIP;
@Localize("The maximum amount above the default skill level that this technique can be raised")
@Localize(locale = "de", value = "Die maximale Erhöhung über den Fertigkeitswert des Grundwerts.")
@Localize(locale = "ru", value = "Максимальное значение выше уровня умения по умолчанию, при котором эта техника может быть использована")
@Localize(locale = "es", value = "Cantidad máxima sobre la habilidad por defecto que puede alcanzar esta técnica")
private static String LIMIT_AMOUNT_TOOLTIP;
@Localize("Categories")
@Localize(locale = "de", value = "Kategorie")
@Localize(locale = "ru", value = "Категории")
@Localize(locale = "es", value = "Categoría")
private static String CATEGORIES;
@Localize("The category or categories the technique belongs to (separate multiple categories with a comma)")
@Localize(locale = "de", value = "Die Kategorie oder Kategorien, denen diese Fertigkeit angehört (trenne mehrere Kategorien mit einem Komma)")
@Localize(locale = "ru", value = "Категория или категории, к которым относится техника (перечислить через запятую)")
@Localize(locale = "es", value = "Categoría o categorías a las que pertenece la técnica (separa categorías multiples con una coma)")
private static String CATEGORIES_TOOLTIP;
static {
Localization.initialize();
}
private JTextField mNameField;
private JTextField mNotesField;
private JTextField mCategoriesField;
private JTextField mReferenceField;
private JComboBox<Object> mDifficultyCombo;
private JTextField mPointsField;
private JTextField mLevelField;
private JPanel mDefaultPanel;
private LinkedLabel mDefaultPanelLabel;
private JComboBox<Object> mDefaultTypeCombo;
private JTextField mDefaultNameField;
private JTextField mDefaultSpecializationField;
private JTextField mDefaultModifierField;
private JCheckBox mLimitCheckbox;
private JTextField mLimitField;
private JTabbedPane mTabPanel;
private PrereqsPanel mPrereqs;
private FeaturesPanel mFeatures;
private SkillDefaultType mLastDefaultType;
private MeleeWeaponEditor mMeleeWeapons;
private RangedWeaponEditor mRangedWeapons;
/**
* Creates a new {@link Technique} editor.
*
* @param technique The {@link Technique} to edit.
*/
public TechniqueEditor(Technique technique) {
super(technique);
JPanel content = new JPanel(new ColumnLayout(2));
JPanel fields = new JPanel(new ColumnLayout(2));
JLabel icon = new JLabel(technique.getIcon(true));
Container wrapper;
mNameField = createCorrectableField(fields, fields, NAME, technique.getName(), TECHNIQUE_NAME_TOOLTIP);
mNotesField = createField(fields, fields, NOTES, technique.getNotes(), TECHNIQUE_NOTES_TOOLTIP, 0);
mCategoriesField = createField(fields, fields, CATEGORIES, technique.getCategoriesAsString(), CATEGORIES_TOOLTIP, 0);
createDefaults(fields);
createLimits(fields);
wrapper = createDifficultyPopups(fields);
mReferenceField = createField(wrapper, wrapper, EDITOR_REFERENCE, mRow.getReference(), TECHNIQUE_REFERENCE_TOOLTIP, 6);
icon.setVerticalAlignment(SwingConstants.TOP);
icon.setAlignmentY(-1f);
content.add(icon);
content.add(fields);
add(content);
mTabPanel = new JTabbedPane();
mPrereqs = new PrereqsPanel(mRow, mRow.getPrereqs());
mFeatures = new FeaturesPanel(mRow, mRow.getFeatures());
mMeleeWeapons = MeleeWeaponEditor.createEditor(mRow);
mRangedWeapons = RangedWeaponEditor.createEditor(mRow);
Component panel = embedEditor(mPrereqs);
mTabPanel.addTab(panel.getName(), panel);
panel = embedEditor(mFeatures);
mTabPanel.addTab(panel.getName(), panel);
mTabPanel.addTab(mMeleeWeapons.getName(), mMeleeWeapons);
mTabPanel.addTab(mRangedWeapons.getName(), mRangedWeapons);
UIUtilities.selectTab(mTabPanel, getLastTabName());
add(mTabPanel);
}
private void createDefaults(Container parent) {
mDefaultPanel = new JPanel(new ColumnLayout(4));
mDefaultPanelLabel = new LinkedLabel(DEFAULTS_TO);
mDefaultTypeCombo = createComboBox(mDefaultPanel, SkillDefaultType.values(), mRow.getDefault().getType());
mDefaultTypeCombo.setEnabled(mIsEditable);
parent.add(mDefaultPanelLabel);
parent.add(mDefaultPanel);
rebuildDefaultPanel();
}
private JComboBox<Object> createComboBox(Container parent, Object[] items, Object selection) {
JComboBox<Object> combo = new JComboBox<>(items);
combo.setSelectedItem(selection);
combo.addActionListener(this);
combo.setMaximumRowCount(items.length);
UIUtilities.setOnlySize(combo, combo.getPreferredSize());
parent.add(combo);
return combo;
}
private SkillDefaultType getDefaultType() {
return (SkillDefaultType) mDefaultTypeCombo.getSelectedItem();
}
private String getSpecialization() {
StringBuilder builder = new StringBuilder();
String specialization = mDefaultSpecializationField.getText();
builder.append(mDefaultNameField.getText());
if (specialization.length() > 0) {
builder.append(" ("); //$NON-NLS-1$
builder.append(specialization);
builder.append(')');
}
return builder.toString();
}
private void rebuildDefaultPanel() {
SkillDefault def = mRow.getDefault();
boolean skillBased;
mLastDefaultType = getDefaultType();
skillBased = mLastDefaultType.isSkillBased();
Commitable.sendCommitToFocusOwner();
while (mDefaultPanel.getComponentCount() > 1) {
mDefaultPanel.remove(1);
}
if (skillBased) {
mDefaultNameField = createCorrectableField(null, mDefaultPanel, DEFAULTS_TO, def.getName(), DEFAULTS_TO_TOOLTIP);
mDefaultSpecializationField = createField(null, mDefaultPanel, null, def.getSpecialization(), DEFAULT_SPECIALIZATION_TOOLTIP, 0);
mDefaultPanelLabel.setLink(mDefaultNameField);
}
mDefaultModifierField = createNumberField(null, mDefaultPanel, null, DEFAULT_MODIFIER_TOOLTIP, def.getModifier(), 2);
if (!skillBased) {
mDefaultPanel.add(new JPanel());
mDefaultPanel.add(new JPanel());
}
mDefaultPanel.revalidate();
}
private void createLimits(Container parent) {
JPanel wrapper = new JPanel(new ColumnLayout(3));
mLimitCheckbox = new JCheckBox(LIMIT, mRow.isLimited());
mLimitCheckbox.setToolTipText(Text.wrapPlainTextForToolTip(LIMIT_TOOLTIP));
mLimitCheckbox.addActionListener(this);
mLimitCheckbox.setEnabled(mIsEditable);
mLimitField = createNumberField(null, wrapper, null, LIMIT_AMOUNT_TOOLTIP, mRow.getLimitModifier(), 2);
mLimitField.setEnabled(mIsEditable && mLimitCheckbox.isSelected());
mLimitField.addActionListener(this);
wrapper.add(mLimitCheckbox);
wrapper.add(mLimitField);
wrapper.add(new JPanel());
parent.add(new JLabel());
parent.add(wrapper);
}
private JScrollPane embedEditor(Container editor) {
JScrollPane scrollPanel = new JScrollPane(editor);
scrollPanel.setMinimumSize(new Dimension(500, 120));
scrollPanel.setName(editor.toString());
if (!mIsEditable) {
UIUtilities.disableControls(editor);
}
return scrollPanel;
}
private JTextField createCorrectableField(Container labelParent, Container fieldParent, String title, String text, String tooltip) {
JTextField field = new JTextField(text);
field.setToolTipText(Text.wrapPlainTextForToolTip(tooltip));
field.setEnabled(mIsEditable);
field.getDocument().addDocumentListener(this);
if (labelParent != null) {
LinkedLabel label = new LinkedLabel(title);
label.setLink(field);
labelParent.add(label);
}
fieldParent.add(field);
return field;
}
private JTextField createField(Container labelParent, Container fieldParent, String title, String text, String tooltip, int maxChars) {
JTextField field = new JTextField(maxChars > 0 ? Text.makeFiller(maxChars, 'M') : text);
if (maxChars > 0) {
UIUtilities.setOnlySize(field, field.getPreferredSize());
field.setText(text);
}
field.setToolTipText(Text.wrapPlainTextForToolTip(tooltip));
field.setEnabled(mIsEditable);
field.addActionListener(this);
if (labelParent != null) {
labelParent.add(new LinkedLabel(title, field));
}
fieldParent.add(field);
return field;
}
@SuppressWarnings("unused")
private JTextField createNumberField(Container labelParent, Container fieldParent, String title, String tooltip, int value, int maxDigits) {
JTextField field = createField(labelParent, fieldParent, title, Numbers.formatWithForcedSign(value), tooltip, maxDigits + 1);
new NumberFilter(field, false, true, false, maxDigits);
return field;
}
@SuppressWarnings("unused")
private void createPointsFields(Container parent, boolean forCharacter) {
mPointsField = createField(parent, parent, EDITOR_POINTS, Integer.toString(mRow.getPoints()), TECHNIQUE_POINTS_TOOLTIP, 4);
new NumberFilter(mPointsField, false, false, false, 4);
mPointsField.addActionListener(this);
if (forCharacter) {
mLevelField = createField(parent, parent, EDITOR_LEVEL, Technique.getTechniqueDisplayLevel(mRow.getLevel(), mRow.getRelativeLevel(), mRow.getDefault().getModifier()), EDITOR_LEVEL_TOOLTIP, 6);
mLevelField.setEnabled(false);
}
}
private Container createDifficultyPopups(Container parent) {
GURPSCharacter character = mRow.getCharacter();
boolean forCharacterOrTemplate = character != null || mRow.getTemplate() != null;
JLabel label = new JLabel(EDITOR_DIFFICULTY, SwingConstants.RIGHT);
JPanel wrapper = new JPanel(new ColumnLayout(forCharacterOrTemplate ? character != null ? 8 : 6 : 4));
label.setToolTipText(Text.wrapPlainTextForToolTip(TECHNIQUE_DIFFICULTY_TOOLTIP));
mDifficultyCombo = createComboBox(wrapper, new Object[] { SkillDifficulty.A, SkillDifficulty.H }, mRow.getDifficulty());
mDifficultyCombo.setToolTipText(Text.wrapPlainTextForToolTip(TECHNIQUE_DIFFICULTY_POPUP_TOOLTIP));
mDifficultyCombo.setEnabled(mIsEditable);
if (forCharacterOrTemplate) {
createPointsFields(wrapper, character != null);
}
wrapper.add(new JPanel());
parent.add(label);
parent.add(wrapper);
return wrapper;
}
private void recalculateLevel() {
if (mLevelField != null) {
SkillLevel level = Technique.calculateTechniqueLevel(mRow.getCharacter(), mNameField.getText(), getSpecialization(), createNewDefault(), getSkillDifficulty(), getPoints(), mLimitCheckbox.isSelected(), getLimitModifier());
mLevelField.setText(Technique.getTechniqueDisplayLevel(level.mLevel, level.mRelativeLevel, getDefaultModifier()));
}
}
private SkillDefault createNewDefault() {
SkillDefaultType type = getDefaultType();
if (type.isSkillBased()) {
return new SkillDefault(type, mDefaultNameField.getText(), mDefaultSpecializationField.getText(), getDefaultModifier());
}
return new SkillDefault(type, null, null, getDefaultModifier());
}
private SkillDifficulty getSkillDifficulty() {
return (SkillDifficulty) mDifficultyCombo.getSelectedItem();
}
private int getPoints() {
return Numbers.extractInteger(mPointsField.getText(), 0, true);
}
private int getDefaultModifier() {
return Numbers.extractInteger(mDefaultModifierField.getText(), 0, true);
}
private int getLimitModifier() {
return Numbers.extractInteger(mLimitField.getText(), 0, true);
}
@Override
public boolean applyChangesSelf() {
boolean modified = mRow.setName(mNameField.getText());
modified |= mRow.setDefault(createNewDefault());
modified |= mRow.setReference(mReferenceField.getText());
modified |= mRow.setNotes(mNotesField.getText());
modified |= mRow.setCategories(mCategoriesField.getText());
if (mPointsField != null) {
modified |= mRow.setPoints(getPoints());
}
modified |= mRow.setLimited(mLimitCheckbox.isSelected());
modified |= mRow.setLimitModifier(getLimitModifier());
modified |= mRow.setDifficulty(getSkillDifficulty());
modified |= mRow.setPrereqs(mPrereqs.getPrereqList());
modified |= mRow.setFeatures(mFeatures.getFeatures());
ArrayList<WeaponStats> list = new ArrayList<>(mMeleeWeapons.getWeapons());
list.addAll(mRangedWeapons.getWeapons());
modified |= mRow.setWeapons(list);
return modified;
}
@Override
public void finished() {
if (mTabPanel != null) {
updateLastTabName(mTabPanel.getTitleAt(mTabPanel.getSelectedIndex()));
}
}
@Override
public void actionPerformed(ActionEvent event) {
Object src = event.getSource();
if (src == mLimitCheckbox) {
mLimitField.setEnabled(mLimitCheckbox.isSelected());
} else if (src == mDefaultTypeCombo) {
if (mLastDefaultType != getDefaultType()) {
rebuildDefaultPanel();
}
}
if (src == mDifficultyCombo || src == mPointsField || src == mDefaultNameField || src == mDefaultModifierField || src == mLimitCheckbox || src == mLimitField || src == mDefaultSpecializationField || src == mDefaultTypeCombo) {
recalculateLevel();
}
}
@Override
public void changedUpdate(DocumentEvent event) {
Document doc = event.getDocument();
if (doc == mNameField.getDocument()) {
LinkedLabel.setErrorMessage(mNameField, mNameField.getText().trim().length() != 0 ? null : TECHNIQUE_NAME_CANNOT_BE_EMPTY);
} else if (doc == mDefaultNameField.getDocument()) {
LinkedLabel.setErrorMessage(mDefaultNameField, mDefaultNameField.getText().trim().length() != 0 ? null : DEFAULT_NAME_CANNOT_BE_EMPTY);
}
}
@Override
public void insertUpdate(DocumentEvent event) {
changedUpdate(event);
}
@Override
public void removeUpdate(DocumentEvent event) {
changedUpdate(event);
}
}