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