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 }