/*
* 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.character;
import com.trollworks.gcs.advantage.Advantage;
import com.trollworks.gcs.common.CommonDockable;
import com.trollworks.gcs.equipment.Equipment;
import com.trollworks.gcs.notes.Note;
import com.trollworks.gcs.preferences.SheetPreferences;
import com.trollworks.gcs.skill.Skill;
import com.trollworks.gcs.skill.Technique;
import com.trollworks.gcs.spell.Spell;
import com.trollworks.gcs.widgets.outline.ListOutline;
import com.trollworks.gcs.widgets.outline.ListRow;
import com.trollworks.gcs.widgets.outline.RowItemRenderer;
import com.trollworks.gcs.widgets.outline.RowPostProcessor;
import com.trollworks.toolkit.annotation.Localize;
import com.trollworks.toolkit.ui.UIUtilities;
import com.trollworks.toolkit.ui.menu.RetargetableFocus;
import com.trollworks.toolkit.ui.scale.Scales;
import com.trollworks.toolkit.ui.widget.Toolbar;
import com.trollworks.toolkit.ui.widget.WindowUtils;
import com.trollworks.toolkit.ui.widget.dock.Dock;
import com.trollworks.toolkit.ui.widget.outline.Outline;
import com.trollworks.toolkit.ui.widget.outline.OutlineModel;
import com.trollworks.toolkit.ui.widget.outline.Row;
import com.trollworks.toolkit.ui.widget.outline.RowIterator;
import com.trollworks.toolkit.ui.widget.search.Search;
import com.trollworks.toolkit.ui.widget.search.SearchTarget;
import com.trollworks.toolkit.utility.FileType;
import com.trollworks.toolkit.utility.Localization;
import com.trollworks.toolkit.utility.PathUtils;
import com.trollworks.toolkit.utility.PrintProxy;
import com.trollworks.toolkit.utility.notification.NotifierTarget;
import com.trollworks.toolkit.utility.undo.StdUndoManager;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.EventQueue;
import java.awt.KeyboardFocusManager;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import javax.swing.JComboBox;
import javax.swing.JScrollPane;
import javax.swing.JViewport;
import javax.swing.ListCellRenderer;
import javax.swing.undo.StateEdit;
/** A list of advantages and disadvantages from a library. */
public class SheetDockable extends CommonDockable implements SearchTarget, RetargetableFocus, NotifierTarget {
@Localize("Untitled Sheet")
@Localize(locale = "de", value = "Unbenanntes Charakterblatt")
@Localize(locale = "ru", value = "Лист без названия")
@Localize(locale = "es", value = "Hoja sin título")
private static String UNTITLED;
@Localize("An error occurred while trying to export the sheet as a PNG.")
private static String EXPORT_PNG_ERROR;
@Localize("An error occurred while trying to export the sheet as a PDF.")
private static String EXPORT_PDF_ERROR;
@Localize("An error occurred while trying to export the sheet using the text template.")
private static String EXPORT_TEXT_TEMPLATE_ERROR;
@Localize("Add Rows")
@Localize(locale = "de", value = "Zeilen hinzufügen")
@Localize(locale = "ru", value = "Добавить строки")
@Localize(locale = "es", value = "Añadir filas")
private static String ADD_ROWS;
@Localize(".%s Files")
private static String TEXT_TEMPLATE_DESCRIPTION;
static {
Localization.initialize();
}
private static SheetDockable LAST_ACTIVATED;
private CharacterSheet mSheet;
private Toolbar mToolbar;
private JComboBox<Scales> mScaleCombo;
private Search mSearch;
private JComboBox<HitLocationTable> mHitLocationTableCombo;
private PrerequisitesThread mPrereqThread;
/** Creates a new {@link SheetDockable}. */
public SheetDockable(GURPSCharacter character) {
super(character);
GURPSCharacter dataFile = getDataFile();
mSheet = new CharacterSheet(dataFile);
createToolbar();
JScrollPane scroller = new JScrollPane(mSheet);
scroller.setBorder(null);
JViewport viewport = scroller.getViewport();
viewport.setBackground(Color.LIGHT_GRAY);
viewport.addChangeListener(mSheet);
add(scroller, BorderLayout.CENTER);
mSheet.rebuild();
mPrereqThread = new PrerequisitesThread(mSheet);
mPrereqThread.start();
PrerequisitesThread.waitForProcessingToFinish(dataFile);
dataFile.setModified(false);
StdUndoManager undoManager = getUndoManager();
undoManager.discardAllEdits();
dataFile.setUndoManager(undoManager);
dataFile.addTarget(this, Profile.ID_BODY_TYPE);
}
private void createToolbar() {
mToolbar = new Toolbar();
mScaleCombo = new JComboBox<>(Scales.values());
mScaleCombo.setSelectedItem(SheetPreferences.getInitialUIScale());
mScaleCombo.addActionListener((event) -> mSheet.setScale(((Scales) mScaleCombo.getSelectedItem()).getScale()));
mToolbar.add(mScaleCombo);
mSearch = new Search(this);
mToolbar.add(mSearch, Toolbar.LAYOUT_FILL);
mHitLocationTableCombo = new JComboBox<>(HitLocationTable.ALL);
mHitLocationTableCombo.setSelectedItem(getDataFile().getDescription().getHitLocationTable());
mHitLocationTableCombo.addActionListener((event) -> getDataFile().getDescription().setHitLocationTable((HitLocationTable) mHitLocationTableCombo.getSelectedItem()));
mToolbar.add(mHitLocationTableCombo);
add(mToolbar, BorderLayout.NORTH);
}
@Override
public boolean attemptClose() {
boolean closed = super.attemptClose();
if (closed) {
mSheet.dispose();
}
return closed;
}
@Override
public Component getRetargetedFocus() {
return mSheet;
}
/** @return The last activated {@link SheetDockable}. */
public static SheetDockable getLastActivated() {
if (LAST_ACTIVATED != null) {
Dock dock = UIUtilities.getAncestorOfType(LAST_ACTIVATED, Dock.class);
if (dock == null) {
LAST_ACTIVATED = null;
}
}
return LAST_ACTIVATED;
}
@Override
public void activated() {
super.activated();
LAST_ACTIVATED = this;
}
@Override
public GURPSCharacter getDataFile() {
return (GURPSCharacter) super.getDataFile();
}
/** @return The {@link CharacterSheet}. */
public CharacterSheet getSheet() {
return mSheet;
}
@Override
protected String getUntitledBaseName() {
return UNTITLED;
}
@Override
public PrintProxy getPrintProxy() {
return mSheet;
}
@Override
public String getDescriptor() {
// RAW: Implement
return null;
}
@Override
public FileType[] getAllowedFileTypes() {
File textTemplate = TextTemplate.resolveTextTemplate(null);
String extension = PathUtils.getExtension(textTemplate.getName());
FileType templateType = FileType.getByExtension(extension);
if (templateType == null) {
FileType.register(extension, null, String.format(TEXT_TEMPLATE_DESCRIPTION, extension), "", null, false, false); //$NON-NLS-1$
}
templateType = FileType.getByExtension(extension);
return new FileType[] { FileType.getByExtension(GURPSCharacter.EXTENSION), FileType.getByExtension(FileType.PDF_EXTENSION), FileType.getByExtension(FileType.PNG_EXTENSION), templateType };
}
@Override
public String getPreferredSavePath() {
String name = getDataFile().getDescription().getName();
if (name.length() == 0) {
name = getTitle();
}
return PathUtils.getFullPath(PathUtils.getParent(PathUtils.getFullPath(getBackingFile())), name);
}
@Override
public File[] saveTo(File file) {
ArrayList<File> result = new ArrayList<>();
String extension = PathUtils.getExtension(file.getName());
File textTemplate = TextTemplate.resolveTextTemplate(null);
String templateExtension = PathUtils.getExtension(textTemplate.getName());
if (FileType.PNG_EXTENSION.equals(extension)) {
if (!mSheet.saveAsPNG(file, result)) {
WindowUtils.showError(this, EXPORT_PNG_ERROR);
}
} else if (FileType.PDF_EXTENSION.equals(extension)) {
if (mSheet.saveAsPDF(file)) {
result.add(file);
} else {
WindowUtils.showError(this, EXPORT_PDF_ERROR);
}
} else if (templateExtension.equals(extension)) {
if (new TextTemplate(mSheet).export(file, null)) {
result.add(file);
} else {
WindowUtils.showError(this, EXPORT_TEXT_TEMPLATE_ERROR);
}
} else {
return super.saveTo(file);
}
return result.toArray(new File[result.size()]);
}
@Override
public boolean isJumpToSearchAvailable() {
return mSearch.isEnabled() && mSearch != KeyboardFocusManager.getCurrentKeyboardFocusManager().getPermanentFocusOwner();
}
@Override
public void jumpToSearchField() {
mSearch.requestFocusInWindow();
}
@Override
public ListCellRenderer<Object> getSearchRenderer() {
return new RowItemRenderer();
}
@Override
public List<Object> search(String filter) {
ArrayList<Object> list = new ArrayList<>();
filter = filter.toLowerCase();
searchOne(mSheet.getAdvantageOutline(), filter, list);
searchOne(mSheet.getSkillOutline(), filter, list);
searchOne(mSheet.getSpellOutline(), filter, list);
searchOne(mSheet.getEquipmentOutline(), filter, list);
searchOne(mSheet.getNoteOutline(), filter, list);
return list;
}
private static void searchOne(ListOutline outline, String text, ArrayList<Object> list) {
for (ListRow row : new RowIterator<ListRow>(outline.getModel())) {
if (row.contains(text, true)) {
list.add(row);
}
}
}
@Override
public void searchSelect(List<Object> selection) {
HashMap<OutlineModel, ArrayList<Row>> map = new HashMap<>();
Outline primary = null;
ArrayList<Row> list;
mSheet.getAdvantageOutline().getModel().deselect();
mSheet.getSkillOutline().getModel().deselect();
mSheet.getSpellOutline().getModel().deselect();
mSheet.getEquipmentOutline().getModel().deselect();
mSheet.getNoteOutline().getModel().deselect();
for (Object obj : selection) {
Row row = (Row) obj;
Row parent = row.getParent();
OutlineModel model = row.getOwner();
while (parent != null) {
parent.setOpen(true);
model = parent.getOwner();
parent = parent.getParent();
}
list = map.get(model);
if (list == null) {
list = new ArrayList<>();
list.add(row);
map.put(model, list);
} else {
list.add(row);
}
if (primary == null) {
primary = mSheet.getAdvantageOutline();
if (model != primary.getModel()) {
primary = mSheet.getSkillOutline();
if (model != primary.getModel()) {
primary = mSheet.getSpellOutline();
if (model != primary.getModel()) {
primary = mSheet.getEquipmentOutline();
if (model != primary.getModel()) {
primary = mSheet.getNoteOutline();
if (model != primary.getModel()) {
primary = null;
}
}
}
}
}
}
}
for (OutlineModel model : map.keySet()) {
model.select(map.get(model), false);
}
if (primary != null) {
final Outline outline = primary;
EventQueue.invokeLater(() -> outline.scrollSelectionIntoView());
primary.requestFocus();
}
}
/**
* Adds rows to the sheet.
*
* @param rows The rows to add.
*/
public void addRows(List<Row> rows) {
HashMap<ListOutline, StateEdit> map = new HashMap<>();
HashMap<Outline, ArrayList<Row>> selMap = new HashMap<>();
HashMap<Outline, ArrayList<ListRow>> nameMap = new HashMap<>();
ListOutline outline = null;
for (Row row : rows) {
if (row instanceof Advantage) {
outline = mSheet.getAdvantageOutline();
if (!map.containsKey(outline)) {
map.put(outline, new StateEdit(outline.getModel(), ADD_ROWS));
}
row = new Advantage(getDataFile(), (Advantage) row, true);
addCompleteRow(outline, row, selMap);
} else if (row instanceof Technique) {
outline = mSheet.getSkillOutline();
if (!map.containsKey(outline)) {
map.put(outline, new StateEdit(outline.getModel(), ADD_ROWS));
}
row = new Technique(getDataFile(), (Technique) row, true);
addCompleteRow(outline, row, selMap);
} else if (row instanceof Skill) {
outline = mSheet.getSkillOutline();
if (!map.containsKey(outline)) {
map.put(outline, new StateEdit(outline.getModel(), ADD_ROWS));
}
row = new Skill(getDataFile(), (Skill) row, true, true);
addCompleteRow(outline, row, selMap);
} else if (row instanceof Spell) {
outline = mSheet.getSpellOutline();
if (!map.containsKey(outline)) {
map.put(outline, new StateEdit(outline.getModel(), ADD_ROWS));
}
row = new Spell(getDataFile(), (Spell) row, true, true);
addCompleteRow(outline, row, selMap);
} else if (row instanceof Equipment) {
outline = mSheet.getEquipmentOutline();
if (!map.containsKey(outline)) {
map.put(outline, new StateEdit(outline.getModel(), ADD_ROWS));
}
row = new Equipment(getDataFile(), (Equipment) row, true);
addCompleteRow(outline, row, selMap);
} else if (row instanceof Note) {
outline = mSheet.getNoteOutline();
if (!map.containsKey(outline)) {
map.put(outline, new StateEdit(outline.getModel(), ADD_ROWS));
}
row = new Note(getDataFile(), (Note) row, true);
addCompleteRow(outline, row, selMap);
} else {
row = null;
}
if (row instanceof ListRow) {
ArrayList<ListRow> process = nameMap.get(outline);
if (process == null) {
process = new ArrayList<>();
nameMap.put(outline, process);
}
addRowsToBeProcessed(process, (ListRow) row);
}
}
for (ListOutline anOutline : map.keySet()) {
OutlineModel model = anOutline.getModel();
model.select(selMap.get(anOutline), false);
StateEdit edit = map.get(anOutline);
edit.end();
anOutline.postUndo(edit);
anOutline.scrollSelectionIntoView();
anOutline.requestFocus();
}
if (!nameMap.isEmpty()) {
EventQueue.invokeLater(new RowPostProcessor(nameMap));
}
}
private void addRowsToBeProcessed(ArrayList<ListRow> list, ListRow row) {
int count = row.getChildCount();
list.add(row);
for (int i = 0; i < count; i++) {
addRowsToBeProcessed(list, (ListRow) row.getChild(i));
}
}
private void addCompleteRow(Outline outline, Row row, HashMap<Outline, ArrayList<Row>> selMap) {
ArrayList<Row> selection = selMap.get(outline);
addCompleteRow(outline.getModel(), row);
outline.contentSizeMayHaveChanged();
if (selection == null) {
selection = new ArrayList<>();
selMap.put(outline, selection);
}
selection.add(row);
}
private void addCompleteRow(OutlineModel outlineModel, Row row) {
outlineModel.addRow(row);
if (row.isOpen() && row.hasChildren()) {
for (Row child : row.getChildren()) {
addCompleteRow(outlineModel, child);
}
}
}
/** Notify background threads of prereq or feature modifications. */
public void notifyOfPrereqOrFeatureModification() {
mPrereqThread.markForUpdate();
}
@Override
public int getNotificationPriority() {
return 0;
}
@Override
public void handleNotification(Object producer, String name, Object data) {
if (Profile.ID_BODY_TYPE.equals(name)) {
mHitLocationTableCombo.setSelectedItem(getDataFile().getDescription().getHitLocationTable());
}
}
}