001    // This file is part of the Attempto Java Packages.
002    // Copyright 2008-2009, 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 nextapp.echo2.app.event.WindowPaneEvent;
036    import nextapp.echo2.app.event.WindowPaneListener;
037    import ch.uzh.ifi.attempto.chartparser.ChartParser;
038    import ch.uzh.ifi.attempto.chartparser.Grammar;
039    import ch.uzh.ifi.attempto.chartparser.Terminal;
040    import ch.uzh.ifi.attempto.echocomp.GeneralButton;
041    import ch.uzh.ifi.attempto.echocomp.Label;
042    import ch.uzh.ifi.attempto.echocomp.Style;
043    import ch.uzh.ifi.attempto.echocomp.TextField;
044    import ch.uzh.ifi.attempto.echocomp.WindowPane;
045    import ch.uzh.ifi.attempto.preditor.text.TextContainer;
046    import ch.uzh.ifi.attempto.preditor.text.TextElement;
047    import echopointng.KeyStrokeListener;
048    
049    /**
050     * This class represents a predictive editor window. The predictive editor enables easy creation of texts that
051     * comply with a certain grammar. The users can create such a text word-by-word by clicking on one of different
052     * menu items. The menu items are structured into menu blocks each of which has a name that is displayed above
053     * the menu block.
054     * 
055     * @author Tobias Kuhn
056     */
057    public class PreditorWindow extends WindowPane implements ActionListener, WindowPaneListener {
058            
059            private static final long serialVersionUID = -7815494421993305554L;
060            
061            private List<MenuBlockContent> menuBlockContents = new ArrayList<MenuBlockContent>();
062            private List<MenuBlock> menuBlocksTop = new ArrayList<MenuBlock>();
063            private List<MenuBlock> menuBlocksBottom = new ArrayList<MenuBlock>();
064            private List<SplitPane> menuSplitPanesTop = new ArrayList<SplitPane>();
065            private List<SplitPane> menuSplitPanesBottom = new ArrayList<SplitPane>();
066            
067            private TextArea textArea = new TextArea();
068            private TextField textField;
069            private SplitPane menuBlockPane = new SplitPane(SplitPane.ORIENTATION_HORIZONTAL_LEFT_RIGHT, new Extent(0));
070            private SplitPane doubleColumnMenuPane =  new SplitPane(SplitPane.ORIENTATION_VERTICAL_TOP_BOTTOM, new Extent(258));
071            private GeneralButton deleteButton = new GeneralButton("< Delete", 70, this);
072            
073            private KeyStrokeListener keyStrokeListener = new KeyStrokeListener();
074            
075            // BUG: It would be nice the text elements could be retrieved from the chart so that this text container
076            // would not be needed.
077            private TextContainer newTextContainer = new TextContainer();
078            
079            private MenuCreator menuCreator;
080            
081            private ChartParser parser;
082            
083            private String filter = "";
084            
085            private Button okButton = new GeneralButton("OK", 70, this);
086            private Button cancelButton = new GeneralButton("Cancel", 70, this);
087            
088            private ArrayList<ActionListener> actionListeners = new ArrayList<ActionListener>();
089            
090            /**
091             * Creates a new predictive editor window for the given grammar using the given menu creator.
092             * 
093             * @param title The title of the window.
094             * @param grammar The grammar to be used.
095             * @param menuCreator The menu creator to be used.
096             */
097            public PreditorWindow(String title, Grammar grammar, MenuCreator menuCreator) {
098                    this.parser = new ChartParser(grammar);
099                    this.menuCreator = menuCreator;
100                    
101                    addWindowPaneListener(this);
102                    setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
103                    setModal(true);
104                    setTitle(title);
105                    setTitleFont(new Font(Style.fontTypeface, Font.ITALIC, new Extent(13)));
106                    setWidth(new Extent(753));
107                    setHeight(new Extent(503));
108                    setResizable(false);
109                    setTitleBackground(Style.windowTitleBackground);
110                    setStyleName("Default");
111                    
112                    Row buttonBar = new Row();
113                    buttonBar.setAlignment(new Alignment(Alignment.RIGHT, Alignment.CENTER));
114                    buttonBar.setInsets(new Insets(10, 17, 10, 10));
115                    buttonBar.setCellSpacing(new Extent(5));
116                    buttonBar.add(okButton);
117                    buttonBar.add(cancelButton);
118                    
119                    SplitPane splitPane = new SplitPane(SplitPane.ORIENTATION_VERTICAL_BOTTOM_TOP, new Extent(47));
120                    splitPane.add(buttonBar);
121                    add(splitPane);
122                    
123                    SplitPane editorPane = new SplitPane(SplitPane.ORIENTATION_VERTICAL_TOP_BOTTOM, new Extent(148));
124                    
125                    Column textColumn = new Column();
126                    textColumn.setInsets(new Insets(10, 10, 0, 0));
127                    textColumn.setCellSpacing(new Extent(10));
128                    
129                    Column textAreaColumn = new Column();
130                    
131                    textArea.setWidth(new Extent(702));
132                    textArea.setHeight(new Extent(42));
133                    textArea.setEnabled(false);
134                    textArea.setFont(new Font(Style.fontTypeface, Font.PLAIN, new Extent(12)));
135                    textArea.setBorder(new Border(1, new Color(180, 180, 180), Border.STYLE_SOLID));
136                    textArea.setInsets(new Insets(4, 4));
137                    textArea.setBackground(new Color(255, 255, 255));
138                    textArea.setFocusTraversalParticipant(false);
139                    textAreaColumn.add(textArea);
140                    
141                    Row textAreaButtonBar = new Row();
142                    textAreaButtonBar.setAlignment(new Alignment(Alignment.RIGHT, Alignment.CENTER));
143                    textAreaButtonBar.setInsets(new Insets(0, 5, 10, 0));
144                    textAreaButtonBar.setCellSpacing(new Extent(5));
145                    deleteButton.setFocusTraversalParticipant(false);
146                    textAreaButtonBar.add(deleteButton);
147                    textAreaColumn.add(textAreaButtonBar);
148                    
149                    textColumn.add(textAreaColumn);
150                    
151                    Column textFieldColumn = new Column();
152                    textFieldColumn.setCellSpacing(new Extent(1));
153                    Label textFieldLabel = new Label("text", Font.ITALIC, 11);
154                    textFieldColumn.add(textFieldLabel);
155                    
156                    textField = new TextField(this);
157                    textField.setWidth(new Extent(708));
158                    textField.setFocusTraversalParticipant(true);
159                    textField.setFocusTraversalIndex(0);
160                    textField.setDisabledBackground(Style.lightDisabled);
161                    Row textFieldRow = new Row();
162                    textFieldRow.add(textField);
163                    TextField dummyTextField = new TextField();
164                    dummyTextField.setWidth(new Extent(1));
165                    dummyTextField.setBorder(new Border(0, null, 0));
166                    dummyTextField.setBackground(Color.WHITE);
167                    textFieldRow.add(dummyTextField);
168                    textFieldColumn.add(textFieldRow);
169                    
170                    keyStrokeListener.addKeyCombination(KeyStrokeListener.VK_TAB, "Tab");
171                    keyStrokeListener.addKeyCombination(KeyStrokeListener.VK_ESCAPE, "Esc");
172                    keyStrokeListener.addKeyCombination(KeyStrokeListener.VK_BACK_SPACE | KeyStrokeListener.CONTROL_MASK, "Ctrl-Backspace");
173                    keyStrokeListener.addActionListener(this);
174                    textFieldColumn.add(keyStrokeListener);
175                    
176                    textColumn.add(textFieldColumn);
177                    editorPane.add(textColumn);
178                    
179                    menuBlockPane.setSeparatorWidth(new Extent(10));
180                    menuBlockPane.setSeparatorColor(Color.WHITE);
181                    menuBlockPane.add(new Label());
182                    
183                    SplitPane parentSplitPane = doubleColumnMenuPane;
184                    for (int i=0; i<10; i++) {
185                            MenuBlock menuBlock = new MenuBlock(this, this);
186                            menuBlocksTop.add(menuBlock);
187                            SplitPane menuSplitPane = new SplitPane(SplitPane.ORIENTATION_HORIZONTAL_LEFT_RIGHT);
188                            menuSplitPane.setSeparatorWidth(new Extent(10));
189                            menuSplitPane.setSeparatorColor(Color.WHITE);
190                            menuSplitPane.setVisible(false);
191                            menuSplitPane.add(menuBlock);
192                            menuSplitPanesTop.add(menuSplitPane);
193                            parentSplitPane.add(menuSplitPane);
194                            parentSplitPane = menuSplitPane;
195                    }
196                    
197                    parentSplitPane = doubleColumnMenuPane;
198                    for (int i=0; i<10; i++) {
199                            MenuBlock menuBlock = new MenuBlock(this, this);
200                            menuBlocksBottom.add(menuBlock);
201                            SplitPane menuSplitPane = new SplitPane(SplitPane.ORIENTATION_HORIZONTAL_LEFT_RIGHT);
202                            menuSplitPane.setSeparatorWidth(new Extent(10));
203                            menuSplitPane.setSeparatorColor(Color.WHITE);
204                            menuSplitPane.setVisible(false);
205                            menuSplitPane.add(menuBlock);
206                            menuSplitPanesBottom.add(menuSplitPane);
207                            parentSplitPane.add(menuSplitPane);
208                            parentSplitPane = menuSplitPane;
209                    }
210                    
211                    doubleColumnMenuPane.setSeparatorHeight(new Extent(12));
212                    doubleColumnMenuPane.setSeparatorColor(Color.WHITE);
213                    menuBlockPane.add(doubleColumnMenuPane);
214                    editorPane.add(menuBlockPane);
215                    splitPane.add(editorPane);
216                    
217                    update();
218            }
219            
220            /**
221             * Returns the (partial) text that has been entered .
222             * 
223             * @return The (partial) text in the form of a text container.
224             */
225            public TextContainer getTextContainer() {
226                    return newTextContainer;
227            }
228            
229            /**
230             * Returns a list of text elements that contain one of the given texts and that are
231             * possible next tokens.
232             * 
233             * @param text The content of the text elements to search for.
234             * @return The list of text elements.
235             */
236            public ArrayList<TextElement> getPossibleNextTokens(String... text) {
237                    ArrayList<TextElement> l = new ArrayList<TextElement>();
238                    for (MenuBlockContent m : menuBlockContents) {
239                            for (String s : text) {
240                                    TextElement e = m.getEntry(s);
241                                    if (e != null) l.add(e);
242                            }
243                    }
244                    return l;
245            }
246            
247            /**
248             * Adds the text element to the end of the text.
249             * 
250             * @param te The text element to be added.
251             */
252            public void addTextElement(TextElement te) {
253                    textElementSelected(te);
254                    textField.setText("");
255                    update();
256            }
257            
258            /**
259             * Reads the text and adds it to the end of the current text as far as possible.
260             * 
261             * @param text The text to be added.
262             */
263            public void addText(String text) {
264                    handleTextInput(tokenize(text));
265                    update();
266            }
267            
268            private void textElementSelected(TextElement te) {
269                    ArrayList<TextElement> l = new ArrayList<TextElement>();
270                    l.add(te);
271                    textElementSelected(l);
272            }
273            
274            private void textElementSelected(List<TextElement> te) {
275                    if (te.isEmpty()) return;
276                    
277                    // BUG: We add just the first possible text element to the text container.
278                    // This is not completely correct. Actually all elements should be added in
279                    // a parallel manner.
280                    newTextContainer.addElement(te.get(0));
281                    
282                    Terminal[] categories = new Terminal[te.size()];
283                    for (int i = 0 ; i < te.size() ; i++) {
284                            categories[i] = te.get(i).getCategory();
285                    }
286                    parser.addToken(categories);
287                    
288                    log("edit", "words added: " + te);
289            }
290            
291            private String getStartString() {
292                    String startString = "";
293                    ArrayList<String> blockStartStrings = new ArrayList<String>();
294                    
295                    for (MenuBlockContent mc : menuBlockContents) {
296                            String s = mc.getStartString();
297                            if (s != null) {
298                                    blockStartStrings.add(s);
299                            }
300                    }
301                    
302                    if (blockStartStrings.isEmpty()) return null;
303                    
304                    String first = blockStartStrings.get(0);
305                    blockStartStrings.remove(0);
306                    
307                    if (blockStartStrings.isEmpty()) return first;
308                    
309                    for (int i = 0; i < first.length(); i++) {
310                            char c = first.charAt(i);
311                            boolean stop = false;
312                            for (String s : blockStartStrings) {
313                                    if (s.length() <= i || s.charAt(i) != c) stop = true;
314                            }
315                            if (stop) break;
316                            startString += c;
317                    }
318                    
319                    return startString;
320            }
321            
322            private void update() {
323                    updateMenuBlockContents();
324                    setFilter(textField.getText());
325                    int mbCount = menuBlockContents.size();
326                    if (mbCount < 5) {
327                            int width = ( 720 / ( mbCount > 3 ? mbCount : 3 ) ) - 10;
328                            for (int i=0; i < menuBlocksTop.size(); i++) {
329                                    if (menuBlockContents.size() > i) {
330                                            menuBlocksTop.get(i).setContent(menuBlockContents.get(i), width, 16);
331                                            menuSplitPanesTop.get(i).setSeparatorPosition(new Extent(width));
332                                            menuSplitPanesTop.get(i).setVisible(true);
333                                    } else {
334                                            menuSplitPanesTop.get(i).setVisible(false);
335                                    }
336                            }
337                            doubleColumnMenuPane.setSeparatorPosition(new Extent(258));
338                    } else {
339                            int firstRowCount = (mbCount + 1) / 2;
340                            int width = ( 720 / firstRowCount ) - 10;
341                            for (int i=0; i < menuBlocksTop.size(); i++) {
342                                    if (i < firstRowCount) {
343                                            menuBlocksTop.get(i).setContent(menuBlockContents.get(i), width, 7);
344                                            menuSplitPanesTop.get(i).setSeparatorPosition(new Extent(width));
345                                            menuSplitPanesTop.get(i).setVisible(true);
346                                    } else {
347                                            menuSplitPanesTop.get(i).setVisible(false);
348                                    }
349                            }
350                            for (int i=0; i < menuBlocksBottom.size(); i++) {
351                                    if (firstRowCount + i < mbCount) {
352                                            menuBlocksBottom.get(i).setContent(menuBlockContents.get(firstRowCount + i), width, 7);
353                                            menuSplitPanesBottom.get(i).setSeparatorPosition(new Extent(width));
354                                            menuSplitPanesBottom.get(i).setVisible(true);
355                                    } else {
356                                            menuSplitPanesBottom.get(i).setVisible(false);
357                                    }
358                            }
359                            doubleColumnMenuPane.setSeparatorPosition(new Extent(123));
360                    }
361                    textField.setEnabled(menuBlockContents.size() > 0 || !textField.getText().equals(""));
362                    ApplicationInstance.getActive().setFocusedComponent(textField);
363                    deleteButton.setEnabled(textArea.getText().length() > 0);
364            }
365            
366            private void updateMenuBlockContents() {
367                    textArea.setText(newTextContainer.getText());
368                    menuBlockContents.clear();
369                    List<MenuBlockContent> newMenuBlockContents = menuCreator.createMenu(parser, newTextContainer);
370                    for (MenuBlockContent c : newMenuBlockContents) {
371                            if (!c.isEmpty()) {
372                                    menuBlockContents.add(c);
373                            }
374                    }
375            }
376            
377            private void setFilter(String filter) {
378                    if (filter == null) filter = "";
379                    filter = filter.replaceFirst("^\\s*", "").replaceFirst("\\s*$", "");
380                    
381                    for (MenuBlockContent c : menuBlockContents) {
382                            c.setFilter(filter);
383                    }
384                    this.filter = filter;
385            }
386            
387            private void handleTextInput() {
388                    handleTextInput(tokenize(textField.getText()));
389            }
390            
391            private void handleTextInput(ArrayList<String> textList) {
392                    
393                    ArrayList<TextElement> recognizedElements = null;
394                    int recognizedElementLength = 0;
395                    String text = "";
396                    
397                    for (int pos = 0 ; pos < textList.size() ; pos++) {
398                            if (text.length() > 0) text += " ";
399                            text += textList.get(pos);
400                            
401                            setFilter(text);
402                            ArrayList<TextElement> potentialElements = getPossibleNextTokens(text);
403                            if (potentialElements.isEmpty()) {
404                                    potentialElements = null;
405                            }
406                            
407                            // Counting how many different menu entries are available under the current filter.
408                            // elCount=2 means two or more entries.
409                            int elCount = 0;
410                            String firstElementText = null;
411                            for (MenuBlockContent m : menuBlockContents) {
412                                    for (MenuEntry e : m.getEntries()) {
413                                            String thisElementText = e.getTextElement().getText();
414                                            if (firstElementText == null) {
415                                                    firstElementText = thisElementText;
416                                                    elCount = 1;
417                                            } else if (!firstElementText.equals(thisElementText)) {
418                                                    elCount = 2;
419                                                    break;
420                                            }
421                                    }
422                                    if (elCount == 2) break;
423                            }
424                            
425                            boolean atLastPosition = pos == textList.size()-1;
426                            
427                            if (potentialElements != null && !atLastPosition) {
428                                    recognizedElements = potentialElements;
429                                    recognizedElementLength = pos + 1;
430                                    if (elCount == 1) break;
431                            }
432                            if (elCount == 0) {
433                                    break;
434                            } else if (atLastPosition) {
435                                    recognizedElements = null;
436                                    break;
437                            }
438                    }
439                    
440                    setFilter(null);
441                    
442                    if (recognizedElements != null) {
443                            textElementSelected(recognizedElements);
444                            updateMenuBlockContents();
445                            for (int i = 0; i < recognizedElementLength; i++) {
446                                    textList.remove(0);
447                            }
448                            handleTextInput(textList);
449                    } else {
450                            text = "";
451                            for (String textPart : textList) {
452                                    if (text.length() > 0) text += " ";
453                                    text += textPart;
454                            }
455                            textField.setText(text);
456                    }
457            }
458            
459            private ArrayList<String> tokenize(String text) {
460                    text = text.replaceAll("\\.", " . ");
461                    text = text.replaceAll("\\?", " ? ");
462                    text = text.replaceAll("\\!", " ! ");
463                    
464                    ArrayList<String> tokens = new ArrayList<String>(Arrays.asList(text.split(" ")));
465                    
466                    while (tokens.contains("")) {
467                            tokens.remove("");
468                    }
469                    
470                    if (text.endsWith(" ")) {
471                            tokens.add("");
472                    }
473                    
474                    return tokens;
475            }
476            
477            /**
478             * Adds a new action-listener.
479             * 
480             * @param actionListener The new action-listener.
481             */
482            public void addActionListener(ActionListener actionListener) {
483                    actionListeners.add(actionListener);
484            }
485            
486            /**
487             * Removes the action-listener.
488             * 
489             * @param actionListener The action-listener to be removed.
490             */
491            public void removeActionListener(ActionListener actionListener) {
492                    actionListeners.remove(actionListener);
493            }
494            
495            /**
496             * Removes all action-listeners.
497             */
498            public void removeAllActionListeners() {
499                    actionListeners.clear();
500            }
501            
502            private void notifyActionListeners(ActionEvent event) {
503                    for (ActionListener al : actionListeners) {
504                            al.actionPerformed(event);
505                    }
506            }
507            
508            public void actionPerformed(ActionEvent e) {
509                    if (e.getSource() == cancelButton) {
510                            log("edit", "pressed: cancel");
511                            notifyActionListeners(new ActionEvent(this, "Cancel"));
512                            return;
513                    } else if (e.getSource() == okButton) {
514                            log("edit", "pressed: ok");
515                            handleTextInput();
516                            update();
517                            notifyActionListeners(new ActionEvent(this, "OK"));
518                            return;
519                    } else if (e.getSource() == deleteButton) {
520                            log("edit", "pressed: < delete");
521                            if (newTextContainer.getTextElementsCount() > 0) {
522                                    newTextContainer.removeLastElement();
523                                    parser.removeToken();
524                                    textField.setText("");
525                            }
526                    } else if (e.getSource() instanceof MenuEntry) {
527                            TextElement te = ((MenuEntry) e.getSource()).getTextElement();
528                            log("edit", "pressed: menu-entry " + te.getText());
529                            textElementSelected(te);
530                            textField.setText("");
531                    } else if (e.getSource() == textField) {
532                            log("edit", "pressed: enter-key");
533                            if (textField.getText().equals("") && filter.equals("")) {
534                                    notifyActionListeners(new ActionEvent(this, "OK"));
535                                    return;
536                            } else {
537                                    handleTextInput();
538                                    ArrayList<TextElement> te = getPossibleNextTokens(textField.getText());
539                                    if (!te.isEmpty()) {
540                                            textElementSelected(te);
541                                            textField.setText("");
542                                    }
543                            }
544                    } else if ("Tab".equals(e.getActionCommand())) {
545                            log("edit", "pressed: tab-key");
546                            handleTextInput();
547                    } else if ("Esc".equals(e.getActionCommand())) {
548                            log("edit", "pressed: escape key");
549                            notifyActionListeners(new ActionEvent(this, "Cancel"));
550                            return;
551                    } else if ("Ctrl-Backspace".equals(e.getActionCommand())) {
552                            log("edit", "pressed: ctrl-backspace");
553                            if (newTextContainer.getTextElementsCount() > 0) {
554                                    newTextContainer.removeLastElement();
555                                    parser.removeToken();
556                                    textField.setText("");
557                            }
558                    }
559                    
560                    update();
561                    
562                    if ("Tab".equals(e.getActionCommand())) {
563                            String s = getStartString();
564                            if (s != null) textField.setText(s);
565                    }
566            }
567            
568            public void windowPaneClosing(WindowPaneEvent e) {
569                    log("edit", "pressed: close window");
570                    notifyActionListeners(new ActionEvent(this, "Cancel"));
571            }
572            
573            private void log(String type, String text) {
574                    // No logging done at the moment
575            }
576            
577            public String toString() {
578                    return "sentence: " + newTextContainer.getText();
579            }
580            
581    }