Java tutorial
/* * 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; } }