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}