/* * Copyright (c) 2014 tabletoptool.com team. * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Public License v3.0 * which accompanies this distribution, and is available at * http://www.gnu.org/licenses/gpl.html * * Contributors: * rptools.com team - initial implementation * tabletoptool.com team - further development */ package com.t3.client.ui.campaignproperties; import java.awt.Color; import java.awt.Dimension; import java.awt.EventQueue; import java.awt.GridLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.LineNumberReader; import java.io.StringReader; import java.text.ParseException; import java.util.Arrays; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.TreeMap; import javax.swing.AbstractAction; import javax.swing.DefaultListModel; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JEditorPane; import javax.swing.JFileChooser; import javax.swing.JFrame; import javax.swing.JList; import javax.swing.JTextArea; import javax.swing.JTextField; import javax.swing.KeyStroke; import com.jeta.forms.components.panel.FormPanel; import com.t3.client.TabletopTool; import com.t3.client.ui.zone.ZoneRenderer; import com.t3.guid.GUID; import com.t3.language.I18N; import com.t3.model.AssetManager; import com.t3.model.Light; import com.t3.model.LightSource; import com.t3.model.ShapeType; import com.t3.model.SightType; import com.t3.model.campaign.Campaign; import com.t3.model.campaign.CampaignProperties; import com.t3.model.drawing.DrawableColorPaint; import com.t3.persistence.PersistenceUtil; import com.t3.swing.SwingUtil; import com.t3.util.StringUtil; public class CampaignPropertiesDialog extends JDialog { public enum Status { OK, CANCEL } private TokenPropertiesManagementPanel tokenPropertiesPanel; private TokenStatesController tokenStatesController; private TokenBarController tokenBarController; private Status status; private FormPanel formPanel; private Campaign campaign; public CampaignPropertiesDialog(JFrame owner) { super(owner, "Campaign Properties", true); setMinimumSize(new Dimension(450, 450)); // These sizes mess up my custom LAF settings. :( initialize(); } public Status getStatus() { return status; } @Override public void setVisible(boolean b) { if (b) { SwingUtil.centerOver(this, TabletopTool.getFrame()); } else { TabletopTool.getFrame().repaint(); } super.setVisible(b); } private void initialize() { setLayout(new GridLayout()); formPanel = new FormPanel("com/t3/client/ui/forms/campaignPropertiesDialog.xml"); initTokenPropertiesDialog(formPanel); tokenStatesController = new TokenStatesController(formPanel); tokenBarController = new TokenBarController(formPanel); tokenBarController.setNames(tokenStatesController.getNames()); initOKButton(); initCancelButton(); initAddRepoButton(); initAddGalleryIndexButton(); initDeleteRepoButton(); initImportButton(); initExportButton(); add(formPanel); // Escape key formPanel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "cancel"); formPanel.getActionMap().put("cancel", new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { cancel(); } }); getRootPane().setDefaultButton(getOKButton()); } private void initTokenPropertiesDialog(FormPanel panel) { tokenPropertiesPanel = new TokenPropertiesManagementPanel(); panel.getFormAccessor("propertiesPanel").replaceBean("tokenPropertiesPanel", tokenPropertiesPanel); panel.reset(); } public JTextField getNewServerTextField() { return formPanel.getTextField("newServer"); } private void initAddRepoButton() { JButton button = (JButton) formPanel.getButton("addRepoButton"); button.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { String newRepo = getNewServerTextField().getText(); if (newRepo == null || newRepo.length() == 0) { return; } // TODO: Check for uniqueness ((DefaultListModel<String>) getRepositoryList().getModel()).addElement(newRepo); } }); } private void initAddGalleryIndexButton() { JButton button = (JButton) formPanel.getButton("addGalleryIndexButton"); button.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { // TODO: remove the button } }); } public void initDeleteRepoButton() { JButton button = (JButton) formPanel.getButton("deleteRepoButton"); button.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { int[] selectedRows = getRepositoryList().getSelectedIndices(); Arrays.sort(selectedRows); for (int i = selectedRows.length - 1; i >= 0; i--) { ((DefaultListModel<String>) getRepositoryList().getModel()).remove(selectedRows[i]); } } }); } private void cancel() { status = Status.CANCEL; setVisible(false); } private void accept() { try { copyUIToCampaign(); AssetManager.updateRepositoryList(); status = Status.OK; setVisible(false); } catch (IllegalArgumentException iae) { TabletopTool.showError(iae.getMessage(), iae); } } public void setCampaign(Campaign campaign) { this.campaign = campaign; copyCampaignToUI(campaign.getCampaignProperties()); } private void copyCampaignToUI(CampaignProperties campaignProperties) { JEditorPane epane = (JEditorPane) formPanel.getComponentByName("lightHelp"); epane.setCaretPosition(0); tokenPropertiesPanel.copyCampaignToUI(campaignProperties); updateRepositoryList(campaignProperties); String text; text = updateSightPanel(campaignProperties.getSightTypeMap()); getSightPanel().setText(text); getSightPanel().setCaretPosition(0); text = updateLightPanel(campaignProperties.getLightSourcesMap()); getLightPanel().setText(text); getLightPanel().setCaretPosition(0); tokenStatesController.copyCampaignToUI(campaignProperties); tokenBarController.copyCampaignToUI(campaignProperties); // updateTableList(); } private String updateSightPanel(Map<String, SightType> sightTypeMap) { StringBuilder builder = new StringBuilder(); for (SightType sight : sightTypeMap.values()) { builder.append(sight.getName()).append(": "); switch (sight.getShape()) { case SQUARE: builder.append("square "); if (sight.getDistance() != 0) builder.append("distance=").append(StringUtil.formatDecimal(sight.getDistance())).append(' '); break; case CIRCLE: builder.append("circle "); if (sight.getDistance() != 0) builder.append("distance=").append(StringUtil.formatDecimal(sight.getDistance())).append(' '); break; case CONE: builder.append("cone "); if (sight.getArc() != 0) builder.append("arc=").append(StringUtil.formatDecimal(sight.getArc())).append(' '); if (sight.getOffset() != 0) builder.append("offset=").append(StringUtil.formatDecimal(sight.getOffset())).append(' '); if (sight.getDistance() != 0) builder.append("distance=").append(StringUtil.formatDecimal(sight.getDistance())).append(' '); break; default: throw new IllegalArgumentException("Invalid shape?!"); } // Multiplier if (sight.getMultiplier() != 1 && sight.getMultiplier() != 0) { builder.append("x").append(StringUtil.formatDecimal(sight.getMultiplier())).append(' '); } // Personal light if (sight.getPersonalLightSource() != null) { LightSource source = sight.getPersonalLightSource(); double range = source.getMaxRange(); builder.append("r").append(StringUtil.formatDecimal(range)).append(' '); } builder.append('\n'); } return builder.toString(); } private String updateLightPanel(Map<String, Map<GUID, LightSource>> lightSources) { StringBuilder builder = new StringBuilder(); for (Entry<String, Map<GUID, LightSource>> entry : lightSources.entrySet()) { builder.append(entry.getKey()); builder.append("\n----\n"); for (LightSource lightSource : entry.getValue().values()) { builder.append(lightSource.getName()).append(":"); if (lightSource.getType() != LightSource.Type.NORMAL) { builder.append(' ').append(lightSource.getType().name().toLowerCase()); } String lastShape = ""; // this forces 'circle' to be printed double lastArc = 90; boolean lastGM = false; boolean lastOwner = false; for (Light light : lightSource.getLightList()) { String shape = null; // TODO: This HAS to change, the lights need to be auto describing, this hard wiring sucks if (lightSource.getType() == LightSource.Type.AURA) { // Currently these are mutually exclusive but perhaps not in the future? if (light.isGM() && light.isGM() != lastGM) builder.append(" GM"); if (light.isOwnerOnly() && light.isOwnerOnly() != lastOwner) builder.append(" OWNER"); lastGM = light.isGM(); lastOwner = light.isOwnerOnly(); } if (light.getShape() != null) { switch (light.getShape()) { case SQUARE: case CIRCLE: // TODO: Make this a preference shape = light.getShape().toString().toLowerCase(); break; case CONE: // if (light.getArcAngle() != 0 && light.getArcAngle() != 90 && light.getArcAngle() != lastArc) { lastArc = light.getArcAngle(); shape = "cone arc=" + StringUtil.formatDecimal(lastArc); } // else // shape = "cone"; break; } if (!lastShape.equals(shape)) builder.append(' ').append(shape); lastShape = shape; } builder.append(' ').append(StringUtil.formatDecimal(light.getRadius())); if (light.getPaint() instanceof DrawableColorPaint) { Color color = (Color) light.getPaint().getPaint(); builder.append(toHex(color)); } } builder.append('\n'); } builder.append('\n'); } return builder.toString(); } private String toHex(Color color) { StringBuilder builder = new StringBuilder("#"); builder.append(padLeft(Integer.toHexString(color.getRed()), '0', 2)); builder.append(padLeft(Integer.toHexString(color.getGreen()), '0', 2)); builder.append(padLeft(Integer.toHexString(color.getBlue()), '0', 2)); return builder.toString(); } private String padLeft(String str, char padChar, int length) { while (str.length() < length) { str = padChar + str; } return str; } private void updateRepositoryList(CampaignProperties properties) { DefaultListModel<String> model = new DefaultListModel<String>(); for (String repo : properties.getRemoteRepositoryList()) { model.addElement(repo); } getRepositoryList().setModel(model); } public JList<String> getRepositoryList() { return formPanel.getList("repoList"); } private void copyUIToCampaign() { tokenPropertiesPanel.copyUIToCampaign(campaign); campaign.getRemoteRepositoryList().clear(); for (int i = 0; i < getRepositoryList().getModel().getSize(); i++) { String repo = getRepositoryList().getModel().getElementAt(i); campaign.getRemoteRepositoryList().add(repo); } Map<String, Map<GUID, LightSource>> lightMap; lightMap = commitLightMap(getLightPanel().getText(), campaign.getLightSourcesMap()); campaign.getLightSourcesMap().clear(); campaign.getLightSourcesMap().putAll(lightMap); commitSightMap(getSightPanel().getText()); tokenStatesController.copyUIToCampaign(campaign); tokenBarController.copyUIToCampaign(campaign); ZoneRenderer zr = TabletopTool.getFrame().getCurrentZoneRenderer(); if (zr != null) { zr.getZoneView().flush(); zr.flushFog(); zr.flushLight(); TabletopTool.getFrame().refresh(); } } private void commitSightMap(final String text) { List<SightType> sightList = new LinkedList<SightType>(); LineNumberReader reader = new LineNumberReader(new BufferedReader(new StringReader(text))); String line = null; String toBeParsed = null, errmsg = null; List<String> errlog = new LinkedList<String>(); try { while ((line = reader.readLine()) != null) { line = line.trim(); // Blanks if (line.length() == 0 || line.indexOf(":") < 1) { continue; } // Parse line int split = line.indexOf(":"); String label = line.substring(0, split).trim(); String value = line.substring(split + 1).trim(); if (label.length() == 0) { continue; } // Parse Details double magnifier = 1; LightSource personalLight = null; String[] args = value.split("\\s+"); ShapeType shape = ShapeType.CIRCLE; int arc = 90; float range = 0; int offset = 0; double pLightRange = 0; for (String arg : args) { assert arg.length() > 0; // The split() uses "one or more spaces", removing empty strings try { shape = ShapeType.valueOf(arg.toUpperCase()); continue; } catch (IllegalArgumentException iae) { // Expected when not defining a shape } try { if (arg.startsWith("x")) { toBeParsed = arg.substring(1); // Used in the catch block, below errmsg = "msg.error.mtprops.sight.multiplier"; // (ditto) magnifier = StringUtil.parseDecimal(toBeParsed); } else if (arg.startsWith("r")) { // XXX Why not "r=#" instead of "r#"?? toBeParsed = arg.substring(1); errmsg = "msg.error.mtprops.sight.range"; pLightRange = StringUtil.parseDecimal(toBeParsed); } else if (arg.startsWith("arc=") && arg.length() > 4) { toBeParsed = arg.substring(4); errmsg = "msg.error.mtprops.sight.arc"; arc = StringUtil.parseInteger(toBeParsed); } else if (arg.startsWith("distance=") && arg.length() > 9) { toBeParsed = arg.substring(9); errmsg = "msg.error.mtprops.sight.distance"; range = StringUtil.parseDecimal(toBeParsed).floatValue(); } else if (arg.startsWith("offset=") && arg.length() > 7) { toBeParsed = arg.substring(7); errmsg = "msg.error.mtprops.sight.offset"; offset = StringUtil.parseInteger(toBeParsed); } else { toBeParsed = arg; errmsg = I18N.getText("msg.error.mtprops.sight.unknownField", reader.getLineNumber(), toBeParsed); errlog.add(errmsg); } } catch (ParseException e) { assert errmsg != null; errlog.add(I18N.getText(errmsg, reader.getLineNumber(), toBeParsed)); } } if (pLightRange > 0) { personalLight = new LightSource(); personalLight.add(new Light(shape, 0, pLightRange, arc, null)); } SightType sight = new SightType(label, magnifier, personalLight, shape, arc); sight.setDistance(range); sight.setOffset(offset); // Store sightList.add(sight); } } catch (IOException ioe) { TabletopTool.showError("msg.error.mtprops.sight.ioexception", ioe); } if (!errlog.isEmpty()) { // Show the user a list of errors so they can (attempt to) correct all of them at once TabletopTool.showFeedback(errlog); errlog.clear(); throw new IllegalArgumentException(); // Don't save sights... } campaign.setSightTypes(sightList); } /** * Converts the string stored in <code>getLightPanel().getText()</code> into * a Map that relates a group of light sources to a Map of GUID and * LightSource. * <p> * The format for the text is as follows: * <ol> * <li>Any line starting with a dash ("-") is a comment and is ignored. * <li>Blank lines (those containing only zero or more spaces) are group * separators. * <li>The first line of a sequence is the group name. * <li>Within a group, any line without a colon (":") is ignored. * <li>Remaining lines are of the following format: * <p> * <b> * <code>[Gm | Owner] [Circle+ | Square | Cone] [Normal+ | Aura] [Arc=angle] distance [#rrggbb]</code> * </b> * </p> * <p> * Brackets indicate optional components. A plus sign follows any default * value for a given field. Fields starting with an uppercase letter are * literal text (although they are case-insensitive). Fields that do not * start with an uppercase letter represent user-supplied values, typically * numbers (such as <code>angle</code>, <code>distance</code>, and * <code>#rrggbb</code>). The <code>GM</code>/<code>Owner</code> field is * only valid for Auras. * </p> * </ol> * </p> */ private Map<String, Map<GUID, LightSource>> commitLightMap(final String text, final Map<String, Map<GUID, LightSource>> originalLightSourcesMap) { Map<String, Map<GUID, LightSource>> lightMap = new TreeMap<String, Map<GUID, LightSource>>(); LineNumberReader reader = new LineNumberReader(new BufferedReader(new StringReader(text))); String line = null; List<String> errlog = new LinkedList<String>(); try { String currentGroupName = null; Map<GUID, LightSource> lightSourceMap = null; while ((line = reader.readLine()) != null) { line = line.trim(); // Comments if (line.length() > 0 && line.charAt(0) == '-') { continue; } // Blank lines if (line.length() == 0) { if (currentGroupName != null) { lightMap.put(currentGroupName, lightSourceMap); } currentGroupName = null; continue; } // New group if (currentGroupName == null) { currentGroupName = line; lightSourceMap = new HashMap<GUID, LightSource>(); continue; } // Item int split = line.indexOf(":"); if (split < 1) { continue; } String name = line.substring(0, split).trim(); LightSource lightSource = new LightSource(name); ShapeType shape = ShapeType.CIRCLE; // TODO: Make a preference for default shape double arc = 0; boolean gmOnly = false; boolean owner = false; String distance = null; for (String arg : line.substring(split + 1).split("\\s+")) { arg = arg.trim(); if (arg.length() == 0) { continue; } if (arg.equalsIgnoreCase("GM")) { gmOnly = true; owner = false; continue; } if (arg.equalsIgnoreCase("OWNER")) { gmOnly = false; owner = true; continue; } // Shape designation ? try { shape = ShapeType.valueOf(arg.toUpperCase()); continue; } catch (IllegalArgumentException iae) { // Expected when not defining a shape } // Type designation ? try { LightSource.Type type = LightSource.Type.valueOf(arg.toUpperCase()); lightSource.setType(type); continue; } catch (IllegalArgumentException iae) { // Expected when not defining a shape } // Parameters split = arg.indexOf('='); if (split > 0) { String key = arg.substring(0, split); String value = arg.substring(split + 1); // TODO: Make this a generic map to pass instead of 'arc' if ("arc".equalsIgnoreCase(key)) { try { arc = StringUtil.parseDecimal(value); shape = ShapeType.CONE; // If the user specifies an arc, force the shape to CONE } catch (ParseException pe) { errlog.add(I18N.getText("msg.error.mtprops.light.arc", reader.getLineNumber(), value)); } } continue; } Color color = null; distance = arg; split = arg.indexOf("#"); if (split > 0) { String colorString = arg.substring(split); // Keep the '#' distance = arg.substring(0, split); color = Color.decode(colorString); } boolean isAura = lightSource.getType() == LightSource.Type.AURA; if (!isAura && (gmOnly || owner)) { errlog.add(I18N.getText("msg.error.mtprops.light.gmOrOwner", reader.getLineNumber())); gmOnly = false; owner = false; } owner = gmOnly == true ? false : owner; try { Light t = new Light(shape, 0, StringUtil.parseDecimal(distance), arc, color != null ? new DrawableColorPaint(color) : null, gmOnly, owner); lightSource.add(t); } catch (ParseException pe) { errlog.add(I18N.getText("msg.error.mtprops.light.distance", reader.getLineNumber(), distance)); } } // Keep ID the same if modifying existing light // TODO FJE Why? Is there some benefit to doing so? Changes to light sources require the map to be re-rendered anyway, don't they? if (originalLightSourcesMap.containsKey(currentGroupName)) { for (LightSource ls : originalLightSourcesMap.get(currentGroupName).values()) { if (ls.getName().equalsIgnoreCase(name)) { lightSource.setId(ls.getId()); break; } } } lightSourceMap.put(lightSource.getId(), lightSource); } // Last group if (currentGroupName != null) { lightMap.put(currentGroupName, lightSourceMap); } } catch (IOException ioe) { TabletopTool.showError("msg.error.mtprops.light.ioexception", ioe); } if (!errlog.isEmpty()) { TabletopTool.showFeedback(errlog); errlog.clear(); throw new IllegalArgumentException(); // Don't save lights... } return lightMap; } public JEditorPane getLightPanel() { return (JEditorPane) formPanel.getTextComponent("lightPanel"); } public JEditorPane getSightPanel() { return (JEditorPane) formPanel.getTextComponent("sightPanel"); } public JTextArea getTokenPropertiesTextArea() { return (JTextArea) formPanel.getTextComponent("tokenProperties"); } public JButton getOKButton() { return (JButton) formPanel.getButton("okButton"); } private void initOKButton() { getOKButton().addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { accept(); } }); } public JButton getCancelButton() { return (JButton) formPanel.getButton("cancelButton"); } public JButton getImportButton() { return (JButton) formPanel.getButton("importButton"); } public JButton getExportButton() { return (JButton) formPanel.getButton("exportButton"); } private void initCancelButton() { getCancelButton().addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { status = Status.CANCEL; setVisible(false); } }); } private void initImportButton() { getImportButton().addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { JFileChooser chooser = TabletopTool.getFrame().getLoadPropsFileChooser(); if (chooser.showOpenDialog(TabletopTool.getFrame()) != JFileChooser.APPROVE_OPTION) return; final File selectedFile = chooser.getSelectedFile(); EventQueue.invokeLater(new Runnable() { @Override public void run() { CampaignProperties properties = PersistenceUtil.loadCampaignProperties(selectedFile); // TODO: Allow specifying whether it is a replace or merge if (properties != null) { TabletopTool.getCampaign().mergeCampaignProperties(properties); copyCampaignToUI(properties); } } }); } }); } private void initExportButton() { getExportButton().addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { // TODO: Remove this hack. Specifically, make the export use a properties object // composed of the current dialog entries instead of directly from the campaign copyUIToCampaign(); // END HACK JFileChooser chooser = TabletopTool.getFrame().getSavePropsFileChooser(); if (chooser.showSaveDialog(TabletopTool.getFrame()) != JFileChooser.APPROVE_OPTION) return; File selectedFile = chooser.getSelectedFile(); if (selectedFile.exists()) { if (selectedFile.getName().endsWith(".rpgame")) { if (!TabletopTool.confirm("Import into game settings file?")) { return; } } else if (!TabletopTool.confirm("Overwrite existing file?")) { return; } } try { PersistenceUtil.saveCampaignProperties(campaign, chooser.getSelectedFile()); TabletopTool.showInformation("Properties Saved."); } catch (IOException ioe) { TabletopTool.showError("Could not save properties: ", ioe); } } }); } }