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 }