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}