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.Color;
023import java.awt.event.ActionEvent;
024import java.awt.event.ActionListener;
025import java.io.BufferedReader;
026import java.io.EOFException;
027import java.io.FileNotFoundException;
028import java.io.IOException;
029import java.io.InputStream;
030import java.io.InputStreamReader;
031import java.io.Reader;
032import java.io.UnsupportedEncodingException;
033import java.net.URL;
034import java.util.ArrayList;
035import java.util.Collections;
036import java.util.List;
037import javax.swing.JComboBox;
038import javax.swing.JPopupMenu;
039import javax.swing.text.BadLocationException;
040import net.sf.gridarta.textedit.textarea.JEditTextArea;
041import net.sf.gridarta.utils.IOUtils;
042import org.apache.log4j.Category;
043import org.apache.log4j.Logger;
044import org.jetbrains.annotations.NotNull;
045import org.jetbrains.annotations.Nullable;
046
047/**
048 * This class implements a popup window which shows all python methods in the
049 * 'CFPython' package. <p/> As JPopupMenus are not scrollable, the
050 * implementation of the combo box popup menu (standard UI) is used here. This
051 * is not the perfect approach as it imposes some unwanted limitations. However,
052 * the "perfect approach" would require full coding of a JWindow rendered as a
053 * popup menu - which is an extremely time consuming task.
054 * @author <a href="mailto:andi.vogl@gmx.net">Andreas Vogl</a>
055 */
056public class CFPythonPopup extends JComboBox {
057
058    /**
059     * Python menu definitions.
060     */
061    @NotNull
062    private static final String PYTHON_MENU_FILE = "cfpython_menu.def";
063
064    /**
065     * The Logger for printing log messages.
066     */
067    @NotNull
068    private static final Category log = Logger.getLogger(CFPythonPopup.class);
069
070    /**
071     * Serial Version UID.
072     */
073    private static final long serialVersionUID = 1L;
074
075    /**
076     * List of menu entries (all CFPython commands).
077     * @serial
078     */
079    @Nullable
080    private static String[] menuEntries;
081
082    /**
083     * The popup menu.
084     * @serial
085     */
086    @NotNull
087    private final JPopupMenu menu;
088
089    /**
090     * Whether this menu has been fully initialized.
091     * @serial
092     */
093    private boolean isReady;
094
095    /**
096     * The caret position in the document where this popup was opened.
097     * @serial
098     */
099    private int caretPos;
100
101    /**
102     * Creates a new instance.
103     * @param scriptEditControl the script edit control to forward to
104     */
105    public CFPythonPopup(@NotNull final ScriptEditControl scriptEditControl) {
106        setBackground(Color.white); // white background
107
108        // make sure the command list is initialized
109        if (menuEntries == null) {
110            loadCommandList();
111        }
112
113        menu = new CFPythonPopupMenu(this);
114
115        if (menuEntries != null) {
116            for (final String menuEntry : menuEntries) {
117                addItem(" " + menuEntry);
118            }
119        }
120
121        // listener for selection events
122        addActionListener(new MenuActionListener(this, scriptEditControl));
123
124        if (menuEntries != null && menuEntries.length > 0) {
125            isReady = true; // this menu is now ready for use
126        }
127
128        setRequestFocusEnabled(true);
129    }
130
131    /**
132     * Load the list of CFPython commands from the data file.
133     */
134    private static void loadCommandList() {
135        final URL url;
136        try {
137            url = IOUtils.getResource(null, PYTHON_MENU_FILE);
138        } catch (final FileNotFoundException ex) {
139            log.error("File '" + PYTHON_MENU_FILE + "': " + ex.getMessage());
140            return;
141        }
142        final List<String> cmdList = new ArrayList<String>(); // temporary list to store commands
143        try {
144            final InputStream inputStream = url.openStream();
145            try {
146                final Reader reader = new InputStreamReader(inputStream, IOUtils.MAP_ENCODING);
147                try {
148                    final BufferedReader bufferedReader = new BufferedReader(reader);
149                    try {
150                        // read file into the cmdList vector:
151                        while (true) {
152                            final String inputLine = bufferedReader.readLine();
153                            if (inputLine == null) {
154                                break;
155                            }
156                            final String line = inputLine.trim();
157                            if (!line.isEmpty() && !line.startsWith("#")) {
158                                // ATM, the descriptive info about method headers is cut out
159                                // (TODO: parse and show the full info in a status bar)
160                                final int k = line.indexOf('(');
161                                if (k > 0) {
162                                    cmdList.add(line.substring(0, k) + "()");
163                                } else {
164                                    log.error("Parse error in " + url + ":");
165                                    log.error("   \"" + line + "\" missing '()'");
166                                    cmdList.add(line + "()"); // that line is probably garbage, but will work
167                                }
168                            }
169                        }
170                        Collections.sort(cmdList, String.CASE_INSENSITIVE_ORDER);
171
172                        // now create the 'menuEntries' array
173                        if (!cmdList.isEmpty()) {
174                            menuEntries = cmdList.toArray(new String[cmdList.size()]);
175                        }
176                    } finally {
177                        bufferedReader.close();
178                    }
179                } finally {
180                    reader.close();
181                }
182            } finally {
183                inputStream.close();
184            }
185        } catch (final FileNotFoundException ex) {
186            log.error("File '" + url + "' not found: " + ex.getMessage());
187        } catch (final EOFException ignored) {
188            // expected exception, do not handle: end of file/spell struct reached
189        } catch (final UnsupportedEncodingException ex) {
190            log.error("Cannot decode file '" + url + "': " + ex.getMessage());
191        } catch (final IOException ex) {
192            log.error("Cannot read file '" + url + "': " + ex.getMessage());
193        }
194    }
195
196    /**
197     * Returns whether this popup menu has been fully initialized and is ready
198     * for use.
199     * @return <code>true</code> if initialized, otherwise <code>false</code>
200     */
201    public boolean isInitialized() {
202        return isReady;
203    }
204
205    /**
206     * Set the caret position where this menu has been invoked.
207     * @param pos caret position in the document
208     */
209    public void setCaretPosition(final int pos) {
210        caretPos = pos;
211        menu.requestFocus();
212        ScriptEditControl.registerActivePopup(this);
213    }
214
215    @NotNull
216    public JPopupMenu getMenu() {
217        return menu;
218    }
219
220    /**
221     * Subclass MenuActionListener handles the action events for the menu
222     * items.
223     */
224    private class MenuActionListener implements ActionListener {
225
226        @NotNull
227        private final ScriptEditControl control;
228
229        @NotNull
230        private final CFPythonPopup popup;
231
232        private boolean ignore; // while true, all ActionEvents get ignored
233
234        private MenuActionListener(@NotNull final CFPythonPopup popup, @NotNull final ScriptEditControl control) {
235            this.popup = popup;
236            this.control = control;
237        }
238
239        @Override
240        public void actionPerformed(@NotNull final ActionEvent e) {
241            if (!ignore) {
242                // get method name to insert
243                String method = popup.getSelectedItem().toString();
244                method = method.substring(0, method.indexOf('(')).trim() + "()";
245
246                final JEditTextArea activeTextArea = control.getActiveTextArea();
247                if (activeTextArea != null) {
248                    try {
249                        // insert method into the document
250                        activeTextArea.getDocument().insertString(caretPos, method, null);
251                    } catch (final BadLocationException ex) {
252                        log.error("BadLocationException", ex);
253                    }
254                }
255
256                ignore = true;
257                popup.setSelectedIndex(0);
258                ignore = false;
259                popup.getMenu().setVisible(false); // in some JRE versions, this doesn't happen automatically
260            }
261        }
262
263    }
264
265}