001 // This file is part of the Attempto Java Packages.
002 // Copyright 2008, Attempto Group, University of Zurich (see http://attempto.ifi.uzh.ch).
003 //
004 // The Attempto Java Packages is free software: you can redistribute it and/or modify it under the
005 // terms of the GNU Lesser General Public License as published by the Free Software Foundation,
006 // either version 3 of the License, or (at your option) any later version.
007 //
008 // The Attempto Java Packages is distributed in the hope that it will be useful, but WITHOUT ANY
009 // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
010 // PURPOSE. See the GNU Lesser General Public License for more details.
011 //
012 // You should have received a copy of the GNU Lesser General Public License along with the Attempto
013 // Java Packages. If not, see http://www.gnu.org/licenses/.
014
015 package ch.uzh.ifi.attempto.preditor;
016
017 import java.util.ArrayList;
018 import java.util.Arrays;
019 import java.util.List;
020
021 import nextapp.echo2.app.Alignment;
022 import nextapp.echo2.app.ApplicationInstance;
023 import nextapp.echo2.app.Border;
024 import nextapp.echo2.app.Button;
025 import nextapp.echo2.app.Color;
026 import nextapp.echo2.app.Column;
027 import nextapp.echo2.app.Extent;
028 import nextapp.echo2.app.Font;
029 import nextapp.echo2.app.Insets;
030 import nextapp.echo2.app.Row;
031 import nextapp.echo2.app.SplitPane;
032 import nextapp.echo2.app.TextArea;
033 import nextapp.echo2.app.event.ActionEvent;
034 import nextapp.echo2.app.event.ActionListener;
035 import ch.uzh.ifi.attempto.chartparser.ChartParser;
036 import ch.uzh.ifi.attempto.chartparser.Grammar;
037 import ch.uzh.ifi.attempto.echocomp.GeneralButton;
038 import ch.uzh.ifi.attempto.echocomp.Label;
039 import ch.uzh.ifi.attempto.echocomp.Style;
040 import ch.uzh.ifi.attempto.echocomp.TextField;
041 import ch.uzh.ifi.attempto.echocomp.WindowPane;
042 import ch.uzh.ifi.attempto.preditor.text.TextContainer;
043 import ch.uzh.ifi.attempto.preditor.text.TextElement;
044 import echopointng.KeyStrokeListener;
045
046 /**
047 * This class represents a predictive editor window. The predictive editor enables easy creation of texts that
048 * comply with a certain grammar. The users can create such a text word-by-word by clicking on one of different
049 * menu items. The menu items are structured into menu blocks each of which has a name that is displayed above
050 * the menu block.
051 *
052 * @author Tobias Kuhn
053 */
054 public class PreditorWindow extends WindowPane implements ActionListener {
055
056 private static final long serialVersionUID = -7815494421993305554L;
057
058 private List<MenuBlockContent> menuBlockContents = new ArrayList<MenuBlockContent>();
059 private List<MenuBlock> menuBlocksTop = new ArrayList<MenuBlock>();
060 private List<MenuBlock> menuBlocksBottom = new ArrayList<MenuBlock>();
061 private List<SplitPane> menuSplitPanesTop = new ArrayList<SplitPane>();
062 private List<SplitPane> menuSplitPanesBottom = new ArrayList<SplitPane>();
063
064 private TextArea textArea = new TextArea();
065 private TextField textField;
066 private SplitPane menuBlockPane = new SplitPane(SplitPane.ORIENTATION_HORIZONTAL_LEFT_RIGHT, new Extent(0));
067 private SplitPane doubleColumnMenuPane = new SplitPane(SplitPane.ORIENTATION_VERTICAL_TOP_BOTTOM, new Extent(258));
068 private GeneralButton deleteButton = new GeneralButton("< Delete", 70, this);
069
070 private KeyStrokeListener keyStrokeListener = new KeyStrokeListener();
071
072 private TextContainer newTextContainer = new TextContainer();
073 private MenuCreator menuCreator;
074
075 private ChartParser parser;
076
077 private Button okButton = new GeneralButton("OK", 70, this);
078 private Button cancelButton = new GeneralButton("Cancel", 70, this);
079
080 private ArrayList<ActionListener> actionListeners = new ArrayList<ActionListener>();
081
082 /**
083 * Creates a new predictive editor window for the given grammar using the given menu creator.
084 *
085 * @param title The title of the window.
086 * @param grammar The grammar to be used.
087 * @param menuCreator The menu creator to be used.
088 */
089 public PreditorWindow(String title, Grammar grammar, MenuCreator menuCreator) {
090 this.parser = new ChartParser(grammar);
091 this.menuCreator = menuCreator;
092
093 setModal(true);
094 setTitle(title);
095 setTitleFont(new Font(Style.fontTypeface, Font.ITALIC, new Extent(13)));
096 setWidth(new Extent(753));
097 setHeight(new Extent(503));
098 setResizable(false);
099 setTitleBackground(Style.windowTitleBackground);
100 setStyleName("Default");
101
102 Row buttonBar = new Row();
103 buttonBar.setAlignment(new Alignment(Alignment.RIGHT, Alignment.CENTER));
104 buttonBar.setInsets(new Insets(10, 17, 10, 10));
105 buttonBar.setCellSpacing(new Extent(5));
106 buttonBar.add(okButton);
107 buttonBar.add(cancelButton);
108
109 SplitPane splitPane = new SplitPane(SplitPane.ORIENTATION_VERTICAL_BOTTOM_TOP, new Extent(47));
110 splitPane.add(buttonBar);
111 add(splitPane);
112
113 SplitPane editorPane = new SplitPane(SplitPane.ORIENTATION_VERTICAL_TOP_BOTTOM, new Extent(148));
114
115 Column textColumn = new Column();
116 textColumn.setInsets(new Insets(10, 10, 0, 0));
117 textColumn.setCellSpacing(new Extent(10));
118
119 Column textAreaColumn = new Column();
120
121 textArea.setWidth(new Extent(702));
122 textArea.setHeight(new Extent(42));
123 textArea.setEnabled(false);
124 textArea.setFont(new Font(Style.fontTypeface, Font.PLAIN, new Extent(12)));
125 textArea.setBorder(new Border(1, new Color(180, 180, 180), Border.STYLE_SOLID));
126 textArea.setInsets(new Insets(4, 4));
127 textArea.setBackground(new Color(255, 255, 255));
128 textArea.setFocusTraversalParticipant(false);
129 textAreaColumn.add(textArea);
130
131 Row textAreaButtonBar = new Row();
132 textAreaButtonBar.setAlignment(new Alignment(Alignment.RIGHT, Alignment.CENTER));
133 textAreaButtonBar.setInsets(new Insets(0, 5, 10, 0));
134 textAreaButtonBar.setCellSpacing(new Extent(5));
135 deleteButton.setFocusTraversalParticipant(false);
136 textAreaButtonBar.add(deleteButton);
137 textAreaColumn.add(textAreaButtonBar);
138
139 textColumn.add(textAreaColumn);
140
141 Column textFieldColumn = new Column();
142 textFieldColumn.setCellSpacing(new Extent(1));
143 Label textFieldLabel = new Label("text", Font.ITALIC, 11);
144 textFieldColumn.add(textFieldLabel);
145
146 textField = new TextField(this);
147 textField.setWidth(new Extent(708));
148 textField.setFocusTraversalParticipant(true);
149 textField.setFocusTraversalIndex(0);
150 textField.setDisabledBackground(Style.lightDisabled);
151 Row textFieldRow = new Row();
152 textFieldRow.add(textField);
153 TextField dummyTextField = new TextField();
154 dummyTextField.setWidth(new Extent(1));
155 dummyTextField.setBorder(new Border(0, null, 0));
156 dummyTextField.setBackground(Color.WHITE);
157 textFieldRow.add(dummyTextField);
158 textFieldColumn.add(textFieldRow);
159
160 keyStrokeListener.addKeyCombination(KeyStrokeListener.VK_TAB, "Tab");
161 keyStrokeListener.addKeyCombination(KeyStrokeListener.VK_RIGHT, "Tab");
162 keyStrokeListener.addActionListener(this);
163 textFieldColumn.add(keyStrokeListener);
164
165 textColumn.add(textFieldColumn);
166 editorPane.add(textColumn);
167
168 menuBlockPane.setSeparatorWidth(new Extent(10));
169 menuBlockPane.setSeparatorColor(Color.WHITE);
170 menuBlockPane.add(new Label());
171
172 SplitPane parentSplitPane = doubleColumnMenuPane;
173 for (int i=0; i<10; i++) {
174 MenuBlock menuBlock = new MenuBlock(this, this);
175 menuBlocksTop.add(menuBlock);
176 SplitPane menuSplitPane = new SplitPane(SplitPane.ORIENTATION_HORIZONTAL_LEFT_RIGHT);
177 menuSplitPane.setSeparatorWidth(new Extent(10));
178 menuSplitPane.setSeparatorColor(Color.WHITE);
179 menuSplitPane.setVisible(false);
180 menuSplitPane.add(menuBlock);
181 menuSplitPanesTop.add(menuSplitPane);
182 parentSplitPane.add(menuSplitPane);
183 parentSplitPane = menuSplitPane;
184 }
185
186 parentSplitPane = doubleColumnMenuPane;
187 for (int i=0; i<10; i++) {
188 MenuBlock menuBlock = new MenuBlock(this, this);
189 menuBlocksBottom.add(menuBlock);
190 SplitPane menuSplitPane = new SplitPane(SplitPane.ORIENTATION_HORIZONTAL_LEFT_RIGHT);
191 menuSplitPane.setSeparatorWidth(new Extent(10));
192 menuSplitPane.setSeparatorColor(Color.WHITE);
193 menuSplitPane.setVisible(false);
194 menuSplitPane.add(menuBlock);
195 menuSplitPanesBottom.add(menuSplitPane);
196 parentSplitPane.add(menuSplitPane);
197 parentSplitPane = menuSplitPane;
198 }
199
200 doubleColumnMenuPane.setSeparatorHeight(new Extent(12));
201 doubleColumnMenuPane.setSeparatorColor(Color.WHITE);
202 menuBlockPane.add(doubleColumnMenuPane);
203 editorPane.add(menuBlockPane);
204 splitPane.add(editorPane);
205
206 update();
207 }
208
209 /**
210 * Returns the (partial) text that has been entered .
211 *
212 * @return The (partial) text in the form of a text container.
213 */
214 public TextContainer getTextContainer() {
215 return newTextContainer;
216 }
217
218 /**
219 * Returns a text element with the given content if it is a possible next token.
220 *
221 * @param content The content of the text element.
222 * @return The text element.
223 */
224 public TextElement getPossibleNextToken(String content) {
225 for (MenuBlockContent m : menuBlockContents) {
226 TextElement e = m.getEntry(content);
227 if (e != null) return e;
228 }
229 return null;
230 }
231
232 /**
233 * Adds the text element to the end of the text.
234 *
235 * @param te The text element to be added.
236 */
237 public void addTextElement(TextElement te) {
238 textElementSelected(te);
239 textField.setText("");
240 update();
241 }
242
243 /**
244 * Reads the text and adds it to the end of the current text as far as possible.
245 *
246 * @param text The text to be added.
247 */
248 public void addText(String text) {
249 handleTextInput(tokenize(text));
250 update();
251 }
252
253 private void textElementSelected(TextElement te) {
254 newTextContainer.addElement(te);
255 parser.addToken(te.getCategory());
256 log("edit", "words added: " + te.getText());
257 }
258
259 private int getElementsCount() {
260 int c = 0;
261 for (MenuBlockContent m : menuBlockContents) {
262 c += m.getEntries().size();
263 }
264 return c;
265 }
266
267 private String getStartString() {
268 String startString = "";
269 ArrayList<String> blockStartStrings = new ArrayList<String>();
270
271 for (MenuBlockContent mc : menuBlockContents) {
272 String s = mc.getStartString();
273 if (s != null) {
274 blockStartStrings.add(s);
275 }
276 }
277
278 if (blockStartStrings.isEmpty()) return null;
279
280 String first = blockStartStrings.get(0);
281 blockStartStrings.remove(0);
282
283 if (blockStartStrings.isEmpty()) return first;
284
285 for (int i = 0; i < first.length(); i++) {
286 char c = first.charAt(i);
287 boolean stop = false;
288 for (String s : blockStartStrings) {
289 if (s.length() <= i || s.charAt(i) != c) stop = true;
290 }
291 if (stop) break;
292 startString += c;
293 }
294
295 return startString;
296 }
297
298 private void update() {
299 updateMenuBlockContents();
300 setFilter(textField.getText());
301 int mbCount = menuBlockContents.size();
302 if (mbCount < 5) {
303 int width = ( 720 / ( mbCount > 3 ? mbCount : 3 ) ) - 10;
304 for (int i=0; i < menuBlocksTop.size(); i++) {
305 if (menuBlockContents.size() > i) {
306 menuBlocksTop.get(i).setContent(menuBlockContents.get(i), width, 16);
307 menuSplitPanesTop.get(i).setSeparatorPosition(new Extent(width));
308 menuSplitPanesTop.get(i).setVisible(true);
309 } else {
310 menuSplitPanesTop.get(i).setVisible(false);
311 }
312 }
313 doubleColumnMenuPane.setSeparatorPosition(new Extent(258));
314 } else {
315 int firstRowCount = (mbCount + 1) / 2;
316 int width = ( 720 / firstRowCount ) - 10;
317 for (int i=0; i < menuBlocksTop.size(); i++) {
318 if (i < firstRowCount) {
319 menuBlocksTop.get(i).setContent(menuBlockContents.get(i), width, 7);
320 menuSplitPanesTop.get(i).setSeparatorPosition(new Extent(width));
321 menuSplitPanesTop.get(i).setVisible(true);
322 } else {
323 menuSplitPanesTop.get(i).setVisible(false);
324 }
325 }
326 for (int i=0; i < menuBlocksBottom.size(); i++) {
327 if (firstRowCount + i < mbCount) {
328 menuBlocksBottom.get(i).setContent(menuBlockContents.get(firstRowCount + i), width, 7);
329 menuSplitPanesBottom.get(i).setSeparatorPosition(new Extent(width));
330 menuSplitPanesBottom.get(i).setVisible(true);
331 } else {
332 menuSplitPanesBottom.get(i).setVisible(false);
333 }
334 }
335 doubleColumnMenuPane.setSeparatorPosition(new Extent(123));
336 }
337 textField.setEnabled(menuBlockContents.size() > 0 || !textField.getText().equals(""));
338 ApplicationInstance.getActive().setFocusedComponent(textField);
339 deleteButton.setEnabled(textArea.getText().length() > 0);
340 }
341
342 private void updateMenuBlockContents() {
343 textArea.setText(newTextContainer.getText());
344 menuBlockContents.clear();
345 List<MenuBlockContent> newMenuBlockContents = menuCreator.createMenu(parser, newTextContainer);
346 for (MenuBlockContent c : newMenuBlockContents) {
347 if (!c.isEmpty()) {
348 menuBlockContents.add(c);
349 }
350 }
351 }
352
353 private void setFilter(String filter) {
354 for (MenuBlockContent c : menuBlockContents) {
355 c.setFilter(filter);
356 }
357 }
358
359 private void handleTextInput() {
360 handleTextInput(tokenize(textField.getText()));
361 }
362
363 private void handleTextInput(ArrayList<String> tokens) {
364
365 TextElement t = null;
366 String s = "";
367 int c = 0;
368
369 for (String token : tokens) {
370 setFilter(s + token);
371 TextElement t2 = getPossibleNextToken(s + token);
372 int elCount = getElementsCount();
373 if (t2 != null) {
374 if (elCount == 1 && c < tokens.size()-1) {
375 t = t2;
376 c++;
377 break;
378 } else if (c < tokens.size()-1) {
379 t = t2;
380 }
381 }
382 if (elCount == 0) {
383 break;
384 } else if (c == tokens.size()-1) { // && token.length() > 0) {
385 t = null;
386 c++;
387 break;
388 } else {
389 s += token + " ";
390 c++;
391 }
392 }
393
394 setFilter(null);
395
396 if (t != null) {
397 textElementSelected(t);
398 updateMenuBlockContents();
399 for (int i=0; i<c; i++) tokens.remove(0);
400 handleTextInput(tokens);
401 } else {
402 String text = "";
403 for (String token : tokens) {
404 if (text.length() > 0) text += " ";
405 text += token;
406 }
407 textField.setText(text);
408 }
409 }
410
411 private ArrayList<String> tokenize(String text) {
412 text = text.replaceAll("\\.", " . ");
413 text = text.replaceAll("\\?", " ? ");
414 text = text.replaceAll("\\!", " ! ");
415
416 ArrayList<String> tokens = new ArrayList<String>(Arrays.asList(text.split(" ")));
417
418 while (tokens.contains("")) {
419 tokens.remove("");
420 }
421
422 if (text.endsWith(" ")) {
423 tokens.add("");
424 }
425
426 return tokens;
427 }
428
429 /**
430 * Adds a new action-listener.
431 *
432 * @param actionListener The new action-listener.
433 */
434 public void addActionListener(ActionListener actionListener) {
435 actionListeners.add(actionListener);
436 }
437
438 /**
439 * Removes the action-listener.
440 *
441 * @param actionListener The action-listener to be removed.
442 */
443 public void removeActionListener(ActionListener actionListener) {
444 actionListeners.remove(actionListener);
445 }
446
447 /**
448 * Removes all action-listeners.
449 */
450 public void removeAllActionListeners() {
451 actionListeners.clear();
452 }
453
454 private void notifyActionListeners(ActionEvent event) {
455 for (ActionListener al : actionListeners) {
456 al.actionPerformed(event);
457 }
458 }
459
460 public void actionPerformed(ActionEvent e) {
461 if (e.getSource() == cancelButton) {
462 log("edit", "pressed: cancel");
463 notifyActionListeners(new ActionEvent(this, "Cancel"));
464 return;
465 } else if (e.getSource() == okButton) {
466 log("edit", "pressed: ok");
467 handleTextInput();
468 update();
469 notifyActionListeners(new ActionEvent(this, "OK"));
470 return;
471 } else if (e.getSource() == deleteButton) {
472 log("edit", "pressed: < delete");
473 newTextContainer.removeLastElement();
474 parser.removeToken();
475 textField.setText("");
476 } else if (e.getSource() instanceof MenuEntry) {
477 TextElement te = ((MenuEntry) e.getSource()).getTextElement();
478 log("edit", "pressed: menu-entry " + te.getText());
479 textElementSelected(te);
480 textField.setText("");
481 } else if (e.getSource() == textField) {
482 log("edit", "pressed: enter-key");
483 handleTextInput();
484 TextElement te = getPossibleNextToken(textField.getText());
485 if (te != null) {
486 textElementSelected(te);
487 textField.setText("");
488 }
489 } else if ("Tab".equals(e.getActionCommand())) {
490 log("edit", "pressed: tab-key");
491 handleTextInput();
492 }
493
494 update();
495
496 if ("Tab".equals(e.getActionCommand())) {
497 String s = getStartString();
498 if (s != null) textField.setText(s);
499 }
500 }
501
502 private void log(String type, String text) {
503 // No logging done at the moment
504 }
505
506 public String toString() {
507 return "sentence: " + newTextContainer.getText();
508 }
509
510 }