/* * SkillInfoTab.java * Copyright 2008 Connor Petty <cpmeister@users.sourceforge.net> * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * Created on Jul 10, 2008, 8:03:21 PM */ package pcgen.gui2.tabs; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Dimension; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.text.DecimalFormat; import java.util.EventObject; import javax.swing.AbstractSpinnerModel; import javax.swing.ComboBoxModel; import javax.swing.DefaultComboBoxModel; import javax.swing.DefaultListCellRenderer; import javax.swing.DefaultListSelectionModel; import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JEditorPane; import javax.swing.JFormattedTextField; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JSpinner; import javax.swing.JSpinner.DefaultEditor; import javax.swing.JSplitPane; import javax.swing.JTable; import javax.swing.ListSelectionModel; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; import javax.swing.event.ChangeEvent; import javax.swing.event.ListDataEvent; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.text.DefaultFormatterFactory; import javax.swing.text.NumberFormatter; import pcgen.cdom.enumeration.SkillCost; import pcgen.cdom.enumeration.SkillFilter; import pcgen.facade.core.CharacterFacade; import pcgen.facade.core.CharacterLevelFacade; import pcgen.facade.core.CharacterLevelsFacade; import pcgen.facade.core.CharacterLevelsFacade.CharacterLevelEvent; import pcgen.facade.core.CharacterLevelsFacade.SkillBonusListener; import pcgen.facade.core.CharacterLevelsFacade.SkillPointListener; import pcgen.facade.core.SkillFacade; import pcgen.facade.util.event.ListEvent; import pcgen.facade.util.event.ListListener; import pcgen.gui2.filter.Filter; import pcgen.gui2.filter.FilterBar; import pcgen.gui2.filter.FilterButton; import pcgen.gui2.filter.FilterUtilities; import pcgen.gui2.filter.FilteredTreeViewTable; import pcgen.gui2.filter.SearchFilterPanel; import pcgen.gui2.tabs.models.HtmlSheetSupport; import pcgen.gui2.tabs.skill.SkillPointTableModel; import pcgen.gui2.tabs.skill.SkillTreeViewModel; import pcgen.gui2.tools.FlippingSplitPane; import pcgen.gui2.tools.InfoPane; import pcgen.gui2.util.event.ListDataAdapter; import pcgen.gui2.util.table.TableCellUtilities; import pcgen.gui2.util.table.TableCellUtilities.SpinnerEditor; import pcgen.gui2.util.table.TableCellUtilities.SpinnerRenderer; import pcgen.system.LanguageBundle; import pcgen.util.enumeration.Tab; /** * This component allows the user to manage a character's skills. * * @author Connor Petty <cpmeister@users.sourceforge.net> */ @SuppressWarnings("serial") public class SkillInfoTab extends FlippingSplitPane implements CharacterInfoTab, TodoHandler { private final FilteredTreeViewTable<CharacterFacade, SkillFacade> skillTable; private final JTable skillpointTable; private final InfoPane infoPane; private final TabTitle tabTitle; private final FilterButton<CharacterFacade, SkillFacade> cFilterButton; private final FilterButton<CharacterFacade, SkillFacade> trainedFilterButton; private final JEditorPane htmlPane; private final JComboBox skillFilterBox; public SkillInfoTab() { super("Skill"); this.skillTable = new FilteredTreeViewTable<>(); this.skillpointTable = new JTable(); this.infoPane = new InfoPane(); this.cFilterButton = new FilterButton<>("SkillQualified"); this.trainedFilterButton = new FilterButton<>("SkillTrained"); this.tabTitle = new TabTitle(Tab.SKILLS); this.htmlPane = new JEditorPane(); this.skillFilterBox = new JComboBox(); initComponents(); } private void initComponents() { setOrientation(VERTICAL_SPLIT); setResizeWeight(0.70); JSpinner spinner = new JSpinner(); spinner.setEditor(new JSpinner.NumberEditor(spinner, "#0.#")); //$NON-NLS-1$ skillTable.setDefaultRenderer(Float.class, new SpinnerRenderer(spinner)); skillTable.setDefaultRenderer(Integer.class, new TableCellUtilities.AlignRenderer(SwingConstants.CENTER)); skillTable.setDefaultRenderer(String.class, new TableCellUtilities.AlignRenderer(SwingConstants.CENTER)); skillTable.setRowHeight(26); FilterBar<CharacterFacade, SkillFacade> filterBar = new FilterBar<>(); filterBar.addDisplayableFilter(new SearchFilterPanel()); cFilterButton.setText(LanguageBundle.getString("in_classString")); //$NON-NLS-1$ cFilterButton.setEnabled(false); filterBar.addDisplayableFilter(cFilterButton); trainedFilterButton.setText(LanguageBundle.getString("in_trained")); //$NON-NLS-1$ trainedFilterButton.setEnabled(false); filterBar.addDisplayableFilter(trainedFilterButton); JPanel availPanel = FilterUtilities.configureFilteredTreeViewPane(skillTable, filterBar); availPanel.setPreferredSize(new Dimension(650, 300)); JScrollPane tableScrollPane; JPanel tablePanel = new JPanel(new GridBagLayout()); GridBagConstraints constraints = new GridBagConstraints(); constraints.gridwidth = java.awt.GridBagConstraints.REMAINDER; constraints.fill = java.awt.GridBagConstraints.BOTH; constraints.weightx = 1.0; constraints.ipady = 0; constraints.weighty = 1.0; SkillPointTableModel.initializeTable(skillpointTable); tableScrollPane = new JScrollPane(skillpointTable); tablePanel.add(tableScrollPane, constraints); htmlPane.setOpaque(false); htmlPane.setEditable(false); htmlPane.setFocusable(false); htmlPane.setContentType("text/html"); //$NON-NLS-1$ skillFilterBox.setRenderer(new DefaultListCellRenderer()); JScrollPane selScrollPane = new JScrollPane(htmlPane); JPanel skillPanel = new JPanel(new BorderLayout()); skillPanel.add(skillFilterBox, BorderLayout.NORTH); skillPanel.add(selScrollPane, BorderLayout.CENTER); selScrollPane.setPreferredSize(new Dimension(530, 300)); FlippingSplitPane topPane = new FlippingSplitPane(JSplitPane.HORIZONTAL_SPLIT, true, availPanel, skillPanel, "SkillTop"); setTopComponent(topPane); FlippingSplitPane bottomPane = new FlippingSplitPane(JSplitPane.HORIZONTAL_SPLIT, "SkillBottom"); bottomPane.setLeftComponent(tablePanel); tablePanel.setPreferredSize(new Dimension(650, 100)); bottomPane.setRightComponent(infoPane); infoPane.setPreferredSize(new Dimension(530, 100)); setBottomComponent(bottomPane); } @Override public ModelMap createModels(final CharacterFacade character) { ModelMap models = new ModelMap(); ListSelectionModel listModel = new DefaultListSelectionModel(); listModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); models.put(ListSelectionModel.class, listModel); models.put(SkillPointTableModel.class, new SkillPointTableModel(character)); models.put(SkillTreeViewModel.class, new SkillTreeViewModel(character, listModel)); models.put(FilterHandler.class, new FilterHandler(character, listModel)); models.put(InfoHandler.class, new InfoHandler(character)); models.put(LevelSelectionHandler.class, new LevelSelectionHandler(character, listModel)); models.put(SkillRankSpinnerEditor.class, new SkillRankSpinnerEditor(character, listModel)); SkillSheetHandler skillSheetHandler = new SkillSheetHandler(character); models.put(SkillSheetHandler.class, skillSheetHandler); models.put(SkillFilterHandler.class, new SkillFilterHandler(character, skillSheetHandler)); return models; } @Override public void storeModels(ModelMap models) { models.get(SkillTreeViewModel.class).uninstall(); models.get(FilterHandler.class).uninstall(); models.get(InfoHandler.class).uninstall(); models.get(LevelSelectionHandler.class).uninstall(); models.get(SkillSheetHandler.class).uninstall(); } @Override public void restoreModels(ModelMap models) { models.get(FilterHandler.class).install(); models.get(SkillFilterHandler.class).install(); skillpointTable.setModel(models.get(SkillPointTableModel.class)); skillpointTable.setSelectionModel(models.get(ListSelectionModel.class)); skillTable.setDefaultEditor(Float.class, models.get(SkillRankSpinnerEditor.class)); models.get(SkillTreeViewModel.class).install(skillTable); models.get(InfoHandler.class).install(); models.get(LevelSelectionHandler.class).install(); models.get(SkillSheetHandler.class).install(); } @Override public TabTitle getTabTitle() { return tabTitle; } @Override public void adviseTodo(String fieldName) { skillTable.requestFocusInWindow(); } private class SkillSheetHandler implements SkillBonusListener { private final HtmlSheetSupport support; public SkillSheetHandler(CharacterFacade character) { String sheet = character.getDataSet().getGameMode().getInfoSheetSkill(); support = new HtmlSheetSupport(character, htmlPane, sheet); character.getCharacterLevelsFacade().addSkillBonusListener(this); } public void install() { support.install(); } public void uninstall() { support.uninstall(); } @Override public void skillBonusChanged(CharacterLevelEvent e) { support.refresh(); } } private class LevelSelectionHandler implements ListListener<CharacterLevelFacade>, SkillPointListener, Runnable, ListSelectionListener { private final CharacterLevelsFacade levels; private final ListSelectionModel model; public LevelSelectionHandler(CharacterFacade character, ListSelectionModel model) { this.levels = character.getCharacterLevelsFacade(); this.model = model; } public void install() { levels.addSkillPointListener(this); levels.addListListener(this); model.addListSelectionListener(this); updateSelectedIndex(false); } public void uninstall() { levels.removeSkillPointListener(this); levels.removeListListener(this); model.removeListSelectionListener(this); } @Override public void run() { for (int startIndex : new int[] { model.getMinSelectionIndex(), 0 }) { for (int i = startIndex; i < levels.getSize(); i++) { CharacterLevelFacade level = levels.getElementAt(i); if (levels.getSpentSkillPoints(level) < levels.getGainedSkillPoints(level)) { if (i != model.getMinSelectionIndex()) { //Logging.errorPrint("updateSelectedIndex got " + level); model.setSelectionInterval(i, i); skillpointTable.scrollRectToVisible(skillpointTable .getCellRect(i, 0, true)); } return; } } } // Fall back for a non empty list of levels is to select the highest one. if (levels.getSize() > 0) { model.setSelectionInterval(levels.getSize() - 1, levels.getSize() - 1); skillpointTable.scrollRectToVisible(skillpointTable .getCellRect(levels.getSize() - 1, 0, true)); } } private void updateSelectedIndex(boolean forceChange) { if (levels.isEmpty()) { return; } //if a level is already selected, don't change it //unless all the skill points have been spent if (!model.isSelectionEmpty() && !forceChange) { int index = model.getMinSelectionIndex(); CharacterLevelFacade level = levels.getElementAt(index); if (levels.getSpentSkillPoints(level) < levels.getGainedSkillPoints(level)) { return; } } /* Updating now would conflict with the JTable updating due to the same events. * So update the selection model after JTable has had its way with it. */ SwingUtilities.invokeLater(this); } @Override public void skillPointsChanged(CharacterLevelEvent e) { int firstRow = e.getBaseLevelIndex(); boolean force = firstRow < model.getMinSelectionIndex(); updateSelectedIndex(force); } @Override public void elementAdded(ListEvent<CharacterLevelFacade> e) { updateSelectedIndex(false); } @Override public void elementRemoved(ListEvent<CharacterLevelFacade> e) { updateSelectedIndex(false); } @Override public void elementsChanged(ListEvent<CharacterLevelFacade> e) { updateSelectedIndex(false); } @Override public void elementModified(ListEvent<CharacterLevelFacade> e) { updateSelectedIndex(false); } @Override public void valueChanged(ListSelectionEvent e) { skillTable.refreshModelData(); } } private class FilterHandler implements ListSelectionListener { private final Filter<CharacterFacade, SkillFacade> cFilter = new Filter<CharacterFacade, SkillFacade>() { @Override public boolean accept(CharacterFacade context, SkillFacade element) { if (context == null) { return false; } CharacterLevelsFacade levels = context.getCharacterLevelsFacade(); CharacterLevelFacade level = levels.getElementAt(model.getMinSelectionIndex()); return levels.getSkillCost(level, element) == SkillCost.CLASS; } }; private final Filter<CharacterFacade, SkillFacade> gainedFilter = new Filter<CharacterFacade, SkillFacade>() { @Override public boolean accept(CharacterFacade context, SkillFacade element) { if (context == null) { return false; } CharacterLevelsFacade levels = context.getCharacterLevelsFacade(); return levels.getSkillRanks(null, element) > 0.0f; } }; private final ListSelectionModel model; private final CharacterFacade character; private boolean installed = false; public FilterHandler(CharacterFacade character, ListSelectionModel model) { this.character = character; this.model = model; model.addListSelectionListener(this); } public void install() { installed = true; cFilterButton.setFilter(cFilter); trainedFilterButton.setFilter(gainedFilter); skillTable.setContext(character); } public void uninstall() { installed = false; } @Override public void valueChanged(ListSelectionEvent e) { if (installed && !e.getValueIsAdjusting()) { cFilterButton.setEnabled(model.getMinSelectionIndex() != -1); trainedFilterButton.setEnabled(model.getMinSelectionIndex() != -1); } } } private class SkillRankSpinnerEditor extends SpinnerEditor { private final SkillRankSpinnerModel model; private ListSelectionModel listModel; private CharacterFacade character; public SkillRankSpinnerEditor(CharacterFacade character, ListSelectionModel listModel) { this(new SkillRankSpinnerModel(character)); this.listModel = listModel; this.character = character; } private SkillRankSpinnerEditor(SkillRankSpinnerModel model) { super(model); this.model = model; DefaultEditor editor = new DefaultEditor(spinner); NumberFormatter formatter = new NumberFormatter(new DecimalFormat("#0.#")); //$NON-NLS-1$ formatter.setValueClass(Float.class); DefaultFormatterFactory factory = new DefaultFormatterFactory(formatter); JFormattedTextField ftf = editor.getTextField(); ftf.setEditable(true); ftf.setFormatterFactory(factory); ftf.setHorizontalAlignment(SwingConstants.RIGHT); spinner.setEditor(editor); } @Override public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { SkillFacade skill = (SkillFacade) table.getModel().getValueAt(row, 0); int index = listModel.getMinSelectionIndex(); model.configureModel(skill, character.getCharacterLevelsFacade().getElementAt(index)); return spinner; } @Override public boolean isCellEditable(EventObject e) { return listModel.getMinSelectionIndex() != -1; } @Override public void stateChanged(ChangeEvent e) { releaseMouse(spinner); super.stateChanged(e); } private void releaseMouse(JSpinner jSpinner) { for (int i = 0; i < jSpinner.getComponentCount(); i++) { Component comp = jSpinner.getComponent(i); if (comp instanceof JButton) { releaseMouse(comp); } } } private void releaseMouse(Component component) { MouseListener[] listeners = component.getMouseListeners(); for (int i = 0; i < listeners.length; i++) { MouseListener listener = listeners[i]; listener.mouseReleased(new MouseEvent(component, MouseEvent.MOUSE_RELEASED, System.currentTimeMillis(), 0, 0, 0, 1, false)); } } } private class SkillRankSpinnerModel extends AbstractSpinnerModel { private final CharacterFacade character; private CharacterLevelFacade level; private SkillFacade skill; public SkillRankSpinnerModel(CharacterFacade character) { this.character = character; } @Override public Float getValue() { return character.getCharacterLevelsFacade().getSkillRanks(level, skill); } public void configureModel(SkillFacade sk, CharacterLevelFacade charLevel) { this.skill = sk; this.level = charLevel; fireStateChanged(); } @Override public void setValue(Object value) { if (value instanceof Float) { setValue((Float) value); } } public void setValue(Float value) { if (value == null) { return; } CharacterLevelsFacade levels = character.getCharacterLevelsFacade(); CharacterLevelFacade targetLevel = levels.findNextLevelForSkill(skill, level, value); if (targetLevel == null) { // No level where it can be raised. return; } SkillCost cost = levels.getSkillCost(targetLevel, skill); if (value < 0) { value = 0.0f; } float max = levels.getMaxRanks(targetLevel, cost, levels.isClassSkillForMaxRanks(targetLevel, skill)); if (value > max) { value = max; } int points = (int) ((value - getValue()) * levels.getSkillCost(targetLevel, skill).getCost()); if (points == 0) { // No change, ignore return; } if (levels.investSkillPoints(targetLevel, skill, points)) { fireStateChanged(); //TODO: Remove this method when CharacterFacade's event system is created. //skillpointTable.repaint(); skillTable.refreshModelData(); } else { skillTable.getCellEditor().cancelCellEditing(); } } @Override public Float getNextValue() { float value = getValue(); if (level == null) { return null; } CharacterLevelsFacade levels = character.getCharacterLevelsFacade(); CharacterLevelFacade targetLevel = levels.findNextLevelForSkill(skill, level, value); if (targetLevel == null) { // No level where it can be raised. return null; } SkillCost cost = levels.getSkillCost(targetLevel, skill); CharacterLevelFacade highestLevel = levels.getElementAt(levels.getSize() - 1); if (value == levels.getMaxRanks(highestLevel, cost, levels.isClassSkillForMaxRanks(highestLevel, skill))) { return null; } return value + 1.0f / cost.getCost(); } @Override public Float getPreviousValue() { float value = getValue(); if (level == null || value == 0) { return null; } CharacterLevelsFacade levels = character.getCharacterLevelsFacade(); SkillCost cost = levels.getSkillCost(level, skill); return value - 1.0f / cost.getCost(); } } private class InfoHandler implements ListSelectionListener { private CharacterFacade character; private String text; public InfoHandler(CharacterFacade character) { this.character = character; this.text = ""; //$NON-NLS-1$ } public void install() { skillTable.getSelectionModel().addListSelectionListener(this); infoPane.setText(text); } public void uninstall() { skillTable.getSelectionModel().removeListSelectionListener(this); } @Override public void valueChanged(ListSelectionEvent e) { if (!e.getValueIsAdjusting()) { Object data = skillTable.getSelectedObject(); if (data != null && data instanceof SkillFacade) { text = character.getInfoFactory().getHTMLInfo( (SkillFacade) data); } else { text = ""; //$NON-NLS-1$ } infoPane.setText(text); } } } private class SkillFilterHandler extends ListDataAdapter { private CharacterFacade character; private SkillSheetHandler skillSheetHandler; private ComboBoxModel model; public SkillFilterHandler(CharacterFacade character, SkillSheetHandler skillSheetHandler) { this.character = character; this.skillSheetHandler = skillSheetHandler; model = new DefaultComboBoxModel(new SkillFilter[] { SkillFilter.Ranks, SkillFilter.NonDefault, SkillFilter.Usable, SkillFilter.All }); SkillFilter filter = character.getSkillFilterRef().get(); model.setSelectedItem(filter); model.addListDataListener(this); } public void install() { skillFilterBox.setModel(model); } @Override public void listDataChanged(ListDataEvent e) { if (e.getIndex0() == -1 && e.getIndex1() == -1) { SkillFilter filter = (SkillFilter) skillFilterBox.getSelectedItem(); character.setSkillFilter(filter); skillSheetHandler.support.refresh(); } } } }