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.aceeditor; 016 017 import java.io.IOException; 018 import java.io.InputStream; 019 import java.io.OutputStream; 020 import java.util.List; 021 import java.util.Map; 022 import java.util.Properties; 023 024 import nextapp.echo.app.ApplicationInstance; 025 import nextapp.echo.app.Column; 026 import nextapp.echo.app.Component; 027 import nextapp.echo.app.Extent; 028 import nextapp.echo.app.Insets; 029 import nextapp.echo.app.SplitPane; 030 import nextapp.echo.app.Window; 031 import nextapp.echo.app.WindowPane; 032 import nextapp.echo.app.event.ActionEvent; 033 import nextapp.echo.app.event.ActionListener; 034 import nextapp.echo.filetransfer.app.AbstractDownloadProvider; 035 import nextapp.echo.filetransfer.app.DownloadCommand; 036 import nextapp.echo.webcontainer.command.BrowserRedirectCommand; 037 import ch.uzh.ifi.attempto.base.TextContainer; 038 import ch.uzh.ifi.attempto.base.TextElement; 039 import ch.uzh.ifi.attempto.chartparser.ChartParser; 040 import ch.uzh.ifi.attempto.echocomp.MessageWindow; 041 import ch.uzh.ifi.attempto.echocomp.TextAreaWindow; 042 import ch.uzh.ifi.attempto.echocomp.UploadWindow; 043 import ch.uzh.ifi.attempto.preditor.PreditorWindow; 044 045 /** 046 * This is the main class of the ACE Editor web application. The ACE Editor allows users to write 047 * sentences in ACE by the use of a predictive editor. Users can extend the lexicon and they can 048 * upload their own lexica. 049 * 050 * @author Tobias Kuhn 051 */ 052 public class ACEEditor extends Window implements ActionListener { 053 054 private static final long serialVersionUID = -684743065195237612L; 055 056 private static Properties properties; 057 058 private boolean editMode; 059 private LexiconHandler lexiconHandler; 060 private Map<String, String> parameters; 061 062 private TextEntry selectedEntry; 063 private TextEntry finalEntry = new TextEntry(null, this); 064 private TextEntry clipboard; 065 066 private Column textColumn = new Column(); 067 private Column mainColumn = new Column(); 068 private MenuBar menuBar; 069 070 // TODO: reactive key combinations 071 // private KeyStrokeListener keyStrokeListener = new KeyStrokeListener(); 072 073 /** 074 * Creates a new ACE Editor application. 075 * 076 * @param parameters A set of parameters in the form of name/value pairs. 077 */ 078 public ACEEditor(Map<String, String> parameters) { 079 setTitle("ACE Editor"); 080 this.parameters = parameters; 081 082 lexiconHandler = new LexiconHandler(parameters.get("lexicon")); 083 084 SplitPane splitPane = new SplitPane(SplitPane.ORIENTATION_VERTICAL); 085 splitPane.setSeparatorPosition(new Extent(23)); 086 087 menuBar = new MenuBar(this); 088 menuBar.setSelected("Default Expanded", true); 089 menuBar.setSelected("Default Paraphrase", true); 090 menuBar.setSelected("Default Syntax Boxes", true); 091 menuBar.setSelected("Default Pretty-Printed DRS", true); 092 menuBar.setEnabled("Paste", false); 093 splitPane.add(menuBar.getContent()); 094 095 textColumn.setInsets(new Insets(0, 5)); 096 textColumn.add(finalEntry); 097 098 mainColumn.add(textColumn); 099 100 // // Up and down keys for moving the selection: 101 // keyStrokeListener.addKeyCombination(VK_UP, "Up Pressed"); 102 // keyStrokeListener.addKeyCombination(VK_DOWN, "Down Pressed"); 103 // 104 // // Space key for expand/collapse or add: 105 // keyStrokeListener.addKeyCombination(VK_SPACE, "Space Pressed"); 106 // 107 // // Backspace key for delete: 108 // keyStrokeListener.addKeyCombination(VK_BACK_SPACE, "Backspace Pressed"); 109 // 110 // // Function key + A for add: 111 // keyStrokeListener.addKeyCombination(VK_A | CONTROL_MASK, "Func-A Pressed"); 112 // keyStrokeListener.addKeyCombination(VK_A | META_MASK, "Func-A Pressed"); 113 // keyStrokeListener.addKeyCombination(VK_A | ALT_MASK, "Func-A Pressed"); 114 // 115 // // Function key + M for modify: 116 // keyStrokeListener.addKeyCombination(VK_M | CONTROL_MASK, "Func-M Pressed"); 117 // keyStrokeListener.addKeyCombination(VK_M | META_MASK, "Func-M Pressed"); 118 // keyStrokeListener.addKeyCombination(VK_M | ALT_MASK, "Func-M Pressed"); 119 // 120 // // Function key + X for cut: 121 // keyStrokeListener.addKeyCombination(VK_X | CONTROL_MASK, "Func-X Pressed"); 122 // keyStrokeListener.addKeyCombination(VK_X | META_MASK, "Func-X Pressed"); 123 // keyStrokeListener.addKeyCombination(VK_X | ALT_MASK, "Func-X Pressed"); 124 // 125 // // Function key + C for copy: 126 // keyStrokeListener.addKeyCombination(VK_C | CONTROL_MASK, "Func-C Pressed"); 127 // keyStrokeListener.addKeyCombination(VK_C | META_MASK, "Func-C Pressed"); 128 // keyStrokeListener.addKeyCombination(VK_C | ALT_MASK, "Func-C Pressed"); 129 // 130 // // Function key + V for paste: 131 // keyStrokeListener.addKeyCombination(VK_V | CONTROL_MASK, "Func-V Pressed"); 132 // keyStrokeListener.addKeyCombination(VK_V | META_MASK, "Func-V Pressed"); 133 // keyStrokeListener.addKeyCombination(VK_V | ALT_MASK, "Func-V Pressed"); 134 // 135 // // Function key + O for open: 136 // keyStrokeListener.addKeyCombination(VK_O | CONTROL_MASK, "Func-O Pressed"); 137 // keyStrokeListener.addKeyCombination(VK_O | META_MASK, "Func-O Pressed"); 138 // keyStrokeListener.addKeyCombination(VK_O | ALT_MASK, "Func-O Pressed"); 139 // 140 // // Function key + S for save: 141 // keyStrokeListener.addKeyCombination(VK_S | CONTROL_MASK, "Func-S Pressed"); 142 // keyStrokeListener.addKeyCombination(VK_S | META_MASK, "Func-S Pressed"); 143 // keyStrokeListener.addKeyCombination(VK_S | ALT_MASK, "Func-S Pressed"); 144 // 145 // keyStrokeListener.addActionListener(this); 146 // mainColumn.add(keyStrokeListener); 147 148 splitPane.add(mainColumn); 149 getContent().add(splitPane); 150 151 select(finalEntry); 152 } 153 154 /** 155 * Returns whether parsing with the compiled lexicon of the APE executable is enabled. 156 * 157 * @return true if parsing with the compiled lexicon is enabled. 158 */ 159 public boolean isParseWithClexEnabled() { 160 return !"off".equals(getParameter("parse_with_clex")); 161 } 162 163 /** 164 * Returns whether the lexicon is immutable or can be changed by users. 165 * 166 * @return true if the lexicon is immutable. 167 */ 168 public boolean isLexiconImmutable() { 169 return !"off".equals(getParameter("immutable_lexicon")); 170 } 171 172 /** 173 * Returns the maximum file size (in bytes) for file upload. 0 means unlimited file size. 174 * 175 * @return The maximum file size. 176 */ 177 public int getMaxUploadFileSize() { 178 try { 179 return Integer.parseInt(getParameter("max_upload_file_size")); 180 } catch (NumberFormatException ex) {} 181 return 0; 182 } 183 184 /** 185 * Returns the value of the given parameter. These parameters are defined in the web.xml file 186 * of the web application. 187 * 188 * @param paramName The parameter name. 189 * @return The value of the parameter. 190 */ 191 public String getParameter(String paramName) { 192 return parameters.get(paramName); 193 } 194 195 /** 196 * Returns the full text of the current content of this ACE Editor instance. 197 * 198 * @return The full text. 199 */ 200 public String getFullText() { 201 String text = ""; 202 for (Component c : textColumn.getComponents()) { 203 String s = ((TextEntry) c).getText(); 204 if (c == finalEntry) break; 205 if (s == null) s = ""; 206 text += s + "\n\n"; 207 } 208 return text; 209 } 210 211 LexiconHandler getLexiconHandler() { 212 return lexiconHandler; 213 } 214 215 void select(TextEntry entry) { 216 if (selectedEntry != null) { 217 selectedEntry.setSelected(false); 218 } 219 entry.setSelected(true); 220 selectedEntry = entry; 221 222 if (selectedEntry == finalEntry) { 223 menuBar.setEnabled("Delete", false); 224 menuBar.setEnabled("Cut", false); 225 } else { 226 menuBar.setEnabled("Delete", true); 227 menuBar.setEnabled("Cut", true); 228 } 229 if (selectedEntry.isEmpty()) { 230 menuBar.setEnabled("Modify...", false); 231 } else { 232 menuBar.setEnabled("Modify...", true); 233 } 234 if (selectedEntry.isEmpty() || selectedEntry.isComment()) { 235 menuBar.setEnabled("Expanded", false); 236 menuBar.setSelected("Expanded", false); 237 for (String s : ResultItem.TYPES) { 238 menuBar.setEnabled("Show " + s, false); 239 menuBar.setSelected("Show " + s, false); 240 } 241 } else { 242 menuBar.setEnabled("Expanded", true); 243 menuBar.setSelected("Expanded", selectedEntry.isExpanded()); 244 for (String s : ResultItem.TYPES) { 245 menuBar.setEnabled("Show " + s, true); 246 menuBar.setSelected("Show " + s, selectedEntry.isResultItemVisible(s)); 247 } 248 } 249 menuBar.update(); 250 } 251 252 void entryChanged(TextEntry entry) { 253 if (entry == selectedEntry) { 254 menuBar.setSelected("Expanded", selectedEntry.isExpanded()); 255 menuBar.update(); 256 } 257 } 258 259 void showWindow(WindowPane window) { 260 cleanWindows(); 261 getContent().add(window); 262 } 263 264 void removeWindow(WindowPane window) { 265 window.setVisible(false); 266 window.dispose(); 267 cleanWindows(); 268 } 269 270 private void cleanWindows() { 271 for (Component c : getContent().getComponents()) { 272 if (!c.isVisible()) { 273 getContent().remove(c); 274 } 275 } 276 } 277 278 public void actionPerformed(ActionEvent e) { 279 String c = e.getActionCommand(); 280 Object source = e.getSource(); 281 282 if (c.equals("About")) { 283 String v = getInfo("aceeditor-version"); 284 String r = getInfo("aceeditor-release-stage"); 285 String d = getInfo("aceeditor-build-date"); 286 showWindow(new MessageWindow( 287 "ACE Editor", 288 "ACE Editor " + v + " (" + r + "), " + d, 289 "OK" 290 )); 291 } else if (c.equals("Attempto Website")) { 292 ApplicationInstance.getActive().enqueueCommand( 293 new BrowserRedirectCommand("http://attempto.ifi.uzh.ch") 294 ); 295 } else if (c.equals("Open Text...")) { 296 openFile(); 297 } else if (c.equals("Save Text...")) { 298 saveFile(); 299 } else if (c.equals("Load Lexicon...")) { 300 loadLexicon(false); 301 } else if (c.equals("Replace Lexicon...")) { 302 loadLexicon(true); 303 } else if (c.equals("Save Lexicon...")) { 304 saveLexicon(); 305 } else if (c.equals("Add...")) { 306 showEditor(false); 307 } else if (c.equals("Add Comment...")) { 308 showCommentEditor(false); 309 } else if (c.equals("Add Separator")) { 310 TextEntry newEntry = new TextEntry(null, this); 311 textColumn.add(newEntry, textColumn.indexOf(selectedEntry)); 312 select(newEntry); 313 } else if (c.equals("Modify...")) { 314 if (selectedEntry.isComment()) { 315 showCommentEditor(true); 316 } else { 317 showEditor(true); 318 } 319 } else if (c.equals("Delete")) { 320 deleteSelectedEntry(); 321 } else if (c.equals("Cut")) { 322 cutSelectedEntry(); 323 } else if (c.equals("Copy")) { 324 copySelectedEntry(); 325 } else if (c.equals("Paste")) { 326 pasteFromClipboard(); 327 } else if (c.equals("Expanded")) { 328 selectedEntry.setExpanded(menuBar.isSelected("Expanded")); 329 } else if (c.equals("Expand All")) { 330 for (Component comp : textColumn.getComponents()) { 331 ((TextEntry) comp).setExpanded(true); 332 } 333 } else if (c.equals("Collapse All")) { 334 for (Component comp : textColumn.getComponents()) { 335 ((TextEntry) comp).setExpanded(false); 336 } 337 } else if (c.startsWith("Show ")) { 338 selectedEntry.setResultItemVisible(c.substring(5), menuBar.isSelected(c)); 339 selectedEntry.setExpanded(true); 340 } else if (source instanceof PreditorWindow && c.matches("Cancel|Close|Escape")) { 341 PreditorWindow preditor = (PreditorWindow) source; 342 removeWindow(preditor); 343 refreshKeyStrokeListener(); 344 } else if (source instanceof PreditorWindow && c.matches("OK|Enter")) { 345 PreditorWindow preditor = (PreditorWindow) source; 346 TextContainer textContainer = preditor.getTextContainer(); 347 if (textContainer.getTextElementsCount() == 0) { 348 removeWindow(preditor); 349 refreshKeyStrokeListener(); 350 } else { 351 if (preditor.isPossibleNextToken(".")) { 352 textContainer.addElement(new TextElement(".")); 353 } else if (preditor.isPossibleNextToken("?")) { 354 textContainer.addElement(new TextElement("?")); 355 } 356 List<TextElement> l = textContainer.getTextElements(); 357 if (l.isEmpty() || l.get(l.size() - 1).getText().matches("[.?]")) { 358 if (editMode) { 359 selectedEntry.setText(textContainer.getText()); 360 select(selectedEntry); 361 } else { 362 TextEntry newEntry = new TextEntry( 363 textContainer.getText(), 364 this, 365 menuBar.isSelected("Default Expanded") 366 ); 367 for (String s : ResultItem.TYPES) { 368 newEntry.setResultItemVisible(s, menuBar.isSelected("Default " + s)); 369 } 370 textColumn.add(newEntry, textColumn.indexOf(selectedEntry)); 371 select(newEntry); 372 } 373 removeWindow(preditor); 374 refreshKeyStrokeListener(); 375 } else if (c.equals("OK")) { 376 showWindow(new MessageWindow( 377 "Error", 378 "There are unfinished sentences.", 379 "OK" 380 )); 381 } 382 } 383 } else if (source instanceof TextAreaWindow && c.equals("OK")) { 384 TextAreaWindow cew = (TextAreaWindow) source; 385 if (editMode) { 386 selectedEntry.setText("# " + cew.getText()); 387 select(selectedEntry); 388 } else { 389 TextEntry newEntry = new TextEntry("# " + cew.getText(), this, false); 390 textColumn.add(newEntry, textColumn.indexOf(selectedEntry)); 391 select(newEntry); 392 } 393 } else if (c.equals("Upload File")) { 394 String fileContent = ((UploadWindow) source).getFileContent(); 395 if (fileContent != null) { 396 textColumn.removeAll(); 397 String[] l = fileContent.replaceAll("\\s*(#[^\\n]*\\n)", "\n\n$1\n") 398 .split("\\n[ \\t\\x0B\\f\\r]*\\n"); 399 for (String line : l) { 400 TextEntry newEntry = new TextEntry(line, this, false); 401 textColumn.add(newEntry); 402 for (String s : ResultItem.TYPES) { 403 newEntry.setResultItemVisible(s, menuBar.isSelected("Default " + s)); 404 } 405 } 406 textColumn.add(finalEntry); 407 select((TextEntry) textColumn.getComponent(0)); 408 } 409 } else if (c.equals("Load Lexicon") || c.equals("Replace Lexicon")) { 410 String fileContent = ((UploadWindow) source).getFileContent(); 411 if (fileContent != null) { 412 if (c.equals("Replace Lexicon")) { 413 textColumn.removeAll(); 414 textColumn.add(finalEntry); 415 select(finalEntry); 416 lexiconHandler.clear(); 417 } 418 String[] l = (fileContent + " ").replaceAll("#[^\\n]*\\n", " ") 419 .replaceAll("\\s+", " ").replaceFirst("^ ", "") 420 .replaceAll("\\. ", ".\n").split("\\n"); 421 for (String line : l) { 422 if (line.equals("")) continue; 423 lexiconHandler.addWord(line); 424 } 425 426 if (c.equals("Replace Lexicon")) { 427 showWindow(new MessageWindow( 428 "Lexicon Replaced", 429 "The lexicon has been replaced.", 430 "OK" 431 )); 432 } else { 433 showWindow(new MessageWindow( 434 "Lexicon Loaded", 435 "The lexicon has been loaded.", 436 "OK" 437 )); 438 } 439 } 440 } else if (c.equals("Up Pressed")) { 441 int i = textColumn.indexOf(selectedEntry); 442 if (i > 0) { 443 select((TextEntry) textColumn.getComponent(i-1)); 444 } 445 } else if (c.equals("Down Pressed")) { 446 int i = textColumn.indexOf(selectedEntry); 447 if (i < textColumn.getComponentCount()-1) { 448 select((TextEntry) textColumn.getComponent(i+1)); 449 } 450 } else if (c.equals("Space Pressed")) { 451 if (selectedEntry.isEmpty()) { 452 showEditor(false); 453 } else { 454 if (selectedEntry.isExpanded()) { 455 selectedEntry.setExpanded(false); 456 } else { 457 selectedEntry.setExpanded(true); 458 } 459 } 460 } else if (c.equals("Backspace Pressed")) { 461 deleteSelectedEntry(); 462 } else if (c.equals("Func-A Pressed")) { 463 showEditor(false); 464 } else if (c.equals("Func-M Pressed")) { 465 if (selectedEntry.isComment()) { 466 showCommentEditor(true); 467 } else { 468 showEditor(true); 469 } 470 } else if (c.equals("Func-X Pressed")) { 471 cutSelectedEntry(); 472 } else if (c.equals("Func-C Pressed")) { 473 copySelectedEntry(); 474 } else if (c.equals("Func-V Pressed")) { 475 pasteFromClipboard(); 476 } else if (c.equals("Func-O Pressed")) { 477 openFile(); 478 } else if (c.equals("Func-S Pressed")) { 479 saveFile(); 480 } 481 } 482 483 private void refreshKeyStrokeListener() { 484 // The different keystroke listeners somehow interfere with each other so that this 485 // work-around is needed: 486 // mainColumn.remove(keyStrokeListener); 487 // mainColumn.add(keyStrokeListener); 488 } 489 490 private void showEditor(boolean edit) { 491 if (edit && selectedEntry.isEmpty()) return; 492 493 ACEEditorMenuCreator menuCreator = new ACEEditorMenuCreator(this, lexiconHandler); 494 ChartParser cp = new ChartParser(ACEEditorGrammar.grammar, "text"); 495 cp.setDynamicLexicon(lexiconHandler); 496 PreditorWindow preditor = new PreditorWindow("ACE Text Editor", cp); 497 preditor.setMenuCreator(menuCreator); 498 menuCreator.setPreditorWindow(preditor); 499 preditor.addActionListener(this); 500 this.editMode = edit; 501 if (edit) { 502 preditor.addText(selectedEntry.getText() + " "); 503 } 504 showWindow(preditor); 505 } 506 507 private void showCommentEditor(boolean edit) { 508 this.editMode = edit; 509 if (edit) { 510 showWindow(new TextAreaWindow( 511 "Comment Editor", 512 selectedEntry.getText().substring(2), 513 this 514 )); 515 } else { 516 showWindow(new TextAreaWindow("Comment Editor", "", this)); 517 } 518 } 519 520 private void deleteSelectedEntry() { 521 if (selectedEntry != finalEntry) { 522 int i = textColumn.indexOf(selectedEntry); 523 TextEntry nextEntry = (TextEntry) textColumn.getComponent(i+1); 524 textColumn.remove(selectedEntry); 525 select(nextEntry); 526 } 527 } 528 529 private void copySelectedEntry() { 530 clipboard = selectedEntry.copy(); 531 menuBar.setEnabled("Paste", true); 532 menuBar.update(); 533 } 534 535 private void cutSelectedEntry() { 536 if (selectedEntry != finalEntry) { 537 copySelectedEntry(); 538 deleteSelectedEntry(); 539 } 540 } 541 542 private void pasteFromClipboard() { 543 if (clipboard != null) { 544 TextEntry newEntry = clipboard.copy(); 545 textColumn.add(newEntry, textColumn.indexOf(selectedEntry)); 546 select(newEntry); 547 } 548 } 549 550 private void openFile() { 551 UploadWindow uw = new UploadWindow( 552 "Open File", 553 "Warning: This will delete the current content.\nChoose a file to open:", 554 null, 555 this 556 ); 557 uw.setActionCommand("Upload File"); 558 uw.setMaxFileSize(getMaxUploadFileSize()); 559 showWindow(uw); 560 } 561 562 private void saveFile() { 563 final String f = getFullText(); 564 AbstractDownloadProvider provider = new AbstractDownloadProvider() { 565 566 private static final long serialVersionUID = 898782345234987345L; 567 568 public String getContentType() { 569 return "text/plain"; 570 } 571 572 public String getFileName() { 573 return "text.ace.txt"; 574 } 575 576 public long getSize() { 577 return f.length(); 578 } 579 580 public void writeFile(OutputStream out) throws IOException { 581 out.write(f.getBytes()); 582 out.close(); 583 } 584 585 }; 586 getApplicationInstance().enqueueCommand(new DownloadCommand(provider)); 587 } 588 589 private void loadLexicon(boolean replace) { 590 String title = "Load Lexicon"; 591 String message = "Choose a lexicon file to load:"; 592 String actionCommand = "Load Lexicon"; 593 if (replace) { 594 title = "Replace Lexicon"; 595 message = "Warning: This will delete the current content.\n" + message; 596 actionCommand = "Replace Lexicon"; 597 } 598 UploadWindow uw = new UploadWindow(title, message, null, this); 599 uw.setActionCommand(actionCommand); 600 uw.setMaxFileSize(getMaxUploadFileSize()); 601 showWindow(uw); 602 } 603 604 private void saveLexicon() { 605 final String f = lexiconHandler.getLexiconFileContent(); 606 AbstractDownloadProvider provider = new AbstractDownloadProvider() { 607 608 private static final long serialVersionUID = 1932606314346272768L; 609 610 public String getContentType() { 611 return "text/plain"; 612 } 613 614 public String getFileName() { 615 return "text.lex.pl"; 616 } 617 618 public long getSize() { 619 return f.length(); 620 } 621 622 public void writeFile(OutputStream out) throws IOException { 623 out.write(f.getBytes()); 624 out.close(); 625 } 626 627 }; 628 getApplicationInstance().enqueueCommand(new DownloadCommand(provider)); 629 } 630 631 /** 632 * Returns information about ACE Editor, like the version number and the release date. This 633 * information is read from the file "aceeditor.properties". 634 * 635 * @param key The key string. 636 * @return The value for the given key. 637 */ 638 public static String getInfo(String key) { 639 if (properties == null) { 640 String f = "ch/uzh/ifi/attempto/aceeditor/aceeditor.properties"; 641 InputStream in = Thread.currentThread().getContextClassLoader().getResourceAsStream(f); 642 properties = new Properties(); 643 try { 644 properties.load(in); 645 } catch (Exception ex) { 646 ex.printStackTrace(); 647 } 648 } 649 650 return properties.getProperty(key); 651 } 652 653 }