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    }