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