001/*
002 * Gridarta MMORPG map editor for Crossfire, Daimonin and similar games.
003 * Copyright (C) 2000-2010 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.textedit.scripteditor;
021
022import java.awt.Frame;
023import java.awt.Rectangle;
024import java.awt.geom.RectangularShape;
025import java.io.BufferedReader;
026import java.io.File;
027import java.io.FileInputStream;
028import java.io.FileNotFoundException;
029import java.io.IOException;
030import java.io.InputStreamReader;
031import java.util.ArrayList;
032import java.util.List;
033import java.util.prefs.Preferences;
034import javax.swing.JDialog;
035import javax.swing.JOptionPane;
036import javax.swing.JTabbedPane;
037import javax.swing.SwingConstants;
038import javax.swing.event.ChangeEvent;
039import javax.swing.event.ChangeListener;
040import javax.swing.text.BadLocationException;
041import net.sf.gridarta.textedit.textarea.JEditTextArea;
042import net.sf.gridarta.textedit.textarea.SyntaxDocument;
043import net.sf.gridarta.textedit.textarea.TextAreaDefaults;
044import net.sf.gridarta.textedit.textarea.tokenmarker.TokenMarkerFactory;
045import net.sf.gridarta.utils.Exiter;
046import net.sf.gridarta.utils.ExiterListener;
047import net.sf.japi.swing.action.ActionBuilder;
048import net.sf.japi.swing.action.ActionBuilderFactory;
049import org.apache.log4j.Category;
050import org.apache.log4j.Logger;
051import org.jetbrains.annotations.NotNull;
052import org.jetbrains.annotations.Nullable;
053
054/**
055 * The script editor frame. This class should only exist in ScriptEditControl.
056 * No other class should refer to it.
057 * @author <a href="mailto:andi.vogl@gmx.net">Andreas Vogl</a>
058 */
059public class ScriptEditView extends JDialog {
060
061    /**
062     * The Logger for printing log messages.
063     */
064    @NotNull
065    private static final Category log = Logger.getLogger(ScriptEditView.class);
066
067    /**
068     * Action Builder.
069     */
070    @NotNull
071    private static final ActionBuilder ACTION_BUILDER = ActionBuilderFactory.getInstance().getActionBuilder("net.sf.gridarta");
072
073    /**
074     * The {@link TextAreaDefaults} for tabs.
075     */
076    @NotNull
077    private TextAreaDefaults textAreaDefaults;
078
079    /**
080     * Serial Version UID.
081     */
082    private static final long serialVersionUID = 1L;
083
084    /**
085     * The key used to store the editor window x-coordinate in preferences
086     * file.
087     */
088    @NotNull
089    private static final String WINDOW_X = "ScriptEditWindow.x";
090
091    /**
092     * The key used to store the editor window y-coordinate in preferences
093     * file.
094     */
095    @NotNull
096    private static final String WINDOW_Y = "ScriptEditWindow.y";
097
098    /**
099     * The key used to store the editor window width in preferences file.
100     */
101    @NotNull
102    private static final String WINDOW_WIDTH = "ScriptEditWindow.width";
103
104    /**
105     * The key used to store the editor window height in preferences file.
106     */
107    @NotNull
108    private static final String WINDOW_HEIGHT = "ScriptEditWindow.height";
109
110    /**
111     * The actions for the script editor.
112     */
113    @NotNull
114    private final Actions actions;
115
116    /**
117     * The undo related actions for the script editor.
118     */
119    @NotNull
120    private final ScriptEditUndoActions scriptEditUndoActions;
121
122    @NotNull
123    private final JTabbedPane tabPane;       // tab pane
124
125    @NotNull
126    private final List<JEditTextArea> textAreas;          // list of 'JEditTextArea' objects, same order as tabs
127
128    /**
129     * Builds a frame but keep it hidden (it is shown when first file is
130     * opened).
131     * @param owner the owner of this view
132     * @param preferences the preferences to use
133     * @param exiter the exiter instance
134     */
135    public ScriptEditView(@NotNull final ScriptEditControl control, @NotNull final Frame owner, @NotNull final Preferences preferences, @NotNull final Exiter exiter) {
136        super(owner, "Script Pad");
137        setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
138
139        textAreas = new ArrayList<JEditTextArea>();
140        actions = new Actions(control);
141        scriptEditUndoActions = new ScriptEditUndoActions();
142        setJMenuBar(ACTION_BUILDER.createMenuBar(true, "scriptEditMenu"));
143
144        tabPane = new JTabbedPane(SwingConstants.TOP); // init tab pane
145        tabPane.addChangeListener(new EditTabListener(this));
146
147        getContentPane().add(tabPane);
148        addWindowListener(new EditWindowListener(control)); // add listener for close box
149
150        // calculate some default values in case there is no settings file
151        final RectangularShape screen = getGraphicsConfiguration().getBounds();
152        final int width = preferences.getInt(WINDOW_WIDTH, (int) (0.8 * screen.getWidth()));
153        final int height = preferences.getInt(WINDOW_HEIGHT, (int) (0.8 * screen.getHeight()));
154        final int x = preferences.getInt(WINDOW_X, (int) (screen.getX() + (screen.getWidth() - (double) width) / 2.0));
155        final int y = preferences.getInt(WINDOW_Y, (int) (screen.getY() + (screen.getHeight() - (double) height) / 2.0));
156        setBounds(x, y, width, height);
157
158        final ExiterListener exiterListener = new ExiterListener() {
159
160            @Override
161            public void preExitNotify() {
162                // ignore
163            }
164
165            @Override
166            public void appExitNotify() {
167                final Rectangle bounds = getBounds();
168                preferences.putInt(WINDOW_X, bounds.x);
169                preferences.putInt(WINDOW_Y, bounds.y);
170                preferences.putInt(WINDOW_WIDTH, bounds.width);
171                preferences.putInt(WINDOW_HEIGHT, bounds.height);
172            }
173
174            @Override
175            public void waitExitNotify() {
176                // ignore
177            }
178
179        };
180        exiter.addExiterListener(exiterListener);
181    }
182
183    @Deprecated
184    public void setTextAreaDefaults(@NotNull final TextAreaDefaults textAreaDefaults) {
185        this.textAreaDefaults = textAreaDefaults;
186    }
187
188    /**
189     * Adds a new TextArea Panel to the TabbedPane.
190     * @param title title of this script (filename)
191     * @param file file where this script is stored, null if new script opened
192     */
193    public void addTab(@NotNull final String title, @Nullable final File file) {
194        final JEditTextArea ta = new JEditTextArea(textAreaDefaults); // open new TextArea
195        //ta.setFont(new Font("Courier New", Font.PLAIN, 12));
196        final SyntaxDocument syntaxDocument = new SyntaxDocument();
197        ta.setDocument(syntaxDocument);
198        textAreas.add(ta);
199        ta.getDocument().setTokenMarker(TokenMarkerFactory.createTokenMarker(file));
200        scriptEditUndoActions.addDocument(syntaxDocument);
201        tabPane.addTab(title, ta);
202        if (getTabCount() <= 1 || !isShowing()) {
203            setVisible(true);
204        }
205
206        scriptEditUndoActions.setCurrentDocument(syntaxDocument);
207
208        tabPane.setSelectedIndex(getTabCount() - 1);
209
210        // very important: There must be a drawing update after showing the frame, to make
211        // sure the graphics context is fully initialized before calling 'setEditingFocus()'
212        //if (isFirstTimeShowing) {
213        update(getGraphics());
214        //}
215
216        if (file != null && file.exists()) {
217            // print file into this document
218            try {
219                final FileInputStream fis = new FileInputStream(file);
220                try {
221                    final InputStreamReader isr = new InputStreamReader(fis);
222                    try {
223                        final BufferedReader in = new BufferedReader(isr);
224                        try {
225                            boolean firstLine = true;
226                            final StringBuilder buff = new StringBuilder("");
227                            while (true) {
228                                final String line = in.readLine();
229                                if (line == null) {
230                                    break;
231                                }
232                                if (firstLine) {
233                                    firstLine = false;
234                                } else {
235                                    buff.append('\n');
236                                }
237                                buff.append(line);
238                            }
239                            ta.getDocument().insertString(0, buff.toString(), null);
240                        } finally {
241                            in.close();
242                        }
243                    } finally {
244                        isr.close();
245                    }
246                } finally {
247                    fis.close();
248                }
249                // insert buffer into the document
250            } catch (final FileNotFoundException e) {
251                log.info("addTab(): File '" + file.getName() + "' not found.");
252            } catch (final IOException e) {
253                log.info("addTab(): I/O-Error while reading '" + file.getName() + "'.");
254            } catch (final BadLocationException e) {
255                log.info("addTab(): Bad Location in Document!");
256            }
257            scriptEditUndoActions.resetUndo(syntaxDocument);
258        }
259
260        ta.setEditingFocus(); // set focus to TextArea in order to respond to key press events
261        //ta.scrollToCaret();   // make sure the window shows caret (top left corner)
262
263        ta.resetModified();
264
265        toFront(); // bring window to front
266    }
267
268    /**
269     * Closes the active script-tab.
270     */
271    public void closeActiveTab() {
272        if (textAreas.isEmpty()) {
273            setVisible(false);
274        } else {
275            final int oldIndex = tabPane.getSelectedIndex();
276            final int newIndex = oldIndex >= tabPane.getTabCount() - 1 ? oldIndex - 1 : oldIndex;
277            tabPane.setSelectedIndex(newIndex);
278            final JEditTextArea textArea = textAreas.get(oldIndex);
279            scriptEditUndoActions.removeDocument(textArea.getDocument());
280            textAreas.remove(oldIndex);
281            tabPane.remove(oldIndex);
282            actions.refresh();
283            scriptEditUndoActions.setCurrentDocument(newIndex == -1 ? null : textAreas.get(newIndex).getDocument());
284        }
285    }
286
287    /**
288     * @return the currently active TextArea (in front)
289     */
290    @Nullable
291    public JEditTextArea getActiveTextArea() {
292        if (getTabCount() > 0) {
293            return textAreas.get(tabPane.getSelectedIndex());
294        }
295
296        return null; // no window is open
297    }
298
299    /**
300     * @return the index of the selected tab pane
301     */
302    public int getSelectedIndex() {
303        return tabPane.getSelectedIndex();
304    }
305
306    /**
307     * @return the number of open tabs
308     */
309    public int getTabCount() {
310        return tabPane.getTabCount();
311    }
312
313    /**
314     * Sets the title of the tab at specified index.
315     * @param index the index of the tab to change title
316     * @param title the new title string
317     */
318    public void setTitleAt(final int index, @NotNull final String title) {
319        tabPane.setTitleAt(index, title);
320    }
321
322    /**
323     * Returns the title of the active tab.
324     * @return the title of the active tab, or <code>null</code> if no tab
325     *         exists
326     */
327    @Nullable
328    public String getActiveTitle() {
329        return getTabCount() > 0 ? tabPane.getTitleAt(tabPane.getSelectedIndex()) : null;
330    }
331
332    /**
333     * Shows the given confirmation message as popup frame. The message is a
334     * yes/no option. The parent frame is disabled until the user picks an
335     * answer.
336     * @param title the title of the message
337     * @param message the message to be shown
338     * @return true if the user agrees, false if user disagrees
339     */
340    public boolean askConfirm(@NotNull final String title, @NotNull final String message) {
341        return JOptionPane.showConfirmDialog(this, message, title, JOptionPane.YES_NO_OPTION, JOptionPane.INFORMATION_MESSAGE) == JOptionPane.YES_OPTION;
342    }
343
344    /**
345     * Shows the given message in the UI.
346     * @param title the title of the message
347     * @param message the message to be shown
348     * @param messageType type of message (see JOptionPane constants), defines
349     * icon used
350     */
351    public void showMessage(@NotNull final String title, @NotNull final String message, final int messageType) {
352        JOptionPane.showMessageDialog(this, message, title, messageType);
353    }
354
355    public void showMessage(@NotNull final String title, @NotNull final String message) {
356        JOptionPane.showMessageDialog(this, message, title, JOptionPane.INFORMATION_MESSAGE);
357    }
358
359    /**
360     * Inner class: Listener for ChangeEvents in the tabPane.
361     */
362    private class EditTabListener implements ChangeListener {
363
364        @NotNull
365        private final ScriptEditView view;       // view
366
367        private int index;                 // index of selected tab
368
369        private EditTabListener(@NotNull final ScriptEditView view) {
370            this.view = view;
371            index = view.getSelectedIndex();
372        }
373
374        @Override
375        public void stateChanged(@NotNull final ChangeEvent e) {
376            if (index != view.getSelectedIndex()) {
377                index = view.getSelectedIndex();
378                // active selected tab has changed
379                actions.refresh(); // refresh state of menus
380                scriptEditUndoActions.setCurrentDocument(index == -1 ? null : textAreas.get(index).getDocument());
381            }
382        }
383
384    }
385
386}