001/* 002 * Gridarta MMORPG map editor for Crossfire, Daimonin and similar games. 003 * Copyright (C) 2000-2011 The Gridarta Developers. 004 * 005 * This program is free software; you can redistribute it and/or modify 006 * it under the terms of the GNU General Public License as published by 007 * the Free Software Foundation; either version 2 of the License, or 008 * (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 013 * GNU General Public License for more details. 014 * 015 * You should have received a copy of the GNU General Public License along 016 * with this program; if not, write to the Free Software Foundation, Inc., 017 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 018 */ 019 020package net.sf.gridarta.gui.scripts; 021 022import java.awt.Color; 023import java.awt.Component; 024import java.awt.Container; 025import java.awt.FlowLayout; 026import java.awt.Frame; 027import java.awt.Insets; 028import java.awt.event.ActionEvent; 029import java.awt.event.ActionListener; 030import java.io.File; 031import java.io.IOException; 032import javax.swing.AbstractButton; 033import javax.swing.BorderFactory; 034import javax.swing.Box; 035import javax.swing.BoxLayout; 036import javax.swing.JButton; 037import javax.swing.JComboBox; 038import javax.swing.JDialog; 039import javax.swing.JFileChooser; 040import javax.swing.JLabel; 041import javax.swing.JOptionPane; 042import javax.swing.JPanel; 043import javax.swing.JTextField; 044import javax.swing.WindowConstants; 045import javax.swing.filechooser.FileFilter; 046import javax.swing.text.JTextComponent; 047import net.sf.gridarta.model.archetype.Archetype; 048import net.sf.gridarta.model.gameobject.GameObject; 049import net.sf.gridarta.model.io.PathManager; 050import net.sf.gridarta.model.maparchobject.MapArchObject; 051import net.sf.gridarta.model.mapmanager.MapManager; 052import net.sf.gridarta.model.scripts.ScriptArchData; 053import net.sf.gridarta.model.scripts.ScriptArchUtils; 054import net.sf.gridarta.model.scripts.ScriptUtils; 055import net.sf.gridarta.model.scripts.ScriptedEvent; 056import net.sf.gridarta.model.scripts.ScriptedEventFactory; 057import net.sf.gridarta.model.scripts.UndefinedEventArchetypeException; 058import net.sf.gridarta.model.settings.GlobalSettings; 059import net.sf.gridarta.textedit.scripteditor.ScriptEditControl; 060import net.sf.gridarta.utils.FileChooserUtils; 061import net.sf.japi.swing.action.ActionBuilder; 062import net.sf.japi.swing.action.ActionBuilderFactory; 063import net.sf.japi.util.Arrays2; 064import org.jetbrains.annotations.NotNull; 065import org.jetbrains.annotations.Nullable; 066 067public class DefaultScriptArchEditor<G extends GameObject<G, A, R>, A extends MapArchObject<A>, R extends Archetype<G, A, R>> implements ScriptArchEditor<G, A, R> { 068 069 /** 070 * Action Builder. 071 */ 072 @NotNull 073 private static final ActionBuilder ACTION_BUILDER = ActionBuilderFactory.getInstance().getActionBuilder("net.sf.gridarta"); 074 075 /** 076 * The {@link ScriptArchUtils} instance to use. 077 */ 078 @NotNull 079 private final ScriptArchUtils scriptArchUtils; 080 081 /** 082 * The ending for scripts. 083 */ 084 @NotNull 085 private final String scriptEnding; 086 087 /** 088 * The {@link PathManager} for converting path names. 089 */ 090 @NotNull 091 private final PathManager pathManager; 092 093 /** 094 * The {@link ScriptEditControl} to use. 095 */ 096 @Nullable 097 private ScriptEditControl scriptEditControl; 098 099 @NotNull 100 private final JComboBox eventTypeBox; 101 102 @NotNull 103 private final FileFilter scriptFileFilter; 104 105 /** 106 * The {@link GlobalSettings} to use. 107 */ 108 @NotNull 109 private final GlobalSettings globalSettings; 110 111 /** 112 * The {@link MapManager} to use. 113 */ 114 @NotNull 115 private final MapManager<?, ?, ?> mapManager; 116 117 @NotNull 118 private final JComboBox pluginNameBox; 119 120 // popup frame for new scripts: 121 122 @Nullable 123 private JDialog newScriptFrame; 124 125 @NotNull 126 private JLabel headingLabel; 127 128 @NotNull 129 private JTextComponent inputScriptPath; 130 131 @NotNull 132 private JTextComponent inputOptions; 133 134 @NotNull 135 private PathButtonListener<G, A, R> nsOkListener; 136 137 /** 138 * The {@link ScriptedEventFactory} instance to use. 139 */ 140 @NotNull 141 private final ScriptedEventFactory<G, A, R> scriptedEventFactory; 142 143 /** 144 * Creates a new instance. 145 * @param scriptedEventFactory the scripted event factory instance to use 146 * @param scriptEnding the suffix for script files 147 * @param name the default event type 148 * @param scriptArchUtils the script arch utils to use 149 * @param scriptFileFilter the script file filter to use 150 * @param globalSettings the global settings to use 151 * @param mapManager the map manager instance to use 152 * @param pathManager the path manager for converting path names 153 */ 154 public DefaultScriptArchEditor(@NotNull final ScriptedEventFactory<G, A, R> scriptedEventFactory, final String scriptEnding, final String name, @NotNull final ScriptArchUtils scriptArchUtils, @NotNull final FileFilter scriptFileFilter, @NotNull final GlobalSettings globalSettings, @NotNull final MapManager<?, ?, ?> mapManager, @NotNull final PathManager pathManager) { 155 this.scriptedEventFactory = scriptedEventFactory; 156 this.scriptEnding = scriptEnding; 157 this.scriptArchUtils = scriptArchUtils; 158 this.pathManager = pathManager; 159 160 pluginNameBox = new JComboBox(new String[] { name }); 161 pluginNameBox.setSelectedIndex(0); 162 163 eventTypeBox = createEventTypeBox(scriptArchUtils); 164 this.scriptFileFilter = scriptFileFilter; 165 this.globalSettings = globalSettings; 166 this.mapManager = mapManager; 167 } 168 169 @NotNull 170 private static JComboBox createEventTypeBox(@NotNull final ScriptArchUtils scriptArchUtils) { 171 final String[] valuesArray = scriptArchUtils.getEventNames(); 172 final JComboBox tmpEventTypeBox = new JComboBox(valuesArray); 173 tmpEventTypeBox.setSelectedIndex(Arrays2.linearEqualitySearch("say", valuesArray)); 174 return tmpEventTypeBox; 175 } 176 177 /** 178 * {@inheritDoc} 179 */ 180 @Deprecated 181 @Override 182 public void setScriptEditControl(@Nullable final ScriptEditControl scriptEditControl) { 183 this.scriptEditControl = scriptEditControl; 184 } 185 186 /** 187 * {@inheritDoc} 188 */ 189 @Override 190 public void addEventScript(final G gameObject, final ScriptArchData<G, A, R> scriptArchData, @NotNull final Frame parent) { 191 final String archName = gameObject.getBestName(); 192 // create a reasonable default script name for lazy users :-) 193 final String defScriptName = ScriptUtils.chooseDefaultScriptName(mapManager.getLocalMapDir(), archName, scriptEnding, pathManager); 194 195 if (newScriptFrame == null) { 196 // initialize popup frame 197 newScriptFrame = new JDialog(parent, "New Scripted Event", true); 198 newScriptFrame.setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE); 199 200 final JPanel mainPanel = new JPanel(); 201 mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS)); 202 mainPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 2, 5)); 203 204 // first line: heading 205 final Container line1 = new JPanel(new FlowLayout(FlowLayout.LEFT)); 206 headingLabel = new JLabel("New scripted event for \"" + archName + "\":"); 207 headingLabel.setForeground(Color.black); 208 line1.add(headingLabel); 209 mainPanel.add(line1); 210 211 // event type 212 mainPanel.add(Box.createVerticalStrut(10)); 213 final Container line2 = new JPanel(new FlowLayout(FlowLayout.LEFT)); 214 final Component typeLabel = new JLabel("Event type:"); 215 line2.add(typeLabel); 216 line2.add(eventTypeBox); 217 //mainPanel.add(line2); 218 line2.add(Box.createHorizontalStrut(10)); 219 220 // plugin name 221 final Component pluginLabel = new JLabel("Plugin:"); 222 line2.add(pluginLabel); 223 line2.add(pluginNameBox); 224 mainPanel.add(line2); 225 226 // path 227 mainPanel.add(Box.createVerticalStrut(5)); 228 final Container line3 = new JPanel(new FlowLayout(FlowLayout.LEFT)); 229 final Component scriptFileLabel = new JLabel("Script file:"); 230 line3.add(scriptFileLabel); 231 mainPanel.add(line3); 232 inputScriptPath = new JTextField(defScriptName, 20); 233 final AbstractButton browseButton = new JButton("..."); 234 browseButton.setMargin(new Insets(0, 10, 0, 10)); 235 browseButton.addActionListener(new ActionListener() { 236 237 @Override 238 public void actionPerformed(final ActionEvent e) { 239 final File home = mapManager.getLocalMapDir(); 240 241 final JFileChooser fileChooser = new JFileChooser(); 242 fileChooser.setDialogTitle("Select Script File"); 243 fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY); 244 FileChooserUtils.setCurrentDirectory(fileChooser, home); 245 fileChooser.setMultiSelectionEnabled(false); 246 fileChooser.setFileFilter(scriptFileFilter); 247 248 if (fileChooser.showOpenDialog(newScriptFrame) == JFileChooser.APPROVE_OPTION) { 249 // user has selected a file 250 final File f = fileChooser.getSelectedFile(); 251 inputScriptPath.setText(ScriptUtils.localizeEventPath(mapManager.getLocalMapDir(), f, globalSettings.getMapsDirectory())); 252 } 253 } 254 }); 255 line3.add(inputScriptPath); 256 line3.add(browseButton); 257 mainPanel.add(line3); 258 259 // options 260 mainPanel.add(Box.createVerticalStrut(5)); 261 final Container line4 = new JPanel(new FlowLayout(FlowLayout.LEFT)); 262 line4.add(new JLabel("Script options:")); 263 inputOptions = new JTextField("", 20); 264 line4.add(inputOptions); 265 mainPanel.add(line4); 266 267 // description 268 final Container line5 = new JPanel(new FlowLayout(FlowLayout.LEFT)); 269 final JPanel textPanel = new JPanel(); 270 textPanel.setLayout(new BoxLayout(textPanel, BoxLayout.Y_AXIS)); 271 final Component label1 = new JLabel("When you specify an existing file, the new event will be linked"); 272 textPanel.add(label1); 273 final Component label2 = new JLabel("to that existing script. Otherwise a new script file is created."); 274 textPanel.add(label2); 275 line5.add(textPanel); 276 mainPanel.add(line5); 277 278 // button panel: 279 mainPanel.add(Box.createVerticalStrut(10)); 280 final Container line6 = new JPanel(new FlowLayout(FlowLayout.RIGHT)); 281 final AbstractButton nsOkButton = new JButton("OK"); 282 nsOkListener = new PathButtonListener<G, A, R>(true, newScriptFrame, scriptArchData, gameObject, this, null); 283 nsOkButton.addActionListener(nsOkListener); 284 line6.add(nsOkButton); 285 286 final AbstractButton cancelButton = new JButton("Cancel"); 287 cancelButton.addActionListener(new PathButtonListener<G, A, R>(false, newScriptFrame, null, null, this, null)); 288 line6.add(cancelButton); 289 mainPanel.add(line6); 290 291 newScriptFrame.getContentPane().add(mainPanel); 292 newScriptFrame.pack(); 293 newScriptFrame.setLocationRelativeTo(parent); 294 newScriptFrame.setVisible(true); 295 } else { 296 // just set fields and show 297 headingLabel.setText("New scripted event for \"" + archName + "\":"); 298 inputScriptPath.setText(defScriptName); 299 inputOptions.setText(""); 300 nsOkListener.setScriptArchData(scriptArchData, gameObject); 301 newScriptFrame.toFront(); 302 newScriptFrame.setVisible(true); 303 } 304 } 305 306 /** 307 * {@inheritDoc} 308 */ 309 @Override 310 public void createNewEvent(@NotNull final JDialog frame, @NotNull final ScriptArchData<G, A, R> scriptArchData, @NotNull final G gameObject) { 311 final StringBuilder scriptPath = new StringBuilder(inputScriptPath.getText().trim().replace('\\', '/')); 312 final String options = inputOptions.getText().trim(); 313 final int eventType = scriptArchUtils.indexToEventType(eventTypeBox.getSelectedIndex()); 314 final String pluginName = ((String) pluginNameBox.getSelectedItem()).trim(); 315 316 final File localMapDir = mapManager.getLocalMapDir(); 317 318 // first check if that event type is not already in use 319 final GameObject<G, A, R> replaceObject = scriptArchData.getScriptedEvent(eventType, gameObject); 320 if (replaceObject != null) { 321 // collision with existing event -> ask user: replace? 322 if (JOptionPane.showConfirmDialog(frame, "An event of type \"" + scriptArchUtils.typeName(eventType) + "\" already exists for this object.\n" + "Do you want to replace the existing event?", "Event exists", JOptionPane.YES_NO_OPTION, JOptionPane.INFORMATION_MESSAGE) == JOptionPane.NO_OPTION) { 323 // bail out 324 return; 325 } 326 } 327 328 String absScriptPath; 329 if (scriptPath.length() > 0 && scriptPath.charAt(0) == '/') { 330 // script path is absolute 331 final File mapDir = globalSettings.getMapsDirectory(); // global map directory 332 if (!mapDir.exists()) { 333 // if map dir doesn't exist, this is not going to work 334 frame.setVisible(false); 335 ACTION_BUILDER.showMessageDialog(frame, "mapDirDoesntExist", mapDir); 336 return; 337 } 338 339 absScriptPath = mapDir.getAbsolutePath() + scriptPath; 340 } else { 341 // script path is relative 342 absScriptPath = localMapDir.getAbsolutePath() + "/" + scriptPath; 343 } 344 345 // now check if the specified path points to an existing script 346 File newScriptFile = new File(absScriptPath); 347 if (!newScriptFile.exists() && !absScriptPath.endsWith(scriptEnding)) { 348 absScriptPath += scriptEnding; 349 scriptPath.append(scriptEnding); 350 newScriptFile = new File(absScriptPath); 351 } 352 353 if (newScriptFile.exists()) { 354 if (newScriptFile.isFile()) { 355 // file exists -> link it to the event 356 final ScriptedEvent<G, A, R> event; 357 try { 358 event = scriptedEventFactory.newScriptedEvent(eventType, pluginName, scriptPath.toString(), options); 359 } catch (final UndefinedEventArchetypeException ex) { 360 JOptionPane.showMessageDialog(frame, "Cannot create event of type " + eventType + ":\n" + ex.getMessage() + ".", "Cannot create event", JOptionPane.ERROR_MESSAGE); 361 return; 362 } 363 if (replaceObject != null) { 364 replaceObject.remove(); 365 } 366 gameObject.addLast(event.getEventArch()); 367 frame.setVisible(false); // close dialog 368 } 369 370 return; 371 } 372 373 if (!absScriptPath.endsWith(scriptEnding)) { 374 absScriptPath += scriptEnding; 375 scriptPath.append(scriptEnding); 376 newScriptFile = new File(absScriptPath); 377 } 378 379 // file does not exist -> aks user: create new file? 380 if (JOptionPane.showConfirmDialog(frame, "Create new script '" + newScriptFile.getName() + "'?", "Confirm", JOptionPane.YES_NO_OPTION, JOptionPane.INFORMATION_MESSAGE) != JOptionPane.YES_OPTION) { 381 return; 382 } 383 384 boolean couldCreateFile = false; // true when file creation successful 385 try { 386 // try to create new empty file 387 couldCreateFile = newScriptFile.createNewFile(); 388 } catch (final IOException e) { 389 /* ignore (really?) */ 390 } 391 392 if (!couldCreateFile) { 393 JOptionPane.showMessageDialog(frame, "File '" + newScriptFile.getName() + "' could not be created.\n" + "Please check your path and write permissions.", "Cannot create file", JOptionPane.ERROR_MESSAGE); 394 return; 395 } 396 397 // file has been created, now link it to the event 398 final ScriptedEvent<G, A, R> event; 399 try { 400 event = scriptedEventFactory.newScriptedEvent(eventType, pluginName, scriptPath.toString(), options); 401 } catch (final UndefinedEventArchetypeException ex) { 402 JOptionPane.showMessageDialog(frame, "Cannot create event of type " + eventType + ":\n" + ex.getMessage() + ".", "Cannot create event", JOptionPane.ERROR_MESSAGE); 403 return; 404 } 405 if (replaceObject != null) { 406 replaceObject.remove(); 407 } 408 gameObject.addLast(event.getEventArch()); 409 frame.setVisible(false); // close dialog 410 411 // open new script file 412 if (scriptEditControl != null) { 413 scriptEditControl.openScriptFile(newScriptFile.getAbsolutePath()); 414 } 415 } 416 417}