/*
* Copyright (C) 2010-2012 Klaus Reimer <k@ailis.de>
* See LICENSE.TXT for licensing information.
*/
package de.ailis.xadrian.components;
import java.awt.BorderLayout;
import java.awt.Font;
import java.awt.print.PrinterException;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JComponent;
import javax.swing.JOptionPane;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JTextPane;
import javax.swing.TransferHandler;
import javax.swing.UIManager;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.event.HyperlinkEvent;
import javax.swing.event.HyperlinkEvent.EventType;
import javax.swing.event.HyperlinkListener;
import javax.swing.text.html.HTMLDocument;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import de.ailis.xadrian.Main;
import de.ailis.xadrian.actions.AddFactoryAction;
import de.ailis.xadrian.actions.ChangePricesAction;
import de.ailis.xadrian.actions.ChangeSectorAction;
import de.ailis.xadrian.actions.ChangeSunsAction;
import de.ailis.xadrian.actions.CopyAction;
import de.ailis.xadrian.actions.SelectAllAction;
import de.ailis.xadrian.actions.ToggleBaseComplexAction;
import de.ailis.xadrian.data.Complex;
import de.ailis.xadrian.data.Factory;
import de.ailis.xadrian.data.Game;
import de.ailis.xadrian.data.Sector;
import de.ailis.xadrian.data.Ware;
import de.ailis.xadrian.dialogs.AddFactoryDialog;
import de.ailis.xadrian.dialogs.ChangePricesDialog;
import de.ailis.xadrian.dialogs.ChangeQuantityDialog;
import de.ailis.xadrian.dialogs.ChangeSunsDialog;
import de.ailis.xadrian.dialogs.SaveComplexDialog;
import de.ailis.xadrian.dialogs.SelectSectorDialog;
import de.ailis.xadrian.dialogs.SetYieldsDialog;
import de.ailis.xadrian.freemarker.TemplateFactory;
import de.ailis.xadrian.interfaces.ClipboardProvider;
import de.ailis.xadrian.interfaces.ComplexProvider;
import de.ailis.xadrian.interfaces.GameProvider;
import de.ailis.xadrian.interfaces.SectorProvider;
import de.ailis.xadrian.interfaces.StateProvider;
import de.ailis.xadrian.listeners.ClipboardStateListener;
import de.ailis.xadrian.listeners.EditorStateListener;
import de.ailis.xadrian.listeners.StateListener;
import de.ailis.xadrian.support.Config;
import de.ailis.xadrian.support.I18N;
import de.ailis.xadrian.support.ModalDialog.Result;
import de.ailis.xadrian.utils.FileUtils;
import de.ailis.xadrian.utils.SwingUtils;
import de.ailis.xadrian.utils.XmlUtils;
import freemarker.template.Template;
/**
* Complex Editor component.
*
* @author Klaus Reimer (k@ailis.de)
*/
public class ComplexEditor extends JComponent implements HyperlinkListener,
CaretListener, ClipboardProvider, ComplexProvider, SectorProvider,
GameProvider
{
/** Serial version UID */
private static final long serialVersionUID = -582597303446091577L;
/** The logger */
private static final Log log = LogFactory.getLog(ComplexEditor.class);
/** The freemarker template for the content */
private static final Template template = TemplateFactory
.getTemplate("complex.ftl");
/** The text pane */
private final JTextPane textPane;
/** The edited complex */
private final Complex complex;
/** The file under which this complex was last saved */
private File file;
/** True if this editor has unsaved changes */
private boolean changed = false;
/**
* Constructor
*
* @param complex
* The complex to edit
*/
public ComplexEditor(final Complex complex)
{
this(complex, null);
}
/**
* Constructor
*
* @param complex
* The complex to edit
* @param file
* The file from which the complex was loaded. Null if it not
* loaded from a file.
*/
public ComplexEditor(final Complex complex, final File file)
{
super();
setLayout(new BorderLayout());
this.complex = complex;
this.file = file;
// Create the text pane
this.textPane = new JTextPane();
this.textPane.setEditable(false);
this.textPane.setBorder(null);
this.textPane.setContentType("text/html");
this.textPane.setDoubleBuffered(true);
this.textPane.addHyperlinkListener(this);
this.textPane.addCaretListener(this);
// Create the popup menu for the text pane
final JPopupMenu popupMenu = new JPopupMenu();
popupMenu.add(new CopyAction(this));
popupMenu.add(new SelectAllAction(this));
popupMenu.addSeparator();
popupMenu.add(new AddFactoryAction(this));
popupMenu.add(new ChangeSectorAction(this.complex, this, "complex"));
popupMenu.add(new ChangeSunsAction(this));
popupMenu.add(new ChangePricesAction(this));
popupMenu.add(new JCheckBoxMenuItem(new ToggleBaseComplexAction(this)));
SwingUtils.setPopupMenu(this.textPane, popupMenu);
final HTMLDocument document =
(HTMLDocument) this.textPane.getDocument();
// Set the base URL of the text pane
document.setBase(Main.class.getResource("templates/"));
// Modify the body style so it matches the system font
final Font font = UIManager.getFont("Label.font");
final String bodyRule = "body { font-family: " + font.getFamily() +
"; font-size: " + font.getSize() + "pt; }";
document.getStyleSheet().addRule(bodyRule);
// Create the scroll pane
final JScrollPane scrollPane = new JScrollPane(this.textPane);
add(scrollPane);
// Redraw the content
redraw();
fireComplexState();
}
/**
* Adds an editor state listener.
*
* @param listener
* The editor state listener to add
*/
public void addStateListener(final EditorStateListener listener)
{
this.listenerList.add(EditorStateListener.class, listener);
}
/**
* Removes an editor state listener.
*
* @param listener
* The editor state listener to remove
*/
public void removeStateListener(final EditorStateListener listener)
{
this.listenerList.remove(EditorStateListener.class, listener);
}
/**
* Fire the editor changed event.
*/
private void fireState()
{
final Object[] listeners = this.listenerList.getListenerList();
for (int i = listeners.length - 2; i >= 0; i -= 2)
if (listeners[i] == EditorStateListener.class)
((EditorStateListener) listeners[i + 1])
.editorStateChanged(this);
}
/**
* Mark this editor as changed.
*/
private void doChange()
{
this.changed = true;
fireState();
fireComplexState();
}
/**
* Redraws the freemarker template.
*/
private void redraw()
{
final int c = this.textPane.getCaretPosition();
final Map<String, Object> model = new HashMap<String, Object>();
final Config config = Config.getInstance();
model.put("complex", this.complex);
model.put("print", false);
model.put("config", config);
final String content = TemplateFactory.processTemplate(template, model);
this.textPane.setText(content);
this.textPane.setCaretPosition(Math.min(this.textPane.getDocument()
.getLength() - 1, c));
this.textPane.requestFocus();
}
/**
* @see HyperlinkListener#hyperlinkUpdate(HyperlinkEvent)
*/
@Override
public void hyperlinkUpdate(final HyperlinkEvent e)
{
if (e.getEventType() != EventType.ACTIVATED) return;
final URL url = e.getURL();
final String protocol = url.getProtocol();
if ("file".equals(protocol))
{
final String action = url.getHost();
if ("addFactory".equals(action))
{
addFactory();
}
else if ("removeFactory".equals(action))
{
removeFactory(Integer.parseInt(url.getPath().substring(1)));
}
else if ("disableFactory".equals(action))
{
disableFactory(Integer.parseInt(url.getPath().substring(1)));
}
else if ("enableFactory".equals(action))
{
enableFactory(Integer.parseInt(url.getPath().substring(1)));
}
else if ("acceptFactory".equals(action))
{
acceptFactory(Integer.parseInt(url.getPath().substring(1)));
}
else if ("changeQuantity".equals(action))
{
changeQuantity(Integer.parseInt(url.getPath().substring(1)));
}
else if ("increaseQuantity".equals(action))
{
increaseQuantity(Integer.parseInt(url.getPath().substring(1)));
}
else if ("decreaseQuantity".equals(action))
{
decreaseQuantity(Integer.parseInt(url.getPath().substring(1)));
}
else if ("changeYield".equals(action))
{
changeYield(Integer.parseInt(url.getPath().substring(1)));
}
else if ("changeSuns".equals(action))
{
changeSuns();
}
else if ("changeSector".equals(action))
{
changeSector();
}
else if ("changePrice".equals(action))
{
changePrices(this.complex.getGame().getWareFactory().getWare(
url.getPath().substring(1)));
}
else if ("toggleShowingProductionStats".equals(action))
{
toggleShowingProductionStats();
}
else if ("toggleShowingStorageCapacities".equals(action))
{
toggleShowingStorageCapacities();
}
else if ("toggleShowingShoppingList".equals(action))
{
toggleShowingShoppingList();
}
else if ("toggleShowingComplexSetup".equals(action))
{
toggleShowingComplexSetup();
}
else if ("buildFactory".equals(action))
{
buildFactory(url.getPath().substring(1));
}
else if ("destroyFactory".equals(action))
{
destroyFactory(url.getPath().substring(1));
}
else if ("buildKit".equals(action))
{
buildKit();
}
else if ("destroyKit".equals(action))
{
destroyKit();
}
}
}
/**
* Adds a new factory to the complex.
*/
@Override
public void addFactory()
{
final AddFactoryDialog dialog =
this.complex.getGame().getAddFactoryDialog();
if (dialog.open() == Result.OK)
{
for (final Factory factory : dialog.getFactories())
{
this.complex.addFactory(factory);
}
doChange();
redraw();
}
}
/**
* Sets the sector.
*/
@Override
public void changeSector()
{
final SelectSectorDialog dialog =
this.complex.getGame().getSelectSectorDialog();
dialog.setSelected(this.complex.getSector());
if (dialog.open() == Result.OK)
{
this.complex.setSector(dialog.getSelected());
doChange();
redraw();
}
}
/**
* Toggles the display of the complex setup.
*/
public void toggleShowingComplexSetup()
{
this.complex.toggleShowingComplexSetup();
doChange();
redraw();
}
/**
* Builds the factory with the given id.
*
* @param id
* The ID of the factory to build
*/
public void buildFactory(final String id)
{
this.complex.buildFactory(id);
doChange();
redraw();
}
/**
* Destroys the factory with the given id.
*
* @param id
* The ID of the factory to destroy
*/
public void destroyFactory(final String id)
{
this.complex.destroyFactory(id);
doChange();
redraw();
}
/**
* Builds the factory with the given id.
*/
public void buildKit()
{
this.complex.buildKit();
doChange();
redraw();
}
/**
* Destroys a kit.
*/
public void destroyKit()
{
this.complex.destroyKit();
doChange();
redraw();
}
/**
* Toggles the display of production statistics.
*/
public void toggleShowingProductionStats()
{
this.complex.toggleShowingProductionStats();
doChange();
redraw();
}
/**
* Toggles the display of production statistics.
*/
public void toggleShowingStorageCapacities()
{
this.complex.toggleShowingStorageCapacities();
doChange();
redraw();
}
/**
* Toggles the display of the shopping list.
*/
public void toggleShowingShoppingList()
{
this.complex.toggleShowingShoppingList();
doChange();
redraw();
}
/**
* Removes the factory with the specified index.
*
* @param index
* The index of the factory to remove
*/
public void removeFactory(final int index)
{
this.complex.removeFactory(index);
doChange();
redraw();
}
/**
* Disables the factory with the specified index.
*
* @param index
* The index of the factory to disable
*/
public void disableFactory(final int index)
{
this.complex.disableFactory(index);
doChange();
redraw();
}
/**
* Enables the factory with the specified index.
*
* @param index
* The index of the factory to enable
*/
public void enableFactory(final int index)
{
this.complex.enableFactory(index);
doChange();
redraw();
}
/**
* Accepts an automatically created factory.
*
* @param index
* The index of the factory to accept
*/
public void acceptFactory(final int index)
{
this.complex.acceptFactory(index);
doChange();
redraw();
}
/**
* Changes the quantity of the factory with the specified index.
*
* @param index
* The index of the factory to change
*/
public void changeQuantity(final int index)
{
final ChangeQuantityDialog dialog = ChangeQuantityDialog.getInstance();
dialog.setQuantity(this.complex.getQuantity(index));
if (dialog.open() == Result.OK)
{
this.complex.setQuantity(index, dialog.getQuantity());
doChange();
redraw();
}
}
/**
* Increases the quantity of the factory with the specified index.
*
* @param index
* The index of the factory to change
*/
public void increaseQuantity(final int index)
{
if (this.complex.increaseQuantity(index))
{
doChange();
redraw();
}
}
/**
* Decreases the quantity of the factory with the specified index.
*
* @param index
* The index of the factory to change
*/
public void decreaseQuantity(final int index)
{
if (this.complex.decreaseQuantity(index))
{
doChange();
redraw();
}
}
/**
* Changes the yield of the factory with the specified index.
*
* @param index
* The index of the factory to change
*/
public void changeYield(final int index)
{
final Factory mineType = this.complex.getFactory(index);
final SetYieldsDialog dialog = new SetYieldsDialog(mineType);
dialog.setYields(this.complex.getYields(index));
dialog.setSector(this.complex.getSector());
if (dialog.open() == Result.OK)
{
this.complex.setYields(index, dialog.getYields());
this.complex.setSector(dialog.getSector());
doChange();
redraw();
}
}
/**
* Changes the suns.
*/
@Override
public void changeSuns()
{
final ChangeSunsDialog dialog =
this.complex.getGame().getChangeSunsDialog();
dialog.setSuns(this.complex.getSuns());
if (dialog.open() == Result.OK)
{
this.complex.setSuns(dialog.getSuns());
doChange();
redraw();
}
}
/**
* Saves the complex under the last saved file. If the file was not saved
* before then saveAs() is called instead.
*/
public void save()
{
if (this.file == null)
saveAs();
else
save(this.file);
}
/**
* Prompts for a file name and saves the complex there.
*/
public void saveAs()
{
final SaveComplexDialog dialog = SaveComplexDialog.getInstance();
dialog.setSelectedFile(getSuggestedFile());
File file = dialog.open();
if (file != null)
{
// Add file extension if none present
if (FileUtils.getExtension(file) == null)
file = new File(file.getPath() + ".x3c");
// Save the file if it does not yet exists are user confirms
// overwrite
if (!file.exists()
|| JOptionPane.showConfirmDialog(null, I18N
.getString("confirm.overwrite"), I18N
.getString("confirm.title"),
JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION)
{
save(file);
}
}
}
/**
* Save the complex in the specified file.
*
* @param file
* The file
*/
private void save(final File file)
{
try
{
XmlUtils.write(this.complex.toXML(), file);
this.file = file;
this.changed = false;
this.complex.setName(FileUtils.getNameWithoutExt(file));
redraw();
fireState();
fireComplexState();
}
catch (final IOException e)
{
JOptionPane.showMessageDialog(null, I18N.getString(
"error.cantWriteComplex", file), I18N
.getString("error.title"), JOptionPane.ERROR_MESSAGE);
log.error("Unable to save complex to file '" + file + "': " + e, e);
}
}
/**
* Returns the edited complex.
*
* @return The edited complex
*/
public Complex getComplex()
{
return this.complex;
}
/**
* Returns true if this editor has unsaved changes. False if not.
*
* @return True if this editor has unsaved changes. False if not.
*/
public boolean isChanged()
{
return this.changed;
}
/**
* Toggles the addition of automatically calculated base complex.
*/
@Override
public void toggleBaseComplex()
{
this.complex.toggleAddBaseComplex();
doChange();
redraw();
}
/**
* Prints the complex data
*/
public void print()
{
// Prepare model
final Map<String, Object> model = new HashMap<String, Object>();
model.put("complex", this.complex);
model.put("print", true);
model.put("config", Config.getInstance());
// Generate content
final String content = TemplateFactory.processTemplate(template, model);
// Put content into a text pane component
final JTextPane printPane = new JTextPane();
printPane.setContentType("text/html");
((HTMLDocument) printPane.getDocument()).setBase(Main.class
.getResource("templates/"));
printPane.setText(content);
// Print the text pane
try
{
printPane.print(null, null, true, null, Config.getInstance()
.getPrintAttributes(), true);
}
catch (final PrinterException e)
{
JOptionPane.showMessageDialog(null, I18N
.getString("error.cantPrint"), I18N
.getString("error.title"), JOptionPane.ERROR_MESSAGE);
log.error("Unable to print complex: " + e, e);
}
}
/**
* Returns true if this editor is new (and can be replaced with an other
* editor).
*
* @return True if editor is new
*/
public boolean isNew()
{
return !this.changed && this.file == null
&& this.complex.getFactories().size() == 0;
}
/**
* Updates the base complex
*/
public void updateBaseComplex()
{
this.complex.updateBaseComplex();
redraw();
}
/**
* @see javax.swing.event.CaretListener#caretUpdate(javax.swing.event.CaretEvent)
*/
@Override
public void caretUpdate(final CaretEvent e)
{
fireClipboardState();
}
/**
* Returns the selected text or null if none selected.
*
* @return The selected text or null if none
*/
public String getSelectedText()
{
return this.textPane.getSelectedText();
}
/**
* Copies the selected text into the clipboard.
*/
public void copySelection()
{
this.textPane.copy();
}
/**
* Selects all the text in the text pane.
*/
@Override
public void selectAll()
{
this.textPane.requestFocus();
this.textPane.selectAll();
fireClipboardState();
}
/**
* @see de.ailis.xadrian.interfaces.ClipboardProvider#canCopy()
*/
@Override
public boolean canCopy()
{
return this.textPane.getSelectedText() != null;
}
/**
* @see de.ailis.xadrian.interfaces.ClipboardProvider#canCut()
*/
@Override
public boolean canCut()
{
return false;
}
/**
* @see de.ailis.xadrian.interfaces.ClipboardProvider#canPaste()
*/
@Override
public boolean canPaste()
{
return false;
}
/**
* @see de.ailis.xadrian.interfaces.ClipboardProvider#copy()
*/
@Override
public void copy()
{
this.textPane.requestFocus();
this.textPane.copy();
}
/**
* @see de.ailis.xadrian.interfaces.ClipboardProvider#cut()
*/
@Override
public void cut()
{
this.textPane.requestFocus();
this.textPane.cut();
}
/**
* @see de.ailis.xadrian.interfaces.ClipboardProvider#paste()
*/
@Override
public void paste()
{
this.textPane.requestFocus();
this.textPane.paste();
}
/**
* @see de.ailis.xadrian.interfaces.ClipboardProvider#addClipboardStateListener(de.ailis.xadrian.listeners.ClipboardStateListener)
*/
@Override
public void
addClipboardStateListener(final ClipboardStateListener listener)
{
this.listenerList.add(ClipboardStateListener.class, listener);
}
/**
* @see de.ailis.xadrian.interfaces.ClipboardProvider#removeClipboardStateListener(de.ailis.xadrian.listeners.ClipboardStateListener)
*/
@Override
public void removeClipboardStateListener(
final ClipboardStateListener listener)
{
this.listenerList.remove(ClipboardStateListener.class, listener);
}
/**
* Fire the clipboard state changed event.
*/
private void fireClipboardState()
{
final Object[] listeners = this.listenerList.getListenerList();
for (int i = listeners.length - 2; i >= 0; i -= 2)
if (listeners[i] == ClipboardStateListener.class)
((ClipboardStateListener) listeners[i + 1])
.clipboardStateChanged(this);
}
/**
* @see de.ailis.xadrian.interfaces.ClipboardProvider#canSelectAll()
*/
@Override
public boolean canSelectAll()
{
return true;
}
/**
* @see de.ailis.xadrian.interfaces.ComplexProvider#canAddFactory()
*/
@Override
public boolean canAddFactory()
{
return true;
}
/**
* @see StateProvider#addStateListener(StateListener)
*/
@Override
public void addStateListener(final StateListener listener)
{
this.listenerList.add(StateListener.class, listener);
}
/**
* @see StateProvider#removeStateListener(StateListener)
*/
@Override
public void removeStateListener(final StateListener listener)
{
this.listenerList.remove(StateListener.class, listener);
}
/**
* Fire the complex state event.
*/
private void fireComplexState()
{
final Object[] listeners = this.listenerList.getListenerList();
for (int i = listeners.length - 2; i >= 0; i -= 2)
if (listeners[i] == StateListener.class)
((StateListener) listeners[i + 1]).stateChanged();
}
/**
* @see de.ailis.xadrian.interfaces.ComplexProvider#canChangeSuns()
*/
@Override
public boolean canChangeSuns()
{
return this.complex.getSector() == null;
}
/**
* @see de.ailis.xadrian.interfaces.ComplexProvider#canToggleBaseComplex()
*/
@Override
public boolean canToggleBaseComplex()
{
return true;
}
/**
* @see de.ailis.xadrian.interfaces.ComplexProvider#isAddBaseComplex()
*/
@Override
public boolean isAddBaseComplex()
{
return this.complex.isAddBaseComplex();
}
/**
* @see de.ailis.xadrian.interfaces.ComplexProvider#canChangeSector()
*/
@Override
public boolean canChangeSector()
{
return true;
}
/**
* Returns the file under which the currently edited complex could be saved.
*
* @return A suggested file name for saving.
*/
private File getSuggestedFile()
{
if (this.file != null) return this.file;
return new File(this.complex.getName() + ".x3c");
}
/**
* @see de.ailis.xadrian.interfaces.ComplexProvider#canChangePrices()
*/
@Override
public boolean canChangePrices()
{
return !this.complex.isEmpty();
}
/**
* Opens the change prices dialog. Focuses the specified ware (if not null).
*
* @param focusedWare
* The ware to focus (null for none)
*/
public void changePrices(final Ware focusedWare)
{
final ChangePricesDialog dialog =
this.complex.getGame().getChangePricesDialog();
dialog.setCustomPrices(this.complex.getCustomPrices());
dialog.setActiveWare(focusedWare);
if (dialog.open(this.complex) == Result.OK)
{
this.complex.setCustomPrices(dialog.getCustomPrices());
doChange();
redraw();
}
}
/**
* @see de.ailis.xadrian.interfaces.ComplexProvider#changePrices()
*/
@Override
public void changePrices()
{
changePrices(null);
}
/**
* @see de.ailis.xadrian.interfaces.SectorProvider#getSector()
*/
@Override
public Sector getSector()
{
return this.complex.getSector();
}
/**
* @see de.ailis.xadrian.interfaces.SectorProvider#setSector(de.ailis.xadrian.data.Sector)
*/
@Override
public void setSector(final Sector sector)
{
this.complex.setSector(sector);
doChange();
redraw();
}
/**
* @see de.ailis.xadrian.interfaces.GameProvider#getGame()
*/
@Override
public Game getGame()
{
return this.complex.getGame();
}
/**
* @see JComponent#setTransferHandler(TransferHandler)
*/
@Override
public void setTransferHandler(final TransferHandler transferHandler)
{
super.setTransferHandler(transferHandler);
this.textPane.setTransferHandler(transferHandler);
}
/**
* Returns the file from which the file was opened or to which it was
* saved.
*
* @return The complex file. Null if file has not been loaded from a while
* and it was not saved to a file yet.
*/
public File getFile()
{
return this.file;
}
}