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 List<MenuBlockContent> menuBlockContents = new ArrayList<MenuBlockContent>();
057            private List<MenuBlock> menuBlocksTop = new ArrayList<MenuBlock>();
058            private List<MenuBlock> menuBlocksBottom = new ArrayList<MenuBlock>();
059            private List<SplitPane> menuSplitPanesTop = new ArrayList<SplitPane>();
060            private List<SplitPane> menuSplitPanesBottom = new ArrayList<SplitPane>();
061            
062            private TextArea textArea = new TextArea();
063            private TextField textField;
064            private SplitPane menuBlockPane = new SplitPane(SplitPane.ORIENTATION_HORIZONTAL_LEFT_RIGHT, new Extent(0));
065            private SplitPane doubleColumnMenuPane =  new SplitPane(SplitPane.ORIENTATION_VERTICAL_TOP_BOTTOM, new Extent(258));
066            private GeneralButton deleteButton = new GeneralButton("< Delete", 70, this);
067            
068            private KeyStrokeListener keyStrokeListener = new KeyStrokeListener();
069            
070            private TextContainer newTextContainer = new TextContainer();
071            private MenuCreator menuCreator;
072            
073            private ChartParser parser;
074            
075            private Button okButton = new GeneralButton("OK", 70, this);
076            private Button cancelButton = new GeneralButton("Cancel", 70, this);
077            
078            private ArrayList<ActionListener> actionListeners = new ArrayList<ActionListener>();
079            
080            /**
081             * Creates a new predictive editor window for the given grammar using the given menu creator.
082             * 
083             * @param title The title of the window.
084             * @param grammar The grammar to be used.
085             * @param menuCreator The menu creator to be used.
086             */
087            public PreditorWindow(String title, Grammar grammar, MenuCreator menuCreator) {
088                    this.parser = new ChartParser(grammar);
089                    this.menuCreator = menuCreator;
090                    
091                    setModal(true);
092                    setTitle(title);
093                    setTitleFont(new Font(Style.fontTypeface, Font.ITALIC, new Extent(13)));
094                    setWidth(new Extent(753));
095                    setHeight(new Extent(503));
096                    setResizable(false);
097                    setTitleBackground(Style.windowTitleBackground);
098                    setStyleName("Default");
099                    
100                    Row buttonBar = new Row();
101                    buttonBar.setAlignment(new Alignment(Alignment.RIGHT, Alignment.CENTER));
102                    buttonBar.setInsets(new Insets(10, 17, 10, 10));
103                    buttonBar.setCellSpacing(new Extent(5));
104                    buttonBar.add(okButton);
105                    buttonBar.add(cancelButton);
106                    
107                    SplitPane splitPane = new SplitPane(SplitPane.ORIENTATION_VERTICAL_BOTTOM_TOP, new Extent(47));
108                    splitPane.add(buttonBar);
109                    add(splitPane);
110                    
111                    SplitPane editorPane = new SplitPane(SplitPane.ORIENTATION_VERTICAL_TOP_BOTTOM, new Extent(148));
112                    
113                    Column textColumn = new Column();
114                    textColumn.setInsets(new Insets(10, 10, 0, 0));
115                    textColumn.setCellSpacing(new Extent(10));
116                    
117                    Column textAreaColumn = new Column();
118                    
119                    textArea.setWidth(new Extent(702));
120                    textArea.setHeight(new Extent(42));
121                    textArea.setEnabled(false);
122                    textArea.setFont(new Font(Style.fontTypeface, Font.PLAIN, new Extent(12)));
123                    textArea.setBorder(new Border(1, new Color(180, 180, 180), Border.STYLE_SOLID));
124                    textArea.setInsets(new Insets(4, 4));
125                    textArea.setBackground(new Color(255, 255, 255));
126                    textArea.setFocusTraversalParticipant(false);
127                    textAreaColumn.add(textArea);
128                    
129                    Row textAreaButtonBar = new Row();
130                    textAreaButtonBar.setAlignment(new Alignment(Alignment.RIGHT, Alignment.CENTER));
131                    textAreaButtonBar.setInsets(new Insets(0, 5, 10, 0));
132                    textAreaButtonBar.setCellSpacing(new Extent(5));
133                    deleteButton.setFocusTraversalParticipant(false);
134                    textAreaButtonBar.add(deleteButton);
135                    textAreaColumn.add(textAreaButtonBar);
136                    
137                    textColumn.add(textAreaColumn);
138                    
139                    Column textFieldColumn = new Column();
140                    textFieldColumn.setCellSpacing(new Extent(1));
141                    Label textFieldLabel = new Label("text", Font.ITALIC, 11);
142                    textFieldColumn.add(textFieldLabel);
143                    
144                    textField = new TextField(this);
145                    textField.setWidth(new Extent(708));
146                    textField.setFocusTraversalParticipant(true);
147                    textField.setFocusTraversalIndex(0);
148                    textField.setDisabledBackground(Style.lightDisabled);
149                    Row textFieldRow = new Row();
150                    textFieldRow.add(textField);
151                    TextField dummyTextField = new TextField();
152                    dummyTextField.setWidth(new Extent(1));
153                    dummyTextField.setBorder(new Border(0, null, 0));
154                    dummyTextField.setBackground(Color.WHITE);
155                    textFieldRow.add(dummyTextField);
156                    textFieldColumn.add(textFieldRow);
157                    
158                    keyStrokeListener.addKeyCombination(KeyStrokeListener.VK_TAB, "Tab");
159                    keyStrokeListener.addKeyCombination(KeyStrokeListener.VK_RIGHT, "Tab");
160                    keyStrokeListener.addActionListener(this);
161                    textFieldColumn.add(keyStrokeListener);
162                    
163                    textColumn.add(textFieldColumn);
164                    editorPane.add(textColumn);
165                    
166                    menuBlockPane.setSeparatorWidth(new Extent(10));
167                    menuBlockPane.setSeparatorColor(Color.WHITE);
168                    menuBlockPane.add(new Label());
169                    
170                    SplitPane parentSplitPane = doubleColumnMenuPane;
171                    for (int i=0; i<10; i++) {
172                            MenuBlock menuBlock = new MenuBlock(this, this);
173                            menuBlocksTop.add(menuBlock);
174                            SplitPane menuSplitPane = new SplitPane(SplitPane.ORIENTATION_HORIZONTAL_LEFT_RIGHT);
175                            menuSplitPane.setSeparatorWidth(new Extent(10));
176                            menuSplitPane.setSeparatorColor(Color.WHITE);
177                            menuSplitPane.setVisible(false);
178                            menuSplitPane.add(menuBlock);
179                            menuSplitPanesTop.add(menuSplitPane);
180                            parentSplitPane.add(menuSplitPane);
181                            parentSplitPane = menuSplitPane;
182                    }
183                    
184                    parentSplitPane = doubleColumnMenuPane;
185                    for (int i=0; i<10; i++) {
186                            MenuBlock menuBlock = new MenuBlock(this, this);
187                            menuBlocksBottom.add(menuBlock);
188                            SplitPane menuSplitPane = new SplitPane(SplitPane.ORIENTATION_HORIZONTAL_LEFT_RIGHT);
189                            menuSplitPane.setSeparatorWidth(new Extent(10));
190                            menuSplitPane.setSeparatorColor(Color.WHITE);
191                            menuSplitPane.setVisible(false);
192                            menuSplitPane.add(menuBlock);
193                            menuSplitPanesBottom.add(menuSplitPane);
194                            parentSplitPane.add(menuSplitPane);
195                            parentSplitPane = menuSplitPane;
196                    }
197                    
198                    doubleColumnMenuPane.setSeparatorHeight(new Extent(12));
199                    doubleColumnMenuPane.setSeparatorColor(Color.WHITE);
200                    menuBlockPane.add(doubleColumnMenuPane);
201                    editorPane.add(menuBlockPane);
202                    splitPane.add(editorPane);
203                    
204                    update();
205            }
206            
207            /**
208             * Returns the (partial) text that has been entered .
209             * 
210             * @return The (partial) text in the form of a text container.
211             */
212            public TextContainer getTextContainer() {
213                    return newTextContainer;
214            }
215            
216            /**
217             * Returns a text element with the given content if it is a possible next token.
218             * 
219             * @param content The content of the text element.
220             * @return The text element.
221             */
222            public TextElement getPossibleNextToken(String content) {
223                    for (MenuBlockContent m : menuBlockContents) {
224                            TextElement e = m.getEntry(content);
225                            if (e != null) return e;
226                    }
227                    return null;
228            }
229            
230            /**
231             * Adds the text element to the end of the text.
232             * 
233             * @param te The text element to be added.
234             */
235            public void addTextElement(TextElement te) {
236                    textElementSelected(te);
237                    textField.setText("");
238                    update();
239            }
240            
241            /**
242             * Reads the text and adds it to the end of the current text as far as possible.
243             * 
244             * @param text The text to be added.
245             */
246            public void addText(String text) {
247                    handleTextInput(tokenize(text));
248                    update();
249            }
250            
251            private void textElementSelected(TextElement te) {
252                    newTextContainer.addElement(te);
253                    parser.addToken(te.getCategory());
254                    log("edit", "words added: " + te.getText());
255            }
256            
257            private int getElementsCount() {
258                    int c = 0;
259                    for (MenuBlockContent m : menuBlockContents) {
260                            c += m.getEntries().size();
261                    }
262                    return c;       
263            }
264            
265            private String getStartString() {
266                    String startString = "";
267                    ArrayList<String> blockStartStrings = new ArrayList<String>();
268                    
269                    for (MenuBlockContent mc : menuBlockContents) {
270                            String s = mc.getStartString();
271                            if (s != null) {
272                                    blockStartStrings.add(s);
273                            }
274                    }
275                    
276                    if (blockStartStrings.isEmpty()) return null;
277                    
278                    String first = blockStartStrings.get(0);
279                    blockStartStrings.remove(0);
280                    
281                    if (blockStartStrings.isEmpty()) return first;
282                    
283                    for (int i = 0; i < first.length(); i++) {
284                            char c = first.charAt(i);
285                            boolean stop = false;
286                            for (String s : blockStartStrings) {
287                                    if (s.length() <= i || s.charAt(i) != c) stop = true;
288                            }
289                            if (stop) break;
290                            startString += c;
291                    }
292                    
293                    return startString;
294            }
295            
296            private void update() {
297                    updateMenuBlockContents();
298                    setFilter(textField.getText());
299                    int mbCount = menuBlockContents.size();
300                    if (mbCount < 5) {
301                            int width = ( 720 / ( mbCount > 3 ? mbCount : 3 ) ) - 10;
302                            for (int i=0; i < menuBlocksTop.size(); i++) {
303                                    if (menuBlockContents.size() > i) {
304                                            menuBlocksTop.get(i).setContent(menuBlockContents.get(i), width, 16);
305                                            menuSplitPanesTop.get(i).setSeparatorPosition(new Extent(width));
306                                            menuSplitPanesTop.get(i).setVisible(true);
307                                    } else {
308                                            menuSplitPanesTop.get(i).setVisible(false);
309                                    }
310                            }
311                            doubleColumnMenuPane.setSeparatorPosition(new Extent(258));
312                    } else {
313                            int firstRowCount = (mbCount + 1) / 2;
314                            int width = ( 720 / firstRowCount ) - 10;
315                            for (int i=0; i < menuBlocksTop.size(); i++) {
316                                    if (i < firstRowCount) {
317                                            menuBlocksTop.get(i).setContent(menuBlockContents.get(i), width, 7);
318                                            menuSplitPanesTop.get(i).setSeparatorPosition(new Extent(width));
319                                            menuSplitPanesTop.get(i).setVisible(true);
320                                    } else {
321                                            menuSplitPanesTop.get(i).setVisible(false);
322                                    }
323                            }
324                            for (int i=0; i < menuBlocksBottom.size(); i++) {
325                                    if (firstRowCount + i < mbCount) {
326                                            menuBlocksBottom.get(i).setContent(menuBlockContents.get(firstRowCount + i), width, 7);
327                                            menuSplitPanesBottom.get(i).setSeparatorPosition(new Extent(width));
328                                            menuSplitPanesBottom.get(i).setVisible(true);
329                                    } else {
330                                            menuSplitPanesBottom.get(i).setVisible(false);
331                                    }
332                            }
333                            doubleColumnMenuPane.setSeparatorPosition(new Extent(123));
334                    }
335                    textField.setEnabled(menuBlockContents.size() > 0 || !textField.getText().equals(""));
336                    ApplicationInstance.getActive().setFocusedComponent(textField);
337                    deleteButton.setEnabled(textArea.getText().length() > 0);
338            }
339            
340            private void updateMenuBlockContents() {
341                    textArea.setText(newTextContainer.getText());
342                    menuBlockContents.clear();
343                    List<MenuBlockContent> newMenuBlockContents = menuCreator.createMenu(parser, newTextContainer);
344                    for (MenuBlockContent c : newMenuBlockContents) {
345                            if (!c.isEmpty()) {
346                                    menuBlockContents.add(c);
347                            }
348                    }
349            }
350            
351            private void setFilter(String filter) {
352                    for (MenuBlockContent c : menuBlockContents) {
353                            c.setFilter(filter);
354                    }
355            }
356            
357            private void handleTextInput() {
358                    addText(textField.getText());
359            }
360            
361            private void handleTextInput(ArrayList<String> tokens) {
362                    
363                    TextElement t = null;
364                    String s = "";
365                    int c = 0;
366                    
367                    for (String token : tokens) {
368                            setFilter(s + token);
369                            TextElement t2 = getPossibleNextToken(s + token);
370                            if (t2 != null) {
371                                    t = t2;
372                                    c++;
373                                    break;
374                            }
375                            if (getElementsCount() == 0) {
376                                    break;
377                            } else {
378                                    s += token + " ";
379                                    c++;
380                            }
381                    }
382                    
383                    setFilter(null);
384                    
385                    if (t != null) {
386                            textElementSelected(t);
387                            updateMenuBlockContents();
388                            for (int i=0; i<c; i++) tokens.remove(0);
389                            handleTextInput(tokens);
390                    } else {
391                            String text = "";
392                            for (String token : tokens) {
393                                    if (text.length() > 0) text += " ";
394                                    text += token;
395                            }
396                            textField.setText(text);
397                    }
398            }
399            
400            private ArrayList<String> tokenize(String text) {
401                    text = text.replaceAll("\\.", " . ");
402                    text = text.replaceAll("\\?", " ? ");
403                    text = text.replaceAll("\\!", " ! ");
404                    
405                    ArrayList<String> tokens = new ArrayList<String>(Arrays.asList(text.split(" ")));
406                    
407                    while (tokens.contains("")) {
408                            tokens.remove("");
409                    }
410                    return tokens;
411            }
412            
413            /**
414             * Adds a new action-listener.
415             * 
416             * @param actionListener The new action-listener.
417             */
418            public void addActionListener(ActionListener actionListener) {
419                    actionListeners.add(actionListener);
420            }
421            
422            /**
423             * Removes the action-listener.
424             * 
425             * @param actionListener The action-listener to be removed.
426             */
427            public void removeActionListener(ActionListener actionListener) {
428                    actionListeners.remove(actionListener);
429            }
430            
431            /**
432             * Removes all action-listeners.
433             */
434            public void removeAllActionListeners() {
435                    actionListeners.clear();
436            }
437            
438            private void notifyActionListeners(ActionEvent event) {
439                    for (ActionListener al : actionListeners) {
440                            al.actionPerformed(event);
441                    }
442            }
443            
444            public void actionPerformed(ActionEvent e) {
445                    if (e.getSource() == cancelButton) {
446                            log("edit", "pressed: cancel");
447                            notifyActionListeners(new ActionEvent(this, "Cancel"));
448                            return;
449                    } else if (e.getSource() == okButton) {
450                            log("edit", "pressed: ok");
451                            handleTextInput();
452                            update();
453                            notifyActionListeners(new ActionEvent(this, "OK"));
454                            return;
455                    } else if (e.getSource() == deleteButton) {
456                            log("edit", "pressed: < delete");
457                            newTextContainer.removeLastElement();
458                            parser.removeToken();
459                            textField.setText("");
460                    } else if (e.getSource() instanceof MenuEntry) {
461                            TextElement te = ((MenuEntry) e.getSource()).getTextElement();
462                            log("edit", "pressed: menu-entry " + te.getText());
463                            textElementSelected(te);
464                            textField.setText("");
465                    } else if (e.getSource() == textField) {
466                            log("edit", "pressed: enter-key");
467                            handleTextInput();
468                    } else if ("Tab".equals(e.getActionCommand())) {
469                            log("edit", "pressed: tab-key");
470                            handleTextInput();
471                    }
472                    
473                    update();
474                    
475                    if ("Tab".equals(e.getActionCommand())) {
476                            String s = getStartString();
477                            if (s != null) textField.setText(s);
478                    }
479            }
480            
481            private void log(String type, String text) {
482                    // No logging done at the moment
483            }
484            
485            public String toString() {
486                    return "sentence: " + newTextContainer.getText();
487            }
488            
489    }