/*
* 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.LinkedLabel;
import com.trollworks.toolkit.ui.widget.outline.OutlineModel;
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.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashSet;
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;
/** The detailed editor for {@link Skill}s. */
public class SkillEditor extends RowEditor<Skill> implements ActionListener, DocumentListener {
@Localize("Name")
@Localize(locale = "de", value = "Name")
@Localize(locale = "ru", value = "Название")
@Localize(locale = "es", value = "Nombre")
private static String NAME;
@Localize("The base name of the skill, without any notes or specialty information")
@Localize(locale = "de", value = "Der Name der Fertigkeit ohne Anmerkungen oder Spezialisierung")
@Localize(locale = "ru", value = "Базовое название умения, без заметок или информации по специализации")
@Localize(locale = "es", value = "Nombre de la habilidad, sin notas ni otra información")
private static String 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 NAME_CANNOT_BE_EMPTY;
@Localize("Specialization")
@Localize(locale = "de", value = "Spezialisierung")
@Localize(locale = "ru", value = "Специализация")
@Localize(locale = "es", value = "Especialización")
private static String SPECIALIZATION;
@Localize("The specialization, if any, taken for this skill")
@Localize(locale = "de", value = "Die Spezialisierung, wenn für diese Fertigkeit eine genommen wurde")
@Localize(locale = "ru", value = "Специализация, если есть, используемая для этого умения")
@Localize(locale = "es", value = "Especialización, si la hay, viene de esta habilidad")
private static String SPECIALIZATION_TOOLTIP;
@Localize("Categories")
@Localize(locale = "de", value = "Kategorie")
@Localize(locale = "ru", value = "Категории")
@Localize(locale = "es", value = "Categorías")
private static String CATEGORIES;
@Localize("The category or categories the skill 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 habilidad (separa categorías multiples con una coma)")
private static String CATEGORIES_TOOLTIP;
@Localize("Notes")
@Localize(locale = "de", value = "Anmerkungen")
@Localize(locale = "ru", value = "Заметка")
@Localize(locale = "es", value = "Notas")
private static String NOTES;
@Localize("Any notes that you would like to show up in the list along with this skill")
@Localize(locale = "de", value = "Anmerkungen, die in der Liste neben der Fertigkeit erscheinen sollen")
@Localize(locale = "ru", value = "Заметки, которые показываются в списке рядом с умением")
@Localize(locale = "es", value = "Cualquier nota que te gustaría que se mostrara junto a la habilidad")
private static String NOTES_TOOLTIP;
@Localize("Tech Level")
@Localize(locale = "de", value = "Techlevel")
@Localize(locale = "ru", value = "Технологический уровень")
@Localize(locale = "es", value = "Nivel tecnológico")
private static String TECH_LEVEL;
@Localize("Whether this skill requires tech level specialization, and, if so, at what tech level it was learned")
@Localize(locale = "de", value = "Ob diese Fertigkeit auf einen bestimmten Techlevel spezialisiert ist und wenn, mit welchem Techlevel sie gelernt wurde")
@Localize(locale = "ru", value = "Для умения необходима специализации с технологическим уровнем с указанием уровня изучения")
@Localize(locale = "es", value = "Si la habilidad requiere especialización por nivel tecnológico, este es para el que se ha aprendido")
private static String TECH_LEVEL_TOOLTIP;
@Localize("Tech Level Required")
@Localize(locale = "de", value = "Techlevel benötigt")
@Localize(locale = "ru", value = "Необходимый технологический уровень")
@Localize(locale = "es", value = "Nivel tecnológico requerido")
private static String TECH_LEVEL_REQUIRED;
@Localize("Whether this skill requires tech level specialization")
@Localize(locale = "de", value = "Ob diese Fertigkeit auf einen bestimmten Techlevel spezialisiert ist")
@Localize(locale = "ru", value = "Для умения необходима специализация с технологическим уровнем")
@Localize(locale = "es", value = "Esta habilidad requiere especializarse por nivel tecnológico")
private static String TECH_LEVEL_REQUIRED_TOOLTIP;
@Localize("Difficulty")
@Localize(locale = "de", value = "Schwierigkeit")
@Localize(locale = "ru", value = "Сложность")
@Localize(locale = "es", value = "Dificultad")
private static String EDITOR_DIFFICULTY;
@Localize("The difficulty of learning this skill")
@Localize(locale = "de", value = "Die Schwierigkeit dieser Fertikgeit")
@Localize(locale = "ru", value = "Сложность изучения умения")
@Localize(locale = "es", value = "Dificultad de la arendizaje de la habilidad")
private static String EDITOR_DIFFICULTY_TOOLTIP;
@Localize("The relative difficulty of learning this skill")
@Localize(locale = "de", value = "Die relative Schwierigkeit dieser Fertigkeit")
@Localize(locale = "ru", value = "Относительная сложность изучения умения")
@Localize(locale = "es", value = "Dificultad relativa de aprendizaje de la habilidad")
private static String EDITOR_DIFFICULTY_POPUP_TOOLTIP;
@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("The attribute this skill is based on")
@Localize(locale = "de", value = "Das Attribut, auf dem diese Fertigkeit basiert")
@Localize(locale = "ru", value = "Базовый атрибут умения ")
@Localize(locale = "es", value = "Atributo que se aplica a esta habilidad")
private static String ATTRIBUTE_POPUP_TOOLTIP;
@Localize("Points")
@Localize(locale = "de", value = "Punkte")
@Localize(locale = "ru", value = "Очки")
@Localize(locale = "es", value = "Puntos")
private static String EDITOR_POINTS;
@Localize("The number of points spent on this skill")
@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 habilidad")
private static String EDITOR_POINTS_TOOLTIP;
@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("A reference to the book and page this skill 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 Fertigkeit 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 habilidad (p.e. B22 se refiere al \"Manual Básico\", página 22)")
private static String REFERENCE_TOOLTIP;
@Localize("Encumbrance")
@Localize(locale = "de", value = "Belastung")
@Localize(locale = "ru", value = "Нагрузка")
@Localize(locale = "es", value = "Carga")
private static String ENC_PENALTY_MULT;
@Localize("The encumbrance penalty multiplier")
@Localize(locale = "de", value = "Der Belastungs-Malus-Multiplikator")
@Localize(locale = "ru", value = "Множитель штрафа за нагрузку")
@Localize(locale = "es", value = "Multiplicador a la penalización por carga")
private static String ENC_PENALTY_MULT_TOOLTIP;
@Localize("No penalty due to encumbrance")
@Localize(locale = "de", value = "Kein Malus durch Belastung")
@Localize(locale = "ru", value = "Нет штрафа за нагрузку")
@Localize(locale = "es", value = "Sin penalización por carga")
private static String NO_ENC_PENALTY;
@Localize("Penalty equal to the current encumbrance level")
@Localize(locale = "de", value = "Malus gleich der aktuellen Belastungsstufe")
@Localize(locale = "ru", value = "Штраф равен текущему уровню нагрузки")
@Localize(locale = "es", value = "Penalización igual al nivel de carga actual")
private static String ONE_ENC_PENALTY;
@Localize("Penalty equal to {0} times the current encumbrance level")
@Localize(locale = "de", value = "Malus gleich dem {0}-fachen der aktuellen Belastungsstufe")
@Localize(locale = "ru", value = "Штраф в {0} раза выше нынешнего уровня нагрузки")
@Localize(locale = "es", value = "Penalización igual a {0} veces el nivel de carga actual")
private static String ENC_PENALTY_FORMAT;
static {
Localization.initialize();
}
private JTextField mNameField;
private JTextField mSpecializationField;
private JTextField mNotesField;
private JTextField mCategoriesField;
private JTextField mReferenceField;
private JCheckBox mHasTechLevel;
private JTextField mTechLevel;
private String mSavedTechLevel;
private JComboBox<Object> mAttributePopup;
private JComboBox<Object> mDifficultyPopup;
private JTextField mPointsField;
private JTextField mLevelField;
private JComboBox<Object> mEncPenaltyPopup;
private JTabbedPane mTabPanel;
private PrereqsPanel mPrereqs;
private FeaturesPanel mFeatures;
private Defaults mDefaults;
private MeleeWeaponEditor mMeleeWeapons;
private RangedWeaponEditor mRangedWeapons;
/**
* Creates a new {@link Skill} editor.
*
* @param skill The {@link Skill} to edit.
*/
public SkillEditor(Skill skill) {
super(skill);
JPanel content = new JPanel(new ColumnLayout(2));
JPanel fields = new JPanel(new ColumnLayout(2));
JLabel icon = new JLabel(skill.getIcon(true));
boolean notContainer = !skill.canHaveChildren();
Container wrapper;
mNameField = createCorrectableField(fields, NAME, skill.getName(), NAME_TOOLTIP);
if (notContainer) {
wrapper = new JPanel(new ColumnLayout(2));
mSpecializationField = createField(fields, wrapper, SPECIALIZATION, skill.getSpecialization(), SPECIALIZATION_TOOLTIP, 0);
createTechLevelFields(wrapper);
fields.add(wrapper);
mEncPenaltyPopup = createEncumbrancePenaltyMultiplierPopup(fields);
}
mNotesField = createField(fields, fields, NOTES, skill.getNotes(), NOTES_TOOLTIP, 0);
mCategoriesField = createField(fields, fields, CATEGORIES, skill.getCategoriesAsString(), CATEGORIES_TOOLTIP, 0);
if (notContainer) {
wrapper = createDifficultyPopups(fields);
} else {
wrapper = fields;
}
mReferenceField = createField(wrapper, wrapper, EDITOR_REFERENCE, mRow.getReference(), REFERENCE_TOOLTIP, 6);
icon.setVerticalAlignment(SwingConstants.TOP);
icon.setAlignmentY(-1f);
content.add(icon);
content.add(fields);
add(content);
if (notContainer) {
mTabPanel = new JTabbedPane();
mPrereqs = new PrereqsPanel(mRow, mRow.getPrereqs());
mMeleeWeapons = MeleeWeaponEditor.createEditor(mRow);
mRangedWeapons = RangedWeaponEditor.createEditor(mRow);
mFeatures = new FeaturesPanel(mRow, mRow.getFeatures());
mDefaults = new Defaults(mRow.getDefaults());
mDefaults.addActionListener(this);
Component panel = embedEditor(mDefaults);
mTabPanel.addTab(panel.getName(), panel);
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);
if (!mIsEditable) {
UIUtilities.disableControls(mMeleeWeapons);
UIUtilities.disableControls(mRangedWeapons);
}
UIUtilities.selectTab(mTabPanel, getLastTabName());
add(mTabPanel);
}
}
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 parent, String title, String text, String tooltip) {
JTextField field = new JTextField(text);
field.setToolTipText(Text.wrapPlainTextForToolTip(tooltip));
field.setEnabled(mIsEditable);
field.getDocument().addDocumentListener(this);
LinkedLabel label = new LinkedLabel(title);
label.setLink(field);
parent.add(label);
parent.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);
labelParent.add(new LinkedLabel(title, field));
fieldParent.add(field);
return field;
}
@SuppressWarnings("unused")
private void createPointsFields(Container parent, boolean forCharacter) {
mPointsField = createField(parent, parent, EDITOR_POINTS, Integer.toString(mRow.getPoints()), EDITOR_POINTS_TOOLTIP, 4);
new NumberFilter(mPointsField, false, false, false, 4);
mPointsField.addActionListener(this);
if (forCharacter) {
mLevelField = createField(parent, parent, EDITOR_LEVEL, Skill.getSkillDisplayLevel(mRow.getLevel(), mRow.getRelativeLevel(), mRow.getAttribute(), mRow.canHaveChildren()), EDITOR_LEVEL_TOOLTIP, 8);
mLevelField.setEnabled(false);
}
}
private void createTechLevelFields(Container parent) {
OutlineModel owner = mRow.getOwner();
GURPSCharacter character = mRow.getCharacter();
boolean enabled = !owner.isLocked();
boolean hasTL;
mSavedTechLevel = mRow.getTechLevel();
hasTL = mSavedTechLevel != null;
if (!hasTL) {
mSavedTechLevel = ""; //$NON-NLS-1$
}
if (character != null) {
JPanel wrapper = new JPanel(new ColumnLayout(2));
mHasTechLevel = new JCheckBox(TECH_LEVEL, hasTL);
mHasTechLevel.setToolTipText(Text.wrapPlainTextForToolTip(TECH_LEVEL_TOOLTIP));
mHasTechLevel.setEnabled(enabled);
mHasTechLevel.addActionListener(this);
wrapper.add(mHasTechLevel);
mTechLevel = new JTextField("9999"); //$NON-NLS-1$
UIUtilities.setOnlySize(mTechLevel, mTechLevel.getPreferredSize());
mTechLevel.setText(mSavedTechLevel);
mTechLevel.setToolTipText(Text.wrapPlainTextForToolTip(TECH_LEVEL_TOOLTIP));
mTechLevel.setEnabled(enabled && hasTL);
wrapper.add(mTechLevel);
parent.add(wrapper);
if (!hasTL) {
mSavedTechLevel = character.getDescription().getTechLevel();
}
} else {
mTechLevel = new JTextField(mSavedTechLevel);
mHasTechLevel = new JCheckBox(TECH_LEVEL_REQUIRED, hasTL);
mHasTechLevel.setToolTipText(Text.wrapPlainTextForToolTip(TECH_LEVEL_REQUIRED_TOOLTIP));
mHasTechLevel.setEnabled(enabled);
mHasTechLevel.addActionListener(this);
parent.add(mHasTechLevel);
}
}
private JComboBox<Object> createEncumbrancePenaltyMultiplierPopup(Container parent) {
Object[] items = new Object[10];
items[0] = NO_ENC_PENALTY;
items[1] = ONE_ENC_PENALTY;
for (int i = 2; i < 10; i++) {
items[i] = MessageFormat.format(ENC_PENALTY_FORMAT, new Integer(i));
}
LinkedLabel label = new LinkedLabel(ENC_PENALTY_MULT);
parent.add(label);
JComboBox<Object> popup = createComboBox(parent, items, items[mRow.getEncumbrancePenaltyMultiplier()], ENC_PENALTY_MULT_TOOLTIP);
label.setLink(popup);
return popup;
}
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 ? 10 : 8 : 6));
label.setToolTipText(Text.wrapPlainTextForToolTip(EDITOR_DIFFICULTY_TOOLTIP));
mAttributePopup = createComboBox(wrapper, SkillAttribute.values(), mRow.getAttribute(), ATTRIBUTE_POPUP_TOOLTIP);
wrapper.add(new JLabel(" /")); //$NON-NLS-1$
mDifficultyPopup = createComboBox(wrapper, SkillDifficulty.values(), mRow.getDifficulty(), EDITOR_DIFFICULTY_POPUP_TOOLTIP);
if (forCharacterOrTemplate) {
createPointsFields(wrapper, character != null);
}
wrapper.add(new JPanel());
parent.add(label);
parent.add(wrapper);
return wrapper;
}
private JComboBox<Object> createComboBox(Container parent, Object[] items, Object selection, String tooltip) {
JComboBox<Object> combo = new JComboBox<>(items);
combo.setToolTipText(Text.wrapPlainTextForToolTip(tooltip));
combo.setSelectedItem(selection);
combo.addActionListener(this);
combo.setMaximumRowCount(items.length);
UIUtilities.setOnlySize(combo, combo.getPreferredSize());
combo.setEnabled(mIsEditable);
parent.add(combo);
return combo;
}
private void recalculateLevel() {
if (mLevelField != null) {
SkillAttribute attribute = getSkillAttribute();
SkillLevel level = mRow.calculateLevel(mRow.getCharacter(), mNameField.getText(), mSpecializationField.getText(), mDefaults.getDefaults(), attribute, getSkillDifficulty(), getSkillPoints(), new HashSet<String>(), getEncumbrancePenaltyMultiplier());
mLevelField.setText(Skill.getSkillDisplayLevel(level.mLevel, level.mRelativeLevel, attribute, false));
}
}
private SkillAttribute getSkillAttribute() {
return (SkillAttribute) mAttributePopup.getSelectedItem();
}
private SkillDifficulty getSkillDifficulty() {
return (SkillDifficulty) mDifficultyPopup.getSelectedItem();
}
private int getSkillPoints() {
return Numbers.extractInteger(mPointsField.getText(), 0, true);
}
private int getEncumbrancePenaltyMultiplier() {
return mEncPenaltyPopup.getSelectedIndex();
}
@Override
public boolean applyChangesSelf() {
boolean modified = mRow.setName(mNameField.getText());
modified |= mRow.setReference(mReferenceField.getText());
modified |= mRow.setNotes(mNotesField.getText());
modified |= mRow.setCategories(mCategoriesField.getText());
if (mSpecializationField != null) {
modified |= mRow.setSpecialization(mSpecializationField.getText());
}
if (mHasTechLevel != null) {
modified |= mRow.setTechLevel(mHasTechLevel.isSelected() ? mTechLevel.getText() : null);
}
if (mAttributePopup != null) {
modified |= mRow.setDifficulty(getSkillAttribute(), getSkillDifficulty());
}
if (mEncPenaltyPopup != null) {
modified |= mRow.setEncumbrancePenaltyMultiplier(getEncumbrancePenaltyMultiplier());
}
if (mPointsField != null) {
modified |= mRow.setPoints(getSkillPoints());
}
if (mDefaults != null) {
modified |= mRow.setDefaults(mDefaults.getDefaults());
}
if (mPrereqs != null) {
modified |= mRow.setPrereqs(mPrereqs.getPrereqList());
}
if (mFeatures != null) {
modified |= mRow.setFeatures(mFeatures.getFeatures());
}
if (mMeleeWeapons != null) {
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 == mHasTechLevel) {
boolean enabled = mHasTechLevel.isSelected();
mTechLevel.setEnabled(enabled);
if (enabled) {
mTechLevel.setText(mSavedTechLevel);
mTechLevel.requestFocus();
} else {
mSavedTechLevel = mTechLevel.getText();
mTechLevel.setText(""); //$NON-NLS-1$
}
} else if (src == mAttributePopup || src == mDifficultyPopup || src == mPointsField || src == mDefaults || src == mEncPenaltyPopup) {
recalculateLevel();
}
}
@Override
public void changedUpdate(DocumentEvent event) {
nameChanged();
}
@Override
public void insertUpdate(DocumentEvent event) {
nameChanged();
}
@Override
public void removeUpdate(DocumentEvent event) {
nameChanged();
}
private void nameChanged() {
LinkedLabel.setErrorMessage(mNameField, mNameField.getText().trim().length() != 0 ? null : NAME_CANNOT_BE_EMPTY);
}
}