/* * 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.widgets.outline; import com.trollworks.gcs.character.GURPSCharacter; import com.trollworks.gcs.common.DataFile; import com.trollworks.gcs.common.LoadState; import com.trollworks.gcs.feature.AttributeBonus; import com.trollworks.gcs.feature.ContainedWeightReduction; import com.trollworks.gcs.feature.CostReduction; import com.trollworks.gcs.feature.DRBonus; import com.trollworks.gcs.feature.Feature; import com.trollworks.gcs.feature.SkillBonus; import com.trollworks.gcs.feature.SpellBonus; import com.trollworks.gcs.feature.WeaponBonus; import com.trollworks.gcs.prereq.PrereqList; import com.trollworks.gcs.skill.SkillDefault; import com.trollworks.gcs.skill.Technique; import com.trollworks.gcs.template.Template; import com.trollworks.toolkit.io.xml.XMLNodeType; import com.trollworks.toolkit.io.xml.XMLReader; import com.trollworks.toolkit.io.xml.XMLWriter; import com.trollworks.toolkit.ui.image.StdImage; import com.trollworks.toolkit.ui.widget.outline.Column; import com.trollworks.toolkit.ui.widget.outline.Row; import com.trollworks.toolkit.utility.VersionException; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.TreeSet; /** A common row super-class for the model. */ public abstract class ListRow extends Row { private static final String ATTRIBUTE_OPEN = "open"; //$NON-NLS-1$ private static final String TAG_NOTES = "notes"; //$NON-NLS-1$ private static final String TAG_CATEGORIES = "categories"; //$NON-NLS-1$ private static final String TAG_CATEGORY = "category"; //$NON-NLS-1$ /** The data file the row is associated with. */ protected DataFile mDataFile; private ArrayList<Feature> mFeatures; private PrereqList mPrereqList; private ArrayList<SkillDefault> mDefaults; private boolean mIsSatisfied; private String mUnsatisfiedReason; private String mNotes; private TreeSet<String> mCategories; /** * Extracts any "nameable" portions of the buffer and puts their keys into the provided set. * * @param set The set to add the nameable keys to. * @param buffer The text to check for nameable portions. */ public static void extractNameables(HashSet<String> set, String buffer) { int first = buffer.indexOf('@'); int last = buffer.indexOf('@', first + 1); while (first != -1 && last != -1) { set.add(buffer.substring(first + 1, last)); first = buffer.indexOf('@', last + 1); last = buffer.indexOf('@', first + 1); } } /** * Names any "nameable" portions of the data and returns the resulting string. * * @param map The map of nameable keys to names. * @param data The data to change. * @return The revised string. */ public static String nameNameables(HashMap<String, String> map, String data) { int first = data.indexOf('@'); int last = data.indexOf('@', first + 1); StringBuilder buffer = new StringBuilder(); while (first != -1 && last != -1) { String key = data.substring(first + 1, last); String replacement = map.get(key); if (first != 0) { buffer.append(data.substring(0, first)); } if (replacement != null) { buffer.append(replacement); } else { buffer.append('@'); buffer.append(key); buffer.append('@'); } if (last + 1 != data.length()) { data = data.substring(last + 1); } else { data = ""; //$NON-NLS-1$ } first = data.indexOf('@'); last = data.indexOf('@', first + 1); } buffer.append(data); return buffer.toString(); } /** * Creates a new data row. * * @param dataFile The data file to associate it with. * @param isContainer Whether or not this row allows children. */ public ListRow(DataFile dataFile, boolean isContainer) { super(); setCanHaveChildren(isContainer); setOpen(isContainer); mDataFile = dataFile; mFeatures = new ArrayList<>(); mPrereqList = new PrereqList(null, true); mDefaults = new ArrayList<>(); mIsSatisfied = true; mNotes = ""; //$NON-NLS-1$ mCategories = new TreeSet<>(); } /** * Creates a clone of an existing data row and associates it with the specified data file. * * @param dataFile The data file to associate it with. * @param rowToClone The data row to clone. */ public ListRow(DataFile dataFile, ListRow rowToClone) { this(dataFile, rowToClone.canHaveChildren()); setOpen(rowToClone.isOpen()); mNotes = rowToClone.mNotes; for (Feature feature : rowToClone.mFeatures) { mFeatures.add(feature.cloneFeature()); } mPrereqList = new PrereqList(null, rowToClone.getPrereqs()); mDefaults = new ArrayList<>(); for (SkillDefault skillDefault : rowToClone.mDefaults) { mDefaults.add(new SkillDefault(skillDefault)); } mCategories = new TreeSet<>(rowToClone.mCategories); } /** * @param obj The other object to compare against. * @return Whether or not this {@link ListRow} is equivalent. */ public boolean isEquivalentTo(Object obj) { if (obj == this) { return true; } if (obj instanceof ListRow) { ListRow row = (ListRow) obj; if (mNotes.equals(row.mNotes) && mCategories.equals(row.mCategories)) { if (mDefaults.equals(row.mDefaults)) { if (mPrereqList.equals(row.mPrereqList)) { if (mFeatures.equals(row.mFeatures)) { int childCount = getChildCount(); if (childCount == row.getChildCount()) { for (int i = 0; i < childCount; i++) { if (!((ListRow) getChild(i)).isEquivalentTo(row.getChild(i))) { return false; } } return true; } } } } } } return false; } /** @return Creates a detailed editor for this row. */ public abstract RowEditor<? extends ListRow> createEditor(); /** @return The localized name for this row object. */ public abstract String getLocalizedName(); @Override public boolean addChild(Row row) { boolean result = super.addChild(row); if (result) { notifySingle(getListChangedID()); } return result; } /** @return The ID for the "list changed" notification. */ public abstract String getListChangedID(); /** @return The XML root container tag name for this particular row. */ public abstract String getXMLTagName(); /** @return The most recent version of the XML tag this object knows how to load. */ public abstract int getXMLTagVersion(); /** @return The type of row. */ public abstract String getRowType(); /** @return Whether or not this row's prerequisites are currently satisfied. */ public boolean isSatisfied() { return mIsSatisfied; } /** @param satisfied Whether or not this row's prerequisites are currently satisfied. */ public void setSatisfied(boolean satisfied) { mIsSatisfied = satisfied; if (satisfied) { mUnsatisfiedReason = null; } } /** @return The reason {@link #isSatisfied()} is returning <code>false</code>. */ public String getReasonForUnsatisfied() { return mUnsatisfiedReason; } /** @param reason The reason {@link #isSatisfied()} is returning <code>false</code>. */ public void setReasonForUnsatisfied(String reason) { mUnsatisfiedReason = reason; } /** * Loads this row's contents. * * @param reader The XML reader to load from. * @param state The {@link LoadState} to use. */ public final void load(XMLReader reader, LoadState state) throws IOException { String marker = reader.getMarker(); state.mDataItemVersion = reader.getAttributeAsInteger(LoadState.ATTRIBUTE_VERSION, 0); if (state.mDataItemVersion > getXMLTagVersion()) { throw VersionException.createTooNew(); } prepareForLoad(state); loadAttributes(reader, state); do { if (reader.next() == XMLNodeType.START_TAG) { String name = reader.getName(); if (AttributeBonus.TAG_ROOT.equals(name)) { mFeatures.add(new AttributeBonus(reader)); } else if (DRBonus.TAG_ROOT.equals(name)) { mFeatures.add(new DRBonus(reader)); } else if (SkillBonus.TAG_ROOT.equals(name)) { mFeatures.add(new SkillBonus(reader)); } else if (SpellBonus.TAG_ROOT.equals(name)) { mFeatures.add(new SpellBonus(reader)); } else if (WeaponBonus.TAG_ROOT.equals(name)) { mFeatures.add(new WeaponBonus(reader)); } else if (CostReduction.TAG_ROOT.equals(name)) { mFeatures.add(new CostReduction(reader)); } else if (ContainedWeightReduction.TAG_ROOT.equals(name)) { mFeatures.add(new ContainedWeightReduction(reader)); } else if (PrereqList.TAG_ROOT.equals(name)) { mPrereqList = new PrereqList(null, reader); } else if (!(this instanceof Technique) && SkillDefault.TAG_ROOT.equals(name)) { mDefaults.add(new SkillDefault(reader)); } else if (TAG_NOTES.equals(name)) { mNotes = reader.readText(); } else if (TAG_CATEGORIES.equals(name)) { String subMarker = reader.getMarker(); do { if (reader.next() == XMLNodeType.START_TAG) { name = reader.getName(); if (TAG_CATEGORY.equals(name)) { mCategories.add(reader.readText()); } else { reader.skipTag(name); } } } while (reader.withinMarker(subMarker)); } else { loadSubElement(reader, state); } } } while (reader.withinMarker(marker)); finishedLoading(state); } /** * Called to prepare the row for loading. * * @param state The {@link LoadState} to use. */ protected void prepareForLoad(LoadState state) { mNotes = ""; //$NON-NLS-1$ mFeatures.clear(); mDefaults.clear(); mPrereqList = new PrereqList(null, true); mCategories.clear(); } /** * Loads this row's custom attributes from the specified element. * * @param reader The XML reader to load from. * @param state The {@link LoadState} to use. */ protected void loadAttributes(XMLReader reader, LoadState state) { if (canHaveChildren()) { setOpen(reader.isAttributeSet(ATTRIBUTE_OPEN)); } } /** * Loads this row's custom data from the specified element. * * @param reader The XML reader to load from. * @param state The {@link LoadState} to use. */ @SuppressWarnings("static-method") protected void loadSubElement(XMLReader reader, LoadState state) throws IOException { reader.skipTag(reader.getName()); } /** * Called when loading of this row is complete. Does nothing by default. * * @param state The {@link LoadState} to use. */ protected void finishedLoading(LoadState state) { // Nothing to do. } /** * Saves the row. * * @param out The XML writer to use. * @param forUndo Whether this is being called to save undo state. */ public void save(XMLWriter out, boolean forUndo) { out.startTag(getXMLTagName()); out.writeAttribute(LoadState.ATTRIBUTE_VERSION, getXMLTagVersion()); if (canHaveChildren()) { out.writeAttribute(ATTRIBUTE_OPEN, isOpen()); } saveAttributes(out, forUndo); out.finishTagEOL(); saveSelf(out, forUndo); out.simpleTagNotEmpty(TAG_NOTES, mNotes); if (!mCategories.isEmpty()) { out.startSimpleTagEOL(TAG_CATEGORIES); for (String category : mCategories) { out.simpleTag(TAG_CATEGORY, category); } out.endTagEOL(TAG_CATEGORIES, true); } if (!mFeatures.isEmpty()) { for (Feature feature : mFeatures) { feature.save(out); } } mPrereqList.save(out); if (!(this instanceof Technique) && !mDefaults.isEmpty()) { for (SkillDefault skillDefault : mDefaults) { skillDefault.save(out); } } if (!forUndo && canHaveChildren()) { for (Row row : getChildren()) { ((ListRow) row).save(out, false); } } out.endTagEOL(getXMLTagName(), true); } /** * Saves the row. * * @param out The XML writer to use. * @param forUndo Whether this is being called to save undo state. */ protected abstract void saveSelf(XMLWriter out, boolean forUndo); /** * Saves extra attributes of the row, if any. * * @param out The XML writer to use. * @param forUndo Whether this is being called to save undo state. */ protected void saveAttributes(XMLWriter out, boolean forUndo) { // Does nothing by default. } /** * Starts the notification process. Should be called before calling * {@link #notify(String,Object)}. */ protected final void startNotify() { if (mDataFile != null) { mDataFile.startNotify(); } } /** * Sends a notification to all interested consumers. * * @param type The notification type. * @param data Extra data specific to this notification. */ public void notify(String type, Object data) { if (mDataFile != null) { mDataFile.notify(type, this); } } /** * Sends a notification to all interested consumers. * * @param type The notification type. */ public final void notifySingle(String type) { if (mDataFile != null) { mDataFile.notifySingle(type, this); } } /** * Ends the notification process. Must be called after calling {@link #notify(String,Object)}. */ public void endNotify() { if (mDataFile != null) { mDataFile.endNotify(); } } /** Called to update any information that relies on children. */ public void update() { // Do nothing by default. } /** @return The owning data file. */ public DataFile getDataFile() { return mDataFile; } /** @return The owning template. */ public Template getTemplate() { return mDataFile instanceof Template ? (Template) mDataFile : null; } /** @return The owning character. */ public GURPSCharacter getCharacter() { return mDataFile instanceof GURPSCharacter ? (GURPSCharacter) mDataFile : null; } /** @return The features provided by this data row. */ public List<Feature> getFeatures() { return Collections.unmodifiableList(mFeatures); } /** * @param features The new features of this data row. * @return Whether there was a change or not. */ public boolean setFeatures(List<Feature> features) { if (!mFeatures.equals(features)) { mFeatures = new ArrayList<>(features); return true; } return false; } /** @return The categories this data row belongs to. */ public Set<String> getCategories() { return Collections.unmodifiableSet(mCategories); } /** @return The categories this data row belongs to. */ public String getCategoriesAsString() { StringBuilder buffer = new StringBuilder(); for (String category : mCategories) { if (buffer.length() > 0) { buffer.append(", "); //$NON-NLS-1$ } buffer.append(category); } return buffer.toString(); } /** * @param categories The categories this data row belongs to. * @return Whether there was a change or not. */ public boolean setCategories(Collection<String> categories) { TreeSet<String> old = mCategories; mCategories = new TreeSet<>(); for (String category : categories) { category = category.trim(); if (category.length() > 0) { mCategories.add(category); } } if (!old.equals(mCategories)) { String id = getCategoryID(); if (id != null) { notifySingle(id); } return true; } return false; } /** * @param categories The categories this data row belongs to. Use commas to separate categories. * @return Whether there was a change or not. */ public final boolean setCategories(String categories) { return setCategories(Arrays.asList(categories.split(","))); //$NON-NLS-1$ } /** * @param category The category to add. * @return Whether there was a change or not. */ public boolean addCategory(String category) { category = category.trim(); if (category.length() > 0) { if (mCategories.add(category)) { String id = getCategoryID(); if (id != null) { notifySingle(id); } return true; } } return false; } /** @return The notification ID to use with categories. */ @SuppressWarnings("static-method") protected String getCategoryID() { return null; } /** @return The prerequisites needed by this data row. */ public PrereqList getPrereqs() { return mPrereqList; } /** * @param prereqs The new prerequisites needed by this data row. * @return Whether there was a change or not. */ public boolean setPrereqs(PrereqList prereqs) { if (!mPrereqList.equals(prereqs)) { mPrereqList = (PrereqList) prereqs.clone(null); return true; } return false; } /** @return The defaults for this row. */ public List<SkillDefault> getDefaults() { return Collections.unmodifiableList(mDefaults); } /** * @param defaults The new defaults for this row. * @return Whether there was a change or not. */ public boolean setDefaults(List<SkillDefault> defaults) { if (!mDefaults.equals(defaults)) { mDefaults = new ArrayList<>(defaults); return true; } return false; } @Override public final void setData(Column column, Object data) { // Not used. } /** * @param text The text to search for. * @param lowerCaseOnly The passed in text is all lowercase. * @return <code>true</code> if this row contains the text. */ @SuppressWarnings("static-method") public boolean contains(String text, boolean lowerCaseOnly) { return false; } /** * @param large Whether to return the small (16x16) or large (32x32) image. * @return An image representative of this row. */ public abstract StdImage getIcon(boolean large); /** @param set The nameable keys. */ public void fillWithNameableKeys(HashSet<String> set) { extractNameables(set, mNotes); for (SkillDefault def : mDefaults) { def.fillWithNameableKeys(set); } for (Feature feature : mFeatures) { feature.fillWithNameableKeys(set); } mPrereqList.fillWithNameableKeys(set); } /** @param map The map of nameable keys to names to apply. */ public void applyNameableKeys(HashMap<String, String> map) { mNotes = nameNameables(map, mNotes); for (SkillDefault def : mDefaults) { def.applyNameableKeys(map); } for (Feature feature : mFeatures) { feature.applyNameableKeys(map); } mPrereqList.applyNameableKeys(map); } /** @return The notes. */ public String getNotes() { return mNotes; } /** @return The notes due to modifiers. */ @SuppressWarnings("static-method") public String getModifierNotes() { return ""; //$NON-NLS-1$ } /** * @param notes The notes to set. * @return Whether it was changed. */ public boolean setNotes(String notes) { if (!mNotes.equals(notes)) { mNotes = notes; return true; } return false; } }