001 // This file is part of AceWiki. 002 // Copyright 2008-2012, AceWiki developers. 003 // 004 // AceWiki is free software: you can redistribute it and/or modify it under the terms of the GNU 005 // Lesser General Public License as published by the Free Software Foundation, either version 3 of 006 // the License, or (at your option) any later version. 007 // 008 // AceWiki is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 009 // even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 010 // Lesser General Public License for more details. 011 // 012 // You should have received a copy of the GNU Lesser General Public License along with AceWiki. If 013 // 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.HashMap; 019 import java.util.List; 020 import java.util.Map; 021 022 import nextapp.echo.app.Alignment; 023 import nextapp.echo.app.ApplicationInstance; 024 import nextapp.echo.app.Border; 025 import nextapp.echo.app.Button; 026 import nextapp.echo.app.Color; 027 import nextapp.echo.app.Column; 028 import nextapp.echo.app.Extent; 029 import nextapp.echo.app.Font; 030 import nextapp.echo.app.Grid; 031 import nextapp.echo.app.Insets; 032 import nextapp.echo.app.Row; 033 import nextapp.echo.app.event.ActionEvent; 034 import nextapp.echo.app.event.ActionListener; 035 import nextapp.echo.app.event.WindowPaneEvent; 036 import nextapp.echo.app.event.WindowPaneListener; 037 import nextapp.echo.app.layout.GridLayoutData; 038 import ch.uzh.ifi.attempto.base.ConcreteOption; 039 import ch.uzh.ifi.attempto.base.DefaultTextOperator; 040 import ch.uzh.ifi.attempto.base.Logger; 041 import ch.uzh.ifi.attempto.base.NextTokenOptions; 042 import ch.uzh.ifi.attempto.base.PredictiveParser; 043 import ch.uzh.ifi.attempto.base.TextContainer; 044 import ch.uzh.ifi.attempto.base.TextElement; 045 import ch.uzh.ifi.attempto.base.TextOperator; 046 import ch.uzh.ifi.attempto.echocomp.GeneralButton; 047 import ch.uzh.ifi.attempto.echocomp.Label; 048 import ch.uzh.ifi.attempto.echocomp.Style; 049 import ch.uzh.ifi.attempto.echocomp.TabSensitiveTextField; 050 import ch.uzh.ifi.attempto.echocomp.TextField; 051 import echopoint.DirectHtml; 052 053 //import static ch.uzh.ifi.attempto.echocomp.KeyStrokes.*; 054 055 /** 056 * This class represents a predictive editor window. The predictive editor enables easy creation of 057 * texts that comply with a certain grammar. The users can create such a text word-by-word by 058 * clicking on one of different menu items. The menu items are structured into menu blocks each of 059 * which has a name that is displayed above the menu block. 060 * 061 * @author Tobias Kuhn 062 */ 063 public class PreditorWindow extends nextapp.echo.app.WindowPane implements ActionListener, WindowPaneListener { 064 065 private static final long serialVersionUID = -7815494421993305554L; 066 067 private final TextContainer textContainer = new TextContainer(); 068 private MenuCreator menuCreator; 069 private TextOperator textOperator; 070 private PredictiveParser parser; 071 private List<ActionListener> actionListeners = new ArrayList<ActionListener>(); 072 private Logger logger; 073 074 private MenuBlockManager menuBlockManager; 075 private MenuBlock enlargedMenuBlock; 076 077 private DirectHtml textArea = new DirectHtml(); 078 private TabSensitiveTextField textField; 079 private TextField dummyTextField; 080 private Column menuBlockArea; 081 private GeneralButton deleteButton = new GeneralButton("< Delete", 70, this); 082 private GeneralButton clearButton = new GeneralButton("Clear", 70, this); 083 private Button okButton = new GeneralButton("OK", 70, this); 084 private Button cancelButton = new GeneralButton("Cancel", 70, this); 085 086 private String textAreaStartText = ""; 087 private String textAreaEndText = "<span style=\"color: rgb(150, 150, 150)\"> ...</span>"; 088 089 // TODO: reactive key combinations 090 // private KeyStrokeListener keyStrokeListener = new KeyStrokeListener(); 091 092 private boolean isInitialized = false; 093 094 /** 095 * Creates a new predictive editor window using the given predictive parser. 096 * 097 * @param title The title of the window. 098 * @param parser The predictive parser to be used. Do not modify this object while the 099 * preditor window is active! 100 */ 101 public PreditorWindow(String title, PredictiveParser parser) { 102 this.parser = parser; 103 this.menuBlockManager = new MenuBlockManager(this); 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 Grid grid = new Grid(1); 117 grid.setColumnWidth(0, new Extent(730)); 118 add(grid); 119 120 GridLayoutData layout = new GridLayoutData(); 121 layout.setAlignment(new Alignment(Alignment.LEFT, Alignment.TOP)); 122 123 Column textAreaColumn = new Column(); 124 textAreaColumn.setInsets(new Insets(10, 10, 10, 0)); 125 textAreaColumn.add(textArea); 126 textAreaColumn.setLayoutData(layout); 127 grid.setRowHeight(0, new Extent(68)); 128 grid.add(textAreaColumn); 129 130 Column textColumn = new Column(); 131 textColumn.setInsets(new Insets(10, 10, 0, 0)); 132 textColumn.setCellSpacing(new Extent(10)); 133 134 Row textAreaButtonBar = new Row(); 135 textAreaButtonBar.setAlignment(new Alignment(Alignment.RIGHT, Alignment.CENTER)); 136 textAreaButtonBar.setInsets(new Insets(0, 5, 10, 0)); 137 textAreaButtonBar.setCellSpacing(new Extent(5)); 138 clearButton.setVisible(false); 139 textAreaButtonBar.add(clearButton); 140 textAreaButtonBar.add(deleteButton); 141 textColumn.add(textAreaButtonBar); 142 143 Column textFieldColumn = new Column(); 144 textFieldColumn.setCellSpacing(new Extent(1)); 145 Label textFieldLabel = new Label("text", Font.ITALIC, 11); 146 textFieldColumn.add(textFieldLabel); 147 148 textField = new TabSensitiveTextField(this); 149 textField.setWidth(new Extent(708)); 150 textField.setDisabledBackground(Style.lightDisabled); 151 Row textFieldRow = new Row(); 152 textFieldRow.add(textField); 153 dummyTextField = new TextField(); 154 dummyTextField.setWidth(new Extent(1)); 155 dummyTextField.setBorder(new Border(0, Color.BLACK, 0)); 156 dummyTextField.setBackground(Color.WHITE); 157 textFieldRow.add(dummyTextField); 158 textFieldColumn.add(textFieldRow); 159 160 // keyStrokeListener.addKeyCombination(VK_TAB, "Tab"); 161 // keyStrokeListener.addKeyCombination(VK_ESCAPE, "Esc"); 162 // keyStrokeListener.addKeyCombination(VK_BACK_SPACE | CONTROL_MASK, "Ctrl-Backspace"); 163 // keyStrokeListener.addActionListener(this); 164 // textFieldColumn.add(keyStrokeListener); 165 166 textColumn.add(textFieldColumn); 167 grid.setRowHeight(1, new Extent(88)); 168 grid.add(textColumn); 169 170 menuBlockArea = new Column(); 171 menuBlockArea.setInsets(new Insets(10, 15, 0, 0)); 172 grid.setRowHeight(2, new Extent(275)); 173 grid.add(menuBlockArea); 174 175 Row buttonBar = new Row(); 176 buttonBar.setAlignment(new Alignment(Alignment.RIGHT, Alignment.TOP)); 177 buttonBar.setInsets(new Insets(10, 10, 10, 0)); 178 buttonBar.setCellSpacing(new Extent(5)); 179 buttonBar.add(okButton); 180 buttonBar.add(cancelButton); 181 grid.setRowHeight(3, new Extent(30)); 182 grid.add(buttonBar); 183 184 update(); 185 } 186 187 /** 188 * Sets the menu creator. {@link DefaultMenuCreator} is used by default. 189 * 190 * @param menuCreator The menu creator. 191 */ 192 public void setMenuCreator(MenuCreator menuCreator) { 193 this.menuCreator = menuCreator; 194 update(); 195 } 196 197 /** 198 * Returns the menu creator. 199 * 200 * @return The menu creator. 201 */ 202 public MenuCreator getMenuCreator() { 203 if (menuCreator == null) { 204 setMenuCreator(new DefaultMenuCreator()); 205 } 206 return menuCreator; 207 } 208 209 /** 210 * Sets the text operator. {@link DefaultTextOperator} is used by default. 211 * 212 * @param textOperator The text operator. 213 */ 214 public void setTextOperator(TextOperator textOperator) { 215 this.textOperator = textOperator; 216 textContainer.setTextOperator(textOperator); 217 } 218 219 /** 220 * Returns the text operator. 221 * 222 * @return The text operator. 223 */ 224 public TextOperator getTextOperator() { 225 if (textOperator == null) { 226 setTextOperator(new DefaultTextOperator()); 227 } 228 return textOperator; 229 } 230 231 /** 232 * Shows or hides the "clear" button. 233 * 234 * @param visible true to show the "clear" button; false to hide it. 235 */ 236 public void setClearButtonVisible(boolean visible) { 237 clearButton.setVisible(visible); 238 } 239 240 /** 241 * Sets the text to be shown in the text area in front of the text entered by the user. The 242 * default is an empty string. 243 * 244 * @param textAreaStartText The text, possibly enriched with HTML tags. 245 */ 246 public void setTextAreaStartText(String textAreaStartText) { 247 this.textAreaStartText = textAreaStartText; 248 } 249 250 /** 251 * Sets the text to be shown in the text area at the end of the text entered by the user. The 252 * default are three gray dots "...". 253 * 254 * @param textAreaEndText The text, possibly enriched with HTML tags. 255 */ 256 public void setTextAreaEndText(String textAreaEndText) { 257 this.textAreaEndText = textAreaEndText; 258 } 259 260 /** 261 * Returns a copy of the text container object that contains the (partial) text that has been 262 * entered. 263 * 264 * @return A copy of the text container object. 265 */ 266 public TextContainer getTextContainer() { 267 return textContainer.clone(); 268 } 269 270 /** 271 * Returns the number of tokens of the current (partial) text. 272 * 273 * @return The number of tokens. 274 */ 275 public int getTokenCount() { 276 return parser.getTokenCount(); 277 } 278 279 /** 280 * Returns whether the given token is a possible next token. 281 * 282 * @param token The token. 283 * @return true if it is a possible next token. 284 */ 285 public boolean isPossibleNextToken(String token) { 286 return parser.isPossibleNextToken(token); 287 } 288 289 /** 290 * Adds the text element to the end of the text. 291 * 292 * @param te The text element to be added. 293 */ 294 public void addTextElement(TextElement te) { 295 textElementSelected(te); 296 textField.setText(""); 297 update(); 298 } 299 300 /** 301 * Reads the text and adds it to the end of the current text as far as possible. 302 * 303 * @param text The text to be added. 304 */ 305 public void addText(String text) { 306 handleTextInput(text, true); 307 update(); 308 } 309 310 private void textElementSelected(TextElement te) { 311 textContainer.addElement(te); 312 parser.addToken(te.getOriginalText()); 313 314 log("words added: " + te); 315 } 316 317 private void update() { 318 if (!isInitialized) return; 319 updateMenuBlockContents(); 320 menuBlockArea.removeAll(); 321 menuBlockManager.setFilter(textField.getText()); 322 if (enlargedMenuBlock != null) { 323 // One enlarged menu block 324 MenuBlockContent mbc = enlargedMenuBlock.getContent(); 325 int cs = menuCreator.getColorShift(mbc.getName()); 326 enlargedMenuBlock = new MenuBlock(708, 240, cs, this); 327 enlargedMenuBlock.setContent(mbc); 328 enlargedMenuBlock.setEnlarged(true); 329 menuBlockArea.add(enlargedMenuBlock); 330 } else { 331 menuBlockArea.add(menuBlockManager.createGUI()); 332 } 333 textField.setEnabled(menuBlockManager.getMenuBlockCount() > 0 || !textField.getText().equals("")); 334 ApplicationInstance.getActive().setFocusedComponent(textField); 335 clearButton.setEnabled(getTokenCount() > 0); 336 deleteButton.setEnabled(getTokenCount() > 0); 337 } 338 339 private void updateMenuBlockContents() { 340 int ref = parser.getReference(); 341 String t = ""; 342 TextElement prev = null; 343 for (int i = 0; i < getTokenCount() ; i++) { 344 TextElement te = textContainer.getTextElement(i); 345 String glue = ""; 346 if (prev != null) { 347 glue = getTextOperator().getGlue(prev, te); 348 } 349 if (ref > -1 && (ref == i || i == getTokenCount()-1)) { 350 t += glue + "<u>" + te.getText() + "</u>"; 351 } else { 352 t += glue + te.getText(); 353 } 354 prev = te; 355 } 356 if (t.startsWith(" ")) t = t.substring(1); 357 textArea.setText( 358 "<div style=\"font-family: Verdana,Arial,Helvetica,Sans-Serif; font-size: 12px\">" + 359 textAreaStartText + 360 t + 361 textAreaEndText + 362 "</div>" 363 ); 364 365 menuBlockManager.clear(); 366 367 NextTokenOptions options = parser.getNextTokenOptions(); 368 HashMap<String, MenuBlockContent> contentsMap = new HashMap<String, MenuBlockContent>(); 369 for (MenuItem m : getMenuCreator().createSpecialMenuItems(options)) { 370 addMenuItem(m, contentsMap); 371 } 372 for (ConcreteOption o : options.getConcreteOptions()) { 373 addMenuItem(getMenuCreator().createMenuEntry(o), contentsMap); 374 } 375 376 for (String mg : getMenuCreator().getMenuGroupOrdering()) { 377 if (contentsMap.containsKey(mg)) { 378 menuBlockManager.addMenuBlockContent(contentsMap.get(mg)); 379 } 380 } 381 for (String mg : contentsMap.keySet()) { 382 if (!getMenuCreator().getMenuGroupOrdering().contains(mg)) { 383 menuBlockManager.addMenuBlockContent(contentsMap.get(mg)); 384 } 385 } 386 } 387 388 private void addMenuItem(MenuItem menuItem, Map<String, MenuBlockContent> contentsMap) { 389 String menuGroup = menuItem.getMenuGroup(); 390 MenuBlockContent mbc; 391 if (contentsMap.containsKey(menuGroup)) { 392 mbc = contentsMap.get(menuGroup); 393 } else { 394 mbc = new MenuBlockContent(menuGroup); 395 mbc.setComparator(getMenuCreator().getMenuItemComparator()); 396 mbc.setActionListener(this); 397 contentsMap.put(menuGroup, mbc); 398 } 399 mbc.addItem(menuItem); 400 } 401 402 private void handleTextInput(boolean enterPressed) { 403 handleTextInput(textField.getText(), enterPressed); 404 } 405 406 private void handleTextInput(String text, boolean enterPressed) { 407 List<String> subtokens = getTextOperator().splitIntoTokens(text); 408 boolean force = enterPressed && (text.equals(menuBlockManager.getFilter()) || text.endsWith(" ")); 409 handleTokenInput(subtokens, force, true); 410 } 411 412 private void handleTokenInput(List<String> subtokens, boolean force, boolean caseSensitive) { 413 if (subtokens.size() == 0) { 414 textField.setText(""); 415 return; 416 } 417 418 String filter = ""; 419 for (String s : subtokens) filter += s + " "; 420 menuBlockManager.setFilter(filter); 421 422 String text = ""; 423 TextElement textElement = null; 424 List<String> rest = null; 425 List<String> s = new ArrayList<String>(subtokens); 426 while (s.size() > 0) { 427 if (text.length() > 0) text += " "; 428 text += s.remove(0); 429 TextElement te = null; 430 if (caseSensitive) { 431 te = getTextOperator().createTextElement(text); 432 } else { 433 String t = proposeToken(text); 434 if (t != null) { 435 te = getTextOperator().createTextElement(t); 436 } 437 } 438 if (te != null && parser.isPossibleNextToken(te.getOriginalText())) { 439 textElement = te; 440 rest = new ArrayList<String>(s); 441 } 442 } 443 if (textElement != null) { 444 if ((rest.isEmpty() && force) || (!rest.isEmpty() && 445 menuBlockManager.getMenuEntryCount() == 0)) { 446 textElementSelected(textElement); 447 updateMenuBlockContents(); 448 handleTokenInput(rest, force, caseSensitive); 449 return; 450 } 451 } 452 if (caseSensitive) { 453 handleTokenInput(subtokens, force, false); 454 } else { 455 textField.setText(text); 456 } 457 } 458 459 private String proposeToken(String text) { 460 text = text.toLowerCase().replaceAll("\\s+", "_"); 461 for (ConcreteOption o : parser.getNextTokenOptions().getConcreteOptions()) { 462 String t = o.getWord().toLowerCase().replaceAll("\\s+", "_"); 463 if (t.equals(text)) { 464 return o.getWord(); 465 } 466 } 467 return null; 468 } 469 470 /** 471 * Adds a new action-listener. 472 * 473 * @param actionListener The new action-listener. 474 */ 475 public void addActionListener(ActionListener actionListener) { 476 actionListeners.add(actionListener); 477 } 478 479 /** 480 * Removes the action-listener. 481 * 482 * @param actionListener The action-listener to be removed. 483 */ 484 public void removeActionListener(ActionListener actionListener) { 485 actionListeners.remove(actionListener); 486 } 487 488 /** 489 * Removes all action-listeners. 490 */ 491 public void removeAllActionListeners() { 492 actionListeners.clear(); 493 } 494 495 private void notifyActionListeners(ActionEvent event) { 496 for (ActionListener al : actionListeners) { 497 al.actionPerformed(event); 498 } 499 } 500 501 public void actionPerformed(ActionEvent e) { 502 Object src = e.getSource(); 503 String c = e.getActionCommand(); 504 505 if (enlargedMenuBlock != null) { 506 enlargedMenuBlock.setEnlarged(false); 507 enlargedMenuBlock = null; 508 } 509 510 boolean tabKeyPressed = false; 511 512 if (src == cancelButton) { 513 log("pressed: cancel"); 514 notifyActionListeners(new ActionEvent(this, "Cancel")); 515 return; 516 } else if (src == okButton) { 517 log("pressed: ok"); 518 handleTextInput(true); 519 update(); 520 notifyActionListeners(new ActionEvent(this, "OK")); 521 return; 522 } else if (src == deleteButton) { 523 log("pressed: < delete"); 524 removeLastToken(); 525 } else if (src == clearButton) { 526 log("pressed: clear"); 527 clearTokens(); 528 } else if (src == textField) { 529 if (getApplicationInstance().getFocusedComponent() == dummyTextField) { 530 log("pressed: tab-key"); 531 handleTextInput(false); 532 tabKeyPressed = true; 533 } else { 534 log("pressed: enter-key"); 535 if (textField.getText().equals("") && menuBlockManager.getFilter().equals("")) { 536 notifyActionListeners(new ActionEvent(this, "Enter")); 537 return; 538 } else { 539 handleTextInput(true); 540 } 541 } 542 } else if (src instanceof MenuEntry) { 543 TextElement te = ((MenuEntry) e.getSource()).getTextElement(); 544 log("pressed: menu-entry " + te.getText()); 545 textElementSelected(te); 546 textField.setText(""); 547 } else if ("enlarge".equals(c) && src instanceof MenuBlock) { 548 enlargedMenuBlock = (MenuBlock) src; 549 } else if ("Esc".equals(c)) { 550 log("pressed: escape key"); 551 notifyActionListeners(new ActionEvent(this, "Escape")); 552 return; 553 } else if ("Ctrl-Backspace".equals(c)) { 554 log("pressed: ctrl-backspace"); 555 if (getTokenCount() > 0) { 556 textContainer.removeLastElement(); 557 parser.removeToken(); 558 textField.setText(""); 559 } 560 } 561 562 update(); 563 564 if (tabKeyPressed) { 565 String s = menuBlockManager.getStartString(); 566 if (s != null) textField.setText(s); 567 } 568 } 569 570 /** 571 * Removes the last token. 572 */ 573 public void removeLastToken() { 574 if (getTokenCount() > 0) { 575 textContainer.removeLastElement(); 576 parser.removeToken(); 577 textField.setText(""); 578 } 579 } 580 581 /** 582 * Removes all tokens. 583 */ 584 public void clearTokens() { 585 textContainer.removeAllElements(); 586 parser.removeAllTokens(); 587 textField.setText(""); 588 } 589 590 /** 591 * Returns true if the current text is a complete statement. 592 * 593 * @return true if the current text is a complete statement. 594 */ 595 public boolean isTextComplete() { 596 return parser.isComplete(); 597 } 598 599 /** 600 * Returns the predictive parser. Do not modify this object while the preditor window is 601 * active! 602 * 603 * @return The predictive parser. 604 */ 605 public PredictiveParser getPredictiveParser() { 606 return parser; 607 } 608 609 public void windowPaneClosing(WindowPaneEvent e) { 610 log("pressed: close window"); 611 notifyActionListeners(new ActionEvent(this, "Close")); 612 } 613 614 public void init() { 615 isInitialized = true; 616 update(); 617 super.init(); 618 } 619 620 /** 621 * Sets the logger. 622 * 623 * @param logger The logger object or null. 624 */ 625 public void setLogger(Logger logger) { 626 this.logger = logger; 627 } 628 629 private void log(String text) { 630 if (logger != null) { 631 logger.log("pred", text); 632 } 633 } 634 635 public String toString() { 636 return "sentence: " + textContainer.getText(); 637 } 638 639 }