001 // This file is part of AceWiki.
002 // Copyright 2008-2012, AceWiki developers.
003 //
004 // AceWiki is free software: you can redistribute it and/or modify it under the terms of the GNU
005 // Lesser General Public License as published by the Free Software Foundation, either version 3 of
006 // the License, or (at your option) any later version.
007 //
008 // AceWiki is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
009 // even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
010 // Lesser General Public License for more details.
011 //
012 // You should have received a copy of the GNU Lesser General Public License along with AceWiki. If
013 // 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.HashMap;
019 import java.util.List;
020 import java.util.Map;
021
022 import nextapp.echo.app.Alignment;
023 import nextapp.echo.app.ApplicationInstance;
024 import nextapp.echo.app.Border;
025 import nextapp.echo.app.Button;
026 import nextapp.echo.app.Color;
027 import nextapp.echo.app.Column;
028 import nextapp.echo.app.Extent;
029 import nextapp.echo.app.Font;
030 import nextapp.echo.app.Grid;
031 import nextapp.echo.app.Insets;
032 import nextapp.echo.app.Row;
033 import nextapp.echo.app.event.ActionEvent;
034 import nextapp.echo.app.event.ActionListener;
035 import nextapp.echo.app.event.WindowPaneEvent;
036 import nextapp.echo.app.event.WindowPaneListener;
037 import nextapp.echo.app.layout.GridLayoutData;
038 import ch.uzh.ifi.attempto.base.ConcreteOption;
039 import ch.uzh.ifi.attempto.base.DefaultTextOperator;
040 import ch.uzh.ifi.attempto.base.Logger;
041 import ch.uzh.ifi.attempto.base.NextTokenOptions;
042 import ch.uzh.ifi.attempto.base.PredictiveParser;
043 import ch.uzh.ifi.attempto.base.TextContainer;
044 import ch.uzh.ifi.attempto.base.TextElement;
045 import ch.uzh.ifi.attempto.base.TextOperator;
046 import ch.uzh.ifi.attempto.echocomp.GeneralButton;
047 import ch.uzh.ifi.attempto.echocomp.Label;
048 import ch.uzh.ifi.attempto.echocomp.Style;
049 import ch.uzh.ifi.attempto.echocomp.TabSensitiveTextField;
050 import ch.uzh.ifi.attempto.echocomp.TextField;
051 import echopoint.DirectHtml;
052
053 //import static ch.uzh.ifi.attempto.echocomp.KeyStrokes.*;
054
055 /**
056 * This class represents a predictive editor window. The predictive editor enables easy creation of
057 * texts that comply with a certain grammar. The users can create such a text word-by-word by
058 * clicking on one of different menu items. The menu items are structured into menu blocks each of
059 * which has a name that is displayed above the menu block.
060 *
061 * @author Tobias Kuhn
062 */
063 public class PreditorWindow extends nextapp.echo.app.WindowPane implements ActionListener, WindowPaneListener {
064
065 private static final long serialVersionUID = -7815494421993305554L;
066
067 private final TextContainer textContainer = new TextContainer();
068 private MenuCreator menuCreator;
069 private TextOperator textOperator;
070 private PredictiveParser parser;
071 private List<ActionListener> actionListeners = new ArrayList<ActionListener>();
072 private Logger logger;
073
074 private MenuBlockManager menuBlockManager;
075 private MenuBlock enlargedMenuBlock;
076
077 private DirectHtml textArea = new DirectHtml();
078 private TabSensitiveTextField textField;
079 private TextField dummyTextField;
080 private Column menuBlockArea;
081 private GeneralButton deleteButton = new GeneralButton("< Delete", 70, this);
082 private GeneralButton clearButton = new GeneralButton("Clear", 70, this);
083 private Button okButton = new GeneralButton("OK", 70, this);
084 private Button cancelButton = new GeneralButton("Cancel", 70, this);
085
086 private String textAreaStartText = "";
087 private String textAreaEndText = "<span style=\"color: rgb(150, 150, 150)\"> ...</span>";
088
089 // TODO: reactive key combinations
090 // private KeyStrokeListener keyStrokeListener = new KeyStrokeListener();
091
092 private boolean isInitialized = false;
093
094 /**
095 * Creates a new predictive editor window using the given predictive parser.
096 *
097 * @param title The title of the window.
098 * @param parser The predictive parser to be used. Do not modify this object while the
099 * preditor window is active!
100 */
101 public PreditorWindow(String title, PredictiveParser parser) {
102 this.parser = parser;
103 this.menuBlockManager = new MenuBlockManager(this);
104
105 addWindowPaneListener(this);
106 setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
107 setModal(true);
108 setTitle(title);
109 setTitleFont(new Font(Style.fontTypeface, Font.ITALIC, new Extent(13)));
110 setWidth(new Extent(753));
111 setHeight(new Extent(523));
112 setResizable(false);
113 setTitleBackground(Style.windowTitleBackground);
114 setStyleName("Default");
115
116 Grid grid = new Grid(1);
117 grid.setColumnWidth(0, new Extent(730));
118 add(grid);
119
120 GridLayoutData layout = new GridLayoutData();
121 layout.setAlignment(new Alignment(Alignment.LEFT, Alignment.TOP));
122
123 Column textAreaColumn = new Column();
124 textAreaColumn.setInsets(new Insets(10, 10, 10, 0));
125 textAreaColumn.add(textArea);
126 textAreaColumn.setLayoutData(layout);
127 grid.setRowHeight(0, new Extent(68));
128 grid.add(textAreaColumn);
129
130 Column textColumn = new Column();
131 textColumn.setInsets(new Insets(10, 10, 0, 0));
132 textColumn.setCellSpacing(new Extent(10));
133
134 Row textAreaButtonBar = new Row();
135 textAreaButtonBar.setAlignment(new Alignment(Alignment.RIGHT, Alignment.CENTER));
136 textAreaButtonBar.setInsets(new Insets(0, 5, 10, 0));
137 textAreaButtonBar.setCellSpacing(new Extent(5));
138 clearButton.setVisible(false);
139 textAreaButtonBar.add(clearButton);
140 textAreaButtonBar.add(deleteButton);
141 textColumn.add(textAreaButtonBar);
142
143 Column textFieldColumn = new Column();
144 textFieldColumn.setCellSpacing(new Extent(1));
145 Label textFieldLabel = new Label("text", Font.ITALIC, 11);
146 textFieldColumn.add(textFieldLabel);
147
148 textField = new TabSensitiveTextField(this);
149 textField.setWidth(new Extent(708));
150 textField.setDisabledBackground(Style.lightDisabled);
151 Row textFieldRow = new Row();
152 textFieldRow.add(textField);
153 dummyTextField = new TextField();
154 dummyTextField.setWidth(new Extent(1));
155 dummyTextField.setBorder(new Border(0, Color.BLACK, 0));
156 dummyTextField.setBackground(Color.WHITE);
157 textFieldRow.add(dummyTextField);
158 textFieldColumn.add(textFieldRow);
159
160 // keyStrokeListener.addKeyCombination(VK_TAB, "Tab");
161 // keyStrokeListener.addKeyCombination(VK_ESCAPE, "Esc");
162 // keyStrokeListener.addKeyCombination(VK_BACK_SPACE | CONTROL_MASK, "Ctrl-Backspace");
163 // keyStrokeListener.addActionListener(this);
164 // textFieldColumn.add(keyStrokeListener);
165
166 textColumn.add(textFieldColumn);
167 grid.setRowHeight(1, new Extent(88));
168 grid.add(textColumn);
169
170 menuBlockArea = new Column();
171 menuBlockArea.setInsets(new Insets(10, 15, 0, 0));
172 grid.setRowHeight(2, new Extent(275));
173 grid.add(menuBlockArea);
174
175 Row buttonBar = new Row();
176 buttonBar.setAlignment(new Alignment(Alignment.RIGHT, Alignment.TOP));
177 buttonBar.setInsets(new Insets(10, 10, 10, 0));
178 buttonBar.setCellSpacing(new Extent(5));
179 buttonBar.add(okButton);
180 buttonBar.add(cancelButton);
181 grid.setRowHeight(3, new Extent(30));
182 grid.add(buttonBar);
183
184 update();
185 }
186
187 /**
188 * Sets the menu creator. {@link DefaultMenuCreator} is used by default.
189 *
190 * @param menuCreator The menu creator.
191 */
192 public void setMenuCreator(MenuCreator menuCreator) {
193 this.menuCreator = menuCreator;
194 update();
195 }
196
197 /**
198 * Returns the menu creator.
199 *
200 * @return The menu creator.
201 */
202 public MenuCreator getMenuCreator() {
203 if (menuCreator == null) {
204 setMenuCreator(new DefaultMenuCreator());
205 }
206 return menuCreator;
207 }
208
209 /**
210 * Sets the text operator. {@link DefaultTextOperator} is used by default.
211 *
212 * @param textOperator The text operator.
213 */
214 public void setTextOperator(TextOperator textOperator) {
215 this.textOperator = textOperator;
216 textContainer.setTextOperator(textOperator);
217 }
218
219 /**
220 * Returns the text operator.
221 *
222 * @return The text operator.
223 */
224 public TextOperator getTextOperator() {
225 if (textOperator == null) {
226 setTextOperator(new DefaultTextOperator());
227 }
228 return textOperator;
229 }
230
231 /**
232 * Shows or hides the "clear" button.
233 *
234 * @param visible true to show the "clear" button; false to hide it.
235 */
236 public void setClearButtonVisible(boolean visible) {
237 clearButton.setVisible(visible);
238 }
239
240 /**
241 * Sets the text to be shown in the text area in front of the text entered by the user. The
242 * default is an empty string.
243 *
244 * @param textAreaStartText The text, possibly enriched with HTML tags.
245 */
246 public void setTextAreaStartText(String textAreaStartText) {
247 this.textAreaStartText = textAreaStartText;
248 }
249
250 /**
251 * Sets the text to be shown in the text area at the end of the text entered by the user. The
252 * default are three gray dots "...".
253 *
254 * @param textAreaEndText The text, possibly enriched with HTML tags.
255 */
256 public void setTextAreaEndText(String textAreaEndText) {
257 this.textAreaEndText = textAreaEndText;
258 }
259
260 /**
261 * Returns a copy of the text container object that contains the (partial) text that has been
262 * entered.
263 *
264 * @return A copy of the text container object.
265 */
266 public TextContainer getTextContainer() {
267 return textContainer.clone();
268 }
269
270 /**
271 * Returns the number of tokens of the current (partial) text.
272 *
273 * @return The number of tokens.
274 */
275 public int getTokenCount() {
276 return parser.getTokenCount();
277 }
278
279 /**
280 * Returns whether the given token is a possible next token.
281 *
282 * @param token The token.
283 * @return true if it is a possible next token.
284 */
285 public boolean isPossibleNextToken(String token) {
286 return parser.isPossibleNextToken(token);
287 }
288
289 /**
290 * Adds the text element to the end of the text.
291 *
292 * @param te The text element to be added.
293 */
294 public void addTextElement(TextElement te) {
295 textElementSelected(te);
296 textField.setText("");
297 update();
298 }
299
300 /**
301 * Reads the text and adds it to the end of the current text as far as possible.
302 *
303 * @param text The text to be added.
304 */
305 public void addText(String text) {
306 handleTextInput(text, true);
307 update();
308 }
309
310 private void textElementSelected(TextElement te) {
311 textContainer.addElement(te);
312 parser.addToken(te.getOriginalText());
313
314 log("words added: " + te);
315 }
316
317 private void update() {
318 if (!isInitialized) return;
319 updateMenuBlockContents();
320 menuBlockArea.removeAll();
321 menuBlockManager.setFilter(textField.getText());
322 if (enlargedMenuBlock != null) {
323 // One enlarged menu block
324 MenuBlockContent mbc = enlargedMenuBlock.getContent();
325 int cs = menuCreator.getColorShift(mbc.getName());
326 enlargedMenuBlock = new MenuBlock(708, 240, cs, this);
327 enlargedMenuBlock.setContent(mbc);
328 enlargedMenuBlock.setEnlarged(true);
329 menuBlockArea.add(enlargedMenuBlock);
330 } else {
331 menuBlockArea.add(menuBlockManager.createGUI());
332 }
333 textField.setEnabled(menuBlockManager.getMenuBlockCount() > 0 || !textField.getText().equals(""));
334 ApplicationInstance.getActive().setFocusedComponent(textField);
335 clearButton.setEnabled(getTokenCount() > 0);
336 deleteButton.setEnabled(getTokenCount() > 0);
337 }
338
339 private void updateMenuBlockContents() {
340 int ref = parser.getReference();
341 String t = "";
342 TextElement prev = null;
343 for (int i = 0; i < getTokenCount() ; i++) {
344 TextElement te = textContainer.getTextElement(i);
345 String glue = "";
346 if (prev != null) {
347 glue = getTextOperator().getGlue(prev, te);
348 }
349 if (ref > -1 && (ref == i || i == getTokenCount()-1)) {
350 t += glue + "<u>" + te.getText() + "</u>";
351 } else {
352 t += glue + te.getText();
353 }
354 prev = te;
355 }
356 if (t.startsWith(" ")) t = t.substring(1);
357 textArea.setText(
358 "<div style=\"font-family: Verdana,Arial,Helvetica,Sans-Serif; font-size: 12px\">" +
359 textAreaStartText +
360 t +
361 textAreaEndText +
362 "</div>"
363 );
364
365 menuBlockManager.clear();
366
367 NextTokenOptions options = parser.getNextTokenOptions();
368 HashMap<String, MenuBlockContent> contentsMap = new HashMap<String, MenuBlockContent>();
369 for (MenuItem m : getMenuCreator().createSpecialMenuItems(options)) {
370 addMenuItem(m, contentsMap);
371 }
372 for (ConcreteOption o : options.getConcreteOptions()) {
373 addMenuItem(getMenuCreator().createMenuEntry(o), contentsMap);
374 }
375
376 for (String mg : getMenuCreator().getMenuGroupOrdering()) {
377 if (contentsMap.containsKey(mg)) {
378 menuBlockManager.addMenuBlockContent(contentsMap.get(mg));
379 }
380 }
381 for (String mg : contentsMap.keySet()) {
382 if (!getMenuCreator().getMenuGroupOrdering().contains(mg)) {
383 menuBlockManager.addMenuBlockContent(contentsMap.get(mg));
384 }
385 }
386 }
387
388 private void addMenuItem(MenuItem menuItem, Map<String, MenuBlockContent> contentsMap) {
389 String menuGroup = menuItem.getMenuGroup();
390 MenuBlockContent mbc;
391 if (contentsMap.containsKey(menuGroup)) {
392 mbc = contentsMap.get(menuGroup);
393 } else {
394 mbc = new MenuBlockContent(menuGroup);
395 mbc.setComparator(getMenuCreator().getMenuItemComparator());
396 mbc.setActionListener(this);
397 contentsMap.put(menuGroup, mbc);
398 }
399 mbc.addItem(menuItem);
400 }
401
402 private void handleTextInput(boolean enterPressed) {
403 handleTextInput(textField.getText(), enterPressed);
404 }
405
406 private void handleTextInput(String text, boolean enterPressed) {
407 List<String> subtokens = getTextOperator().splitIntoTokens(text);
408 boolean force = enterPressed && (text.equals(menuBlockManager.getFilter()) || text.endsWith(" "));
409 handleTokenInput(subtokens, force, true);
410 }
411
412 private void handleTokenInput(List<String> subtokens, boolean force, boolean caseSensitive) {
413 if (subtokens.size() == 0) {
414 textField.setText("");
415 return;
416 }
417
418 String filter = "";
419 for (String s : subtokens) filter += s + " ";
420 menuBlockManager.setFilter(filter);
421
422 String text = "";
423 TextElement textElement = null;
424 List<String> rest = null;
425 List<String> s = new ArrayList<String>(subtokens);
426 while (s.size() > 0) {
427 if (text.length() > 0) text += " ";
428 text += s.remove(0);
429 TextElement te = null;
430 if (caseSensitive) {
431 te = getTextOperator().createTextElement(text);
432 } else {
433 String t = proposeToken(text);
434 if (t != null) {
435 te = getTextOperator().createTextElement(t);
436 }
437 }
438 if (te != null && parser.isPossibleNextToken(te.getOriginalText())) {
439 textElement = te;
440 rest = new ArrayList<String>(s);
441 }
442 }
443 if (textElement != null) {
444 if ((rest.isEmpty() && force) || (!rest.isEmpty() &&
445 menuBlockManager.getMenuEntryCount() == 0)) {
446 textElementSelected(textElement);
447 updateMenuBlockContents();
448 handleTokenInput(rest, force, caseSensitive);
449 return;
450 }
451 }
452 if (caseSensitive) {
453 handleTokenInput(subtokens, force, false);
454 } else {
455 textField.setText(text);
456 }
457 }
458
459 private String proposeToken(String text) {
460 text = text.toLowerCase().replaceAll("\\s+", "_");
461 for (ConcreteOption o : parser.getNextTokenOptions().getConcreteOptions()) {
462 String t = o.getWord().toLowerCase().replaceAll("\\s+", "_");
463 if (t.equals(text)) {
464 return o.getWord();
465 }
466 }
467 return null;
468 }
469
470 /**
471 * Adds a new action-listener.
472 *
473 * @param actionListener The new action-listener.
474 */
475 public void addActionListener(ActionListener actionListener) {
476 actionListeners.add(actionListener);
477 }
478
479 /**
480 * Removes the action-listener.
481 *
482 * @param actionListener The action-listener to be removed.
483 */
484 public void removeActionListener(ActionListener actionListener) {
485 actionListeners.remove(actionListener);
486 }
487
488 /**
489 * Removes all action-listeners.
490 */
491 public void removeAllActionListeners() {
492 actionListeners.clear();
493 }
494
495 private void notifyActionListeners(ActionEvent event) {
496 for (ActionListener al : actionListeners) {
497 al.actionPerformed(event);
498 }
499 }
500
501 public void actionPerformed(ActionEvent e) {
502 Object src = e.getSource();
503 String c = e.getActionCommand();
504
505 if (enlargedMenuBlock != null) {
506 enlargedMenuBlock.setEnlarged(false);
507 enlargedMenuBlock = null;
508 }
509
510 boolean tabKeyPressed = false;
511
512 if (src == cancelButton) {
513 log("pressed: cancel");
514 notifyActionListeners(new ActionEvent(this, "Cancel"));
515 return;
516 } else if (src == okButton) {
517 log("pressed: ok");
518 handleTextInput(true);
519 update();
520 notifyActionListeners(new ActionEvent(this, "OK"));
521 return;
522 } else if (src == deleteButton) {
523 log("pressed: < delete");
524 removeLastToken();
525 } else if (src == clearButton) {
526 log("pressed: clear");
527 clearTokens();
528 } else if (src == textField) {
529 if (getApplicationInstance().getFocusedComponent() == dummyTextField) {
530 log("pressed: tab-key");
531 handleTextInput(false);
532 tabKeyPressed = true;
533 } else {
534 log("pressed: enter-key");
535 if (textField.getText().equals("") && menuBlockManager.getFilter().equals("")) {
536 notifyActionListeners(new ActionEvent(this, "Enter"));
537 return;
538 } else {
539 handleTextInput(true);
540 }
541 }
542 } else if (src instanceof MenuEntry) {
543 TextElement te = ((MenuEntry) e.getSource()).getTextElement();
544 log("pressed: menu-entry " + te.getText());
545 textElementSelected(te);
546 textField.setText("");
547 } else if ("enlarge".equals(c) && src instanceof MenuBlock) {
548 enlargedMenuBlock = (MenuBlock) src;
549 } else if ("Esc".equals(c)) {
550 log("pressed: escape key");
551 notifyActionListeners(new ActionEvent(this, "Escape"));
552 return;
553 } else if ("Ctrl-Backspace".equals(c)) {
554 log("pressed: ctrl-backspace");
555 if (getTokenCount() > 0) {
556 textContainer.removeLastElement();
557 parser.removeToken();
558 textField.setText("");
559 }
560 }
561
562 update();
563
564 if (tabKeyPressed) {
565 String s = menuBlockManager.getStartString();
566 if (s != null) textField.setText(s);
567 }
568 }
569
570 /**
571 * Removes the last token.
572 */
573 public void removeLastToken() {
574 if (getTokenCount() > 0) {
575 textContainer.removeLastElement();
576 parser.removeToken();
577 textField.setText("");
578 }
579 }
580
581 /**
582 * Removes all tokens.
583 */
584 public void clearTokens() {
585 textContainer.removeAllElements();
586 parser.removeAllTokens();
587 textField.setText("");
588 }
589
590 /**
591 * Returns true if the current text is a complete statement.
592 *
593 * @return true if the current text is a complete statement.
594 */
595 public boolean isTextComplete() {
596 return parser.isComplete();
597 }
598
599 /**
600 * Returns the predictive parser. Do not modify this object while the preditor window is
601 * active!
602 *
603 * @return The predictive parser.
604 */
605 public PredictiveParser getPredictiveParser() {
606 return parser;
607 }
608
609 public void windowPaneClosing(WindowPaneEvent e) {
610 log("pressed: close window");
611 notifyActionListeners(new ActionEvent(this, "Close"));
612 }
613
614 public void init() {
615 isInitialized = true;
616 update();
617 super.init();
618 }
619
620 /**
621 * Sets the logger.
622 *
623 * @param logger The logger object or null.
624 */
625 public void setLogger(Logger logger) {
626 this.logger = logger;
627 }
628
629 private void log(String text) {
630 if (logger != null) {
631 logger.log("pred", text);
632 }
633 }
634
635 public String toString() {
636 return "sentence: " + textContainer.getText();
637 }
638
639 }