/* * InfoTabbedPane.java * Copyright 2009 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 Aug 29, 2009, 1:00:39 PM */ package pcgen.gui2.tabs; import java.awt.Component; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.PriorityQueue; import java.util.Queue; import java.util.WeakHashMap; import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import javax.swing.Icon; import javax.swing.JOptionPane; import javax.swing.JTabbedPane; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import pcgen.base.util.DoubleKeyMap; import pcgen.facade.core.CharacterFacade; import pcgen.facade.core.GameModeFacade; import pcgen.facade.core.TodoFacade; import pcgen.gui2.UIPropertyContext; import pcgen.gui2.tabs.CharacterInfoTab.ModelMap; import pcgen.gui2.tools.CharacterSelectionListener; import pcgen.gui2.util.DisplayAwareTab; import pcgen.system.LanguageBundle; import pcgen.util.Logging; import pcgen.util.enumeration.Tab; /** * This class is the tabbed pane that contains all of the CharacterInfoTabs and * manages the models for those tabs. * * @author Connor Petty <cpmeister@users.sourceforge.net> */ @SuppressWarnings("serial") public final class InfoTabbedPane extends JTabbedPane implements CharacterSelectionListener, ChangeListener { public static final int SUMMARY_TAB = 0; public static final int RACE_TAB = 1; public static final int TEMPLATE_TAB = 2; public static final int CLASS_TAB = 3; public static final int SKILL_TAB = 4; public static final int ABILITIES_TAB = 5; public static final int DOMAIN_TAB = 6; public static final int SPELLS_TAB = 7; public static final int INVENTORY_TAB = 8; public static final int DESCRIPTION_TAB = 9; public static final int CHARACTER_SHEET_TAB = 10; private final DoubleKeyMap<CharacterFacade, CharacterInfoTab, ModelMap> stateMap; private final Map<CharacterFacade, Integer> tabSelectionMap; private final TabModelService modelService; private final List<CharacterInfoTab> fullTabList = new ArrayList<>(); private CharacterFacade currentCharacter = null; public InfoTabbedPane() { this.stateMap = new DoubleKeyMap<>(); this.tabSelectionMap = new WeakHashMap<>(); this.modelService = new TabModelService(); initComponent(); } public void clearStateMap() { //Make sure that models get a chance to detach themselves from the UI before disgarding them if (currentCharacter != null) { Map<CharacterInfoTab, ModelMap> states = stateMap.getMapFor(currentCharacter); for (CharacterInfoTab tab : states.keySet()) { tab.storeModels(states.get(tab)); } } stateMap.clear(); tabSelectionMap.clear(); currentCharacter = null; } private void initComponent() { setTabPlacement(SwingConstants.TOP); SummaryInfoTab tab = new SummaryInfoTab(); addTab(tab); tab.addPropertyChangeListener(new TabActionListener(tab)); addTab(new RaceInfoTab()); addTab(new TemplateInfoTab()); addTab(new ClassInfoTab()); addTab(new SkillInfoTab()); addTab(new AbilitiesInfoTab()); addTab(new DomainInfoTab()); addTab(new SpellsInfoTab()); addTab(new InventoryInfoTab()); addTab(new DescriptionInfoTab()); addTab(new TempBonusInfoTab()); addTab(new CompanionInfoTab()); addTab(new CharacterSheetInfoTab()); addChangeListener(this); } private <T extends Component & CharacterInfoTab> void addTab(T tab) { TabTitle tabTitle = tab.getTabTitle(); String title = (String) tabTitle.getValue(TabTitle.TITLE); String tooltip = (String) tabTitle.getValue(TabTitle.TOOLTIP); Icon icon = (Icon) tabTitle.getValue(TabTitle.ICON); addTab(title, icon, tab, tooltip); fullTabList.add(tab); tabTitle.addPropertyChangeListener(new TabActionListener(tab)); } @Override public void setCharacter(CharacterFacade character) { modelService.cancelRestoreTasks(); if (!stateMap.containsKey(character)) { //This is the first time this character has been added, so initialize the tab states. for (int i = 0; i < getTabCount(); i++) { CharacterInfoTab tab = (CharacterInfoTab) getComponentAt(i); ModelMap models = tab.createModels(character); stateMap.put(character, tab, models); } String key = UIPropertyContext.C_PROP_INITIAL_TAB; key = UIPropertyContext.createCharacterPropertyKey(character, key); //defaults to the summary tab if prop doesn't exist int startingTab = UIPropertyContext.getInstance().getInt(key, SUMMARY_TAB); tabSelectionMap.put(character, startingTab); } if (currentCharacter != null) { Map<CharacterInfoTab, ModelMap> states = stateMap.getMapFor(currentCharacter); modelService.storeModels(states); //Save tabSelection for this character tabSelectionMap.put(currentCharacter, getSelectedIndex()); } currentCharacter = character; Map<CharacterInfoTab, ModelMap> states = stateMap.getMapFor(character); updateTabsForCharacter(character); int selectedIndex = tabSelectionMap.get(character); modelService.restoreModels(states, selectedIndex); } /** * Update the displayed tabs to reflect the settings of the game mode of the * character to which we are switching. * * @param character The character being displayed. */ private void updateTabsForCharacter(CharacterFacade character) { GameModeFacade gameMode = character.getDataSet().getGameMode(); int tabIndex = 0; for (CharacterInfoTab charInfoTab : fullTabList) { TabTitle tabTitle = charInfoTab.getTabTitle(); Tab tab = tabTitle.getTab(); String newName = gameMode.getTabName(tab); if (!newName.equals(tabTitle.getValue(TabTitle.TITLE))) { tabTitle.putValue(TabTitle.TITLE, newName); } if (gameMode.getTabShown(tab)) { if (getComponentAt(tabIndex) != charInfoTab) { String title = (String) tabTitle.getValue(TabTitle.TITLE); String tooltip = (String) tabTitle.getValue(TabTitle.TOOLTIP); Icon icon = (Icon) tabTitle.getValue(TabTitle.ICON); insertTab(title, icon, (Component) charInfoTab, tooltip, tabIndex); } tabIndex++; } else { if (getComponentAt(tabIndex) == charInfoTab) { remove(tabIndex); } } } } /** * Switch the current tab to be the one named, possibly including a sub tab * and then advise the user of the item to be done. generally the tab will * handle this but a fallback of a dialog will be used if the tab can't do * the advising. * * @param dest An arry of the tab name, the field name and optionally the * sub tab name. */ private void switchTabsAndAdviseTodo(String[] dest) { Tab tab = Tab.valueOf(dest[0]); String tabName = currentCharacter.getDataSet().getGameMode().getTabName(tab); Component selTab = null; for (int i = 0; i < getTabCount(); i++) { if (tabName.equals(getTitleAt(i))) { setSelectedIndex(i); selTab = getComponent(i); break; } } if (selTab == null) { Logging.errorPrint("Failed to find tab " + tabName); //$NON-NLS-1$ return; } if (selTab instanceof JTabbedPane && dest.length > 2) { JTabbedPane tabPane = (JTabbedPane) selTab; for (int i = 0; i < tabPane.getTabCount(); i++) { if (dest[2].equals(tabPane.getTitleAt(i))) { tabPane.setSelectedIndex(i); //selTab = tab.getComponent(i); break; } } } if (selTab instanceof TodoHandler) { ((TodoHandler) selTab).adviseTodo(dest[1]); } else { String message = LanguageBundle.getFormattedString("in_todoUseField", dest[1]); //$NON-NLS-1$ JOptionPane.showMessageDialog(selTab, message, LanguageBundle.getString("in_tipsString"), //$NON-NLS-1$ JOptionPane.INFORMATION_MESSAGE); } } private void handleDisplayAware() { // The currently displayed tab has changed so if the new one wants to know about it, let it know Component comp = getSelectedComponent(); if (comp instanceof DisplayAwareTab) { ((DisplayAwareTab) comp).tabSelected(); } } @Override public void stateChanged(ChangeEvent e) { handleDisplayAware(); } public void characterRemoved(CharacterFacade character) { stateMap.removeAll(character); } /** * This class handles the concurrent processing of storing and restoring tab * models. Conceptually this process consists of two separate processing * queues. One queue is the orderly execution of restoring tab models which * takes place in a a semi-concurrent manner. Each tab has its models * restored as a separate task on the EventDispatchThread which allows for * the UI to remain responsive to other events. If the user selects a * different character while tab models are being restored then the model * restoration is canceled and the tabs which completed restoration will be * processed to store their models. This is where the second queue is needed * because it contains the tabs which have completed their model * restoration. So on a character tab change the general process is as * follows:<br> * 1. cancel all restoration tasks that have not yet executed<br> * 2. clear the restoration queue<br> * 3. process the store queue<br> * 4. push all tabs onto the restoration process queue<br> * * The order in which tabs have their models restored is dependent on the * amount of time that it takes a tab to restore their model data. The tabs * that take the least amount of time to restore their models will be * executed first, the second least second, and so on. The calculation of * time taken is based on the amount of time the previous execution of * restoreModels() took. */ private class TabModelService extends ThreadPoolExecutor implements Comparator<CharacterInfoTab> { private final Map<CharacterInfoTab, Long> timingMap; private final Queue<CharacterInfoTab> storeQueue; private final Queue<Future<?>> restoreQueue; public TabModelService() { super(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(), new ThreadFactory() { @Override public Thread newThread(Runnable r) { Thread thread = new Thread(r); thread.setDaemon(true); thread.setPriority(Thread.NORM_PRIORITY); thread.setName("tab-info-thread"); //$NON-NLS-1$ return thread; } }); this.timingMap = new HashMap<>(); storeQueue = new LinkedList<>(); restoreQueue = new LinkedList<>(); } @Override public int compare(CharacterInfoTab o1, CharacterInfoTab o2) { if (timingMap.containsKey(o1) && timingMap.containsKey(o2)) { long dif = timingMap.get(o1) - timingMap.get(o2); if (dif < 0) { return -1; } if (dif > 0) { return 1; } } else if (timingMap.containsKey(o1)) { return 1; } else if (timingMap.containsKey(o2)) { return -1; } return 0; } private void restoreTab(CharacterInfoTab infoTab, ModelMap models) { long starttime = System.nanoTime(); infoTab.restoreModels(models); long time = System.nanoTime() - starttime; timingMap.put(infoTab, time); storeQueue.add(infoTab); } public void restoreModels(Map<CharacterInfoTab, ModelMap> states, int selectedIndex) { CharacterInfoTab firstTab = (CharacterInfoTab) getComponentAt(selectedIndex); restoreTab(firstTab, states.get(firstTab)); int oldSelectedIndex = getSelectedIndex(); setSelectedIndex(selectedIndex); if (oldSelectedIndex == selectedIndex) { handleDisplayAware(); } PriorityQueue<CharacterInfoTab> queue = new PriorityQueue<>(states.keySet().size(), this); queue.addAll(states.keySet()); queue.remove(firstTab); while (!queue.isEmpty()) { CharacterInfoTab infoTab = queue.poll(); ModelMap models = states.get(infoTab); restoreQueue.add(submit(new RestoreModelsTask(infoTab, models))); } } public void storeModels(Map<CharacterInfoTab, ModelMap> states) { while (!storeQueue.isEmpty()) { CharacterInfoTab infoTab = storeQueue.poll(); infoTab.storeModels(states.get(infoTab)); } } public void cancelRestoreTasks() { while (!restoreQueue.isEmpty()) { restoreQueue.poll().cancel(true); } } private class RestoreModelsTask implements Runnable { private final CharacterInfoTab infoTab; private final ModelMap models; private boolean executed; public RestoreModelsTask(CharacterInfoTab infoTab, ModelMap models) { this.infoTab = infoTab; this.models = models; this.executed = false; } @Override public void run() { try { SwingUtilities.invokeAndWait(new Runnable() { @Override public void run() { if (!executed) { restoreTab(infoTab, models); } } }); } catch (InterruptedException ex) { } catch (InvocationTargetException ex) { Logging.errorPrint(null, ex.getCause()); } finally { executed = true; } } } } private class TabActionListener implements PropertyChangeListener { private Component component; public TabActionListener(Component component) { this.component = component; } @Override public void propertyChange(PropertyChangeEvent evt) { int index = indexOfComponent(component); if (index < 0) { return; } String propName = evt.getPropertyName(); if (TabTitle.TITLE.equals(propName)) { InfoTabbedPane.this.setTitleAt(index, (String) evt.getNewValue()); } else if (TabTitle.ICON.equals(propName)) { InfoTabbedPane.this.setIconAt(index, (Icon) evt.getNewValue()); } else if (TabTitle.TOOLTIP.equals(propName)) { InfoTabbedPane.this.setToolTipTextAt(index, (String) evt.getNewValue()); } else if (TabTitle.ENABLED.equals(propName)) { InfoTabbedPane.this.setEnabledAt(index, (Boolean) evt.getNewValue()); } else if (TodoFacade.SWITCH_TABS.equals(propName)) { String destString = (String) evt.getNewValue(); String[] dest = destString.split("/"); //$NON-NLS-1$ switchTabsAndAdviseTodo(dest); } } } }