// This file is part of AceWiki.
// Copyright 2008-2013, AceWiki developers.
//
// AceWiki is free software: you can redistribute it and/or modify it under the terms of the GNU
// Lesser General Public License as published by the Free Software Foundation, either version 3 of
// the License, or (at your option) any later version.
//
// AceWiki is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
// even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with AceWiki. If
// not, see http://www.gnu.org/licenses/.
package ch.uzh.ifi.attempto.aceeditor;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import nextapp.echo.app.ApplicationInstance;
import nextapp.echo.app.Column;
import nextapp.echo.app.Component;
import nextapp.echo.app.Extent;
import nextapp.echo.app.Insets;
import nextapp.echo.app.SplitPane;
import nextapp.echo.app.Window;
import nextapp.echo.app.WindowPane;
import nextapp.echo.app.event.ActionEvent;
import nextapp.echo.app.event.ActionListener;
import nextapp.echo.filetransfer.app.AbstractDownloadProvider;
import nextapp.echo.filetransfer.app.DownloadCommand;
import nextapp.echo.webcontainer.command.BrowserRedirectCommand;
import ch.uzh.ifi.attempto.base.TextContainer;
import ch.uzh.ifi.attempto.base.TextElement;
import ch.uzh.ifi.attempto.chartparser.ChartParser;
import ch.uzh.ifi.attempto.echocomp.EchoThread;
import ch.uzh.ifi.attempto.echocomp.MessageWindow;
import ch.uzh.ifi.attempto.echocomp.TextAreaWindow;
import ch.uzh.ifi.attempto.echocomp.UploadWindow;
import ch.uzh.ifi.attempto.preditor.PreditorWindow;
/**
* This is the main class of the ACE Editor web application. The ACE Editor allows users to write
* sentences in ACE by the use of a predictive editor. Users can extend the lexicon and they can
* upload their own lexica.
*
* @author Tobias Kuhn
*/
public class ACEEditor extends Window implements ActionListener {
private static final long serialVersionUID = -684743065195237612L;
private static Properties properties;
private boolean editMode;
private LexiconHandler lexiconHandler;
private Map<String, String> parameters;
private TextEntry selectedEntry;
private TextEntry finalEntry = new TextEntry(null, this);
private TextEntry clipboard;
private Column textColumn = new Column();
private Column mainColumn = new Column();
private MenuBar menuBar;
// TODO: reactive key combinations
// private KeyStrokeListener keyStrokeListener = new KeyStrokeListener();
/**
* Creates a new ACE Editor application.
*
* @param parameters A set of parameters in the form of name/value pairs.
*/
public ACEEditor(Map<String, String> parameters) {
setTitle("ACE Editor");
this.parameters = parameters;
lexiconHandler = new LexiconHandler(parameters.get("lexicon"));
SplitPane splitPane = new SplitPane(SplitPane.ORIENTATION_VERTICAL);
splitPane.setSeparatorPosition(new Extent(23));
menuBar = new MenuBar(this);
menuBar.setSelected("Default Expanded", true);
menuBar.setSelected("Default Paraphrase", true);
menuBar.setSelected("Default Syntax Boxes", true);
menuBar.setSelected("Default Pretty-Printed DRS", true);
menuBar.setEnabled("Paste", false);
splitPane.add(menuBar.getContent());
textColumn.setInsets(new Insets(0, 5));
textColumn.add(finalEntry);
mainColumn.add(textColumn);
// // Up and down keys for moving the selection:
// keyStrokeListener.addKeyCombination(VK_UP, "Up Pressed");
// keyStrokeListener.addKeyCombination(VK_DOWN, "Down Pressed");
//
// // Space key for expand/collapse or add:
// keyStrokeListener.addKeyCombination(VK_SPACE, "Space Pressed");
//
// // Backspace key for delete:
// keyStrokeListener.addKeyCombination(VK_BACK_SPACE, "Backspace Pressed");
//
// // Function key + A for add:
// keyStrokeListener.addKeyCombination(VK_A | CONTROL_MASK, "Func-A Pressed");
// keyStrokeListener.addKeyCombination(VK_A | META_MASK, "Func-A Pressed");
// keyStrokeListener.addKeyCombination(VK_A | ALT_MASK, "Func-A Pressed");
//
// // Function key + M for modify:
// keyStrokeListener.addKeyCombination(VK_M | CONTROL_MASK, "Func-M Pressed");
// keyStrokeListener.addKeyCombination(VK_M | META_MASK, "Func-M Pressed");
// keyStrokeListener.addKeyCombination(VK_M | ALT_MASK, "Func-M Pressed");
//
// // Function key + X for cut:
// keyStrokeListener.addKeyCombination(VK_X | CONTROL_MASK, "Func-X Pressed");
// keyStrokeListener.addKeyCombination(VK_X | META_MASK, "Func-X Pressed");
// keyStrokeListener.addKeyCombination(VK_X | ALT_MASK, "Func-X Pressed");
//
// // Function key + C for copy:
// keyStrokeListener.addKeyCombination(VK_C | CONTROL_MASK, "Func-C Pressed");
// keyStrokeListener.addKeyCombination(VK_C | META_MASK, "Func-C Pressed");
// keyStrokeListener.addKeyCombination(VK_C | ALT_MASK, "Func-C Pressed");
//
// // Function key + V for paste:
// keyStrokeListener.addKeyCombination(VK_V | CONTROL_MASK, "Func-V Pressed");
// keyStrokeListener.addKeyCombination(VK_V | META_MASK, "Func-V Pressed");
// keyStrokeListener.addKeyCombination(VK_V | ALT_MASK, "Func-V Pressed");
//
// // Function key + O for open:
// keyStrokeListener.addKeyCombination(VK_O | CONTROL_MASK, "Func-O Pressed");
// keyStrokeListener.addKeyCombination(VK_O | META_MASK, "Func-O Pressed");
// keyStrokeListener.addKeyCombination(VK_O | ALT_MASK, "Func-O Pressed");
//
// // Function key + S for save:
// keyStrokeListener.addKeyCombination(VK_S | CONTROL_MASK, "Func-S Pressed");
// keyStrokeListener.addKeyCombination(VK_S | META_MASK, "Func-S Pressed");
// keyStrokeListener.addKeyCombination(VK_S | ALT_MASK, "Func-S Pressed");
//
// keyStrokeListener.addActionListener(this);
// mainColumn.add(keyStrokeListener);
splitPane.add(mainColumn);
getContent().add(splitPane);
select(finalEntry);
}
/**
* Returns whether parsing with the compiled lexicon of the APE executable is enabled.
*
* @return true if parsing with the compiled lexicon is enabled.
*/
public boolean isParseWithClexEnabled() {
return !"off".equals(getParameter("parse_with_clex"));
}
/**
* Returns whether the lexicon is immutable or can be changed by users.
*
* @return true if the lexicon is immutable.
*/
public boolean isLexiconImmutable() {
return !"off".equals(getParameter("immutable_lexicon"));
}
/**
* Returns the maximum file size (in bytes) for file upload. 0 means unlimited file size.
*
* @return The maximum file size.
*/
public int getMaxUploadFileSize() {
try {
return Integer.parseInt(getParameter("max_upload_file_size"));
} catch (NumberFormatException ex) {}
return 0;
}
/**
* Returns the value of the given parameter. These parameters are defined in the web.xml file
* of the web application.
*
* @param paramName The parameter name.
* @return The value of the parameter.
*/
public String getParameter(String paramName) {
return parameters.get(paramName);
}
/**
* Returns the full text of the current content of this ACE Editor instance.
*
* @return The full text.
*/
public String getFullText() {
String text = "";
for (Component c : textColumn.getComponents()) {
String s = ((TextEntry) c).getText();
if (c == finalEntry) break;
if (s == null) s = "";
text += s + "\n\n";
}
return text;
}
LexiconHandler getLexiconHandler() {
return lexiconHandler;
}
void select(TextEntry entry) {
if (selectedEntry != null) {
selectedEntry.setSelected(false);
}
entry.setSelected(true);
selectedEntry = entry;
if (selectedEntry == finalEntry) {
menuBar.setEnabled("Delete", false);
menuBar.setEnabled("Cut", false);
} else {
menuBar.setEnabled("Delete", true);
menuBar.setEnabled("Cut", true);
}
if (selectedEntry.isEmpty()) {
menuBar.setEnabled("Modify...", false);
} else {
menuBar.setEnabled("Modify...", true);
}
if (selectedEntry.isEmpty() || selectedEntry.isComment()) {
menuBar.setEnabled("Expanded", false);
menuBar.setSelected("Expanded", false);
for (String s : ResultItem.TYPES) {
menuBar.setEnabled("Show " + s, false);
menuBar.setSelected("Show " + s, false);
}
} else {
menuBar.setEnabled("Expanded", true);
menuBar.setSelected("Expanded", selectedEntry.isExpanded());
for (String s : ResultItem.TYPES) {
menuBar.setEnabled("Show " + s, true);
menuBar.setSelected("Show " + s, selectedEntry.isResultItemVisible(s));
}
}
menuBar.update();
}
void entryChanged(TextEntry entry) {
if (entry == selectedEntry) {
menuBar.setSelected("Expanded", selectedEntry.isExpanded());
menuBar.update();
}
}
void showWindow(WindowPane window) {
cleanWindows();
getContent().add(window);
}
void removeWindow(WindowPane window) {
window.setVisible(false);
window.dispose();
cleanWindows();
}
private void cleanWindows() {
for (Component c : getContent().getComponents()) {
if (!c.isVisible()) {
getContent().remove(c);
}
}
}
public void actionPerformed(ActionEvent e) {
String c = e.getActionCommand();
Object source = e.getSource();
if (c.equals("About")) {
String v = getInfo("aceeditor-version");
String r = getInfo("aceeditor-release-stage");
String d = getInfo("aceeditor-build-date");
showWindow(new MessageWindow(
"ACE Editor",
"ACE Editor " + v + " (" + r + "), " + d,
"OK"
));
} else if (c.equals("Attempto Website")) {
getApplication().enqueueCommand(
new BrowserRedirectCommand("http://attempto.ifi.uzh.ch")
);
} else if (c.equals("Open Text...")) {
openFile();
} else if (c.equals("Save Text...")) {
saveFile();
} else if (c.equals("Load Lexicon...")) {
loadLexicon(false);
} else if (c.equals("Replace Lexicon...")) {
loadLexicon(true);
} else if (c.equals("Save Lexicon...")) {
saveLexicon();
} else if (c.equals("Add...")) {
showEditor(false);
} else if (c.equals("Add Comment...")) {
showCommentEditor(false);
} else if (c.equals("Add Separator")) {
TextEntry newEntry = new TextEntry(null, this);
textColumn.add(newEntry, textColumn.indexOf(selectedEntry));
select(newEntry);
} else if (c.equals("Modify...")) {
if (selectedEntry.isComment()) {
showCommentEditor(true);
} else {
showEditor(true);
}
} else if (c.equals("Delete")) {
deleteSelectedEntry();
} else if (c.equals("Cut")) {
cutSelectedEntry();
} else if (c.equals("Copy")) {
copySelectedEntry();
} else if (c.equals("Paste")) {
pasteFromClipboard();
} else if (c.equals("Expanded")) {
selectedEntry.setExpanded(menuBar.isSelected("Expanded"));
} else if (c.equals("Expand All")) {
for (Component comp : textColumn.getComponents()) {
((TextEntry) comp).setExpanded(true);
}
} else if (c.equals("Collapse All")) {
for (Component comp : textColumn.getComponents()) {
((TextEntry) comp).setExpanded(false);
}
} else if (c.startsWith("Show ")) {
selectedEntry.setResultItemVisible(c.substring(5), menuBar.isSelected(c));
selectedEntry.setExpanded(true);
} else if (source instanceof PreditorWindow && c.matches("Cancel|Close|Escape")) {
PreditorWindow preditor = (PreditorWindow) source;
removeWindow(preditor);
refreshKeyStrokeListener();
} else if (source instanceof PreditorWindow && c.matches("OK|Enter")) {
PreditorWindow preditor = (PreditorWindow) source;
TextContainer textContainer = preditor.getTextContainer();
if (textContainer.getTextElementsCount() == 0) {
removeWindow(preditor);
refreshKeyStrokeListener();
} else {
if (preditor.isPossibleNextToken(".")) {
textContainer.addElement(new TextElement("."));
} else if (preditor.isPossibleNextToken("?")) {
textContainer.addElement(new TextElement("?"));
}
List<TextElement> l = textContainer.getTextElements();
if (l.isEmpty() || l.get(l.size() - 1).getText().matches("[.?]")) {
if (editMode) {
selectedEntry.setText(textContainer.getText());
select(selectedEntry);
} else {
TextEntry newEntry = new TextEntry(
textContainer.getText(),
this,
menuBar.isSelected("Default Expanded")
);
for (String s : ResultItem.TYPES) {
newEntry.setResultItemVisible(s, menuBar.isSelected("Default " + s));
}
textColumn.add(newEntry, textColumn.indexOf(selectedEntry));
select(newEntry);
}
removeWindow(preditor);
refreshKeyStrokeListener();
} else if (c.equals("OK")) {
showWindow(new MessageWindow(
"Error",
"There are unfinished sentences.",
"OK"
));
}
}
} else if (source instanceof TextAreaWindow && c.equals("OK")) {
TextAreaWindow cew = (TextAreaWindow) source;
if (editMode) {
selectedEntry.setText("# " + cew.getText());
select(selectedEntry);
} else {
TextEntry newEntry = new TextEntry("# " + cew.getText(), this, false);
textColumn.add(newEntry, textColumn.indexOf(selectedEntry));
select(newEntry);
}
} else if (c.equals("Upload File")) {
String fileContent = ((UploadWindow) source).getFileContent();
if (fileContent != null) {
textColumn.removeAll();
String[] l = fileContent.replaceAll("\\s*(#[^\\n]*\\n)", "\n\n$1\n")
.split("\\n[ \\t\\x0B\\f\\r]*\\n");
for (String line : l) {
TextEntry newEntry = new TextEntry(line, this, false);
textColumn.add(newEntry);
for (String s : ResultItem.TYPES) {
newEntry.setResultItemVisible(s, menuBar.isSelected("Default " + s));
}
}
textColumn.add(finalEntry);
select((TextEntry) textColumn.getComponent(0));
}
} else if (c.equals("Load Lexicon") || c.equals("Replace Lexicon")) {
String fileContent = ((UploadWindow) source).getFileContent();
if (fileContent != null) {
if (c.equals("Replace Lexicon")) {
textColumn.removeAll();
textColumn.add(finalEntry);
select(finalEntry);
lexiconHandler.clear();
}
String[] l = (fileContent + " ").replaceAll("#[^\\n]*\\n", " ")
.replaceAll("\\s+", " ").replaceFirst("^ ", "")
.replaceAll("\\. ", ".\n").split("\\n");
for (String line : l) {
if (line.equals("")) continue;
lexiconHandler.addWord(line);
}
if (c.equals("Replace Lexicon")) {
showWindow(new MessageWindow(
"Lexicon Replaced",
"The lexicon has been replaced.",
"OK"
));
} else {
showWindow(new MessageWindow(
"Lexicon Loaded",
"The lexicon has been loaded.",
"OK"
));
}
}
} else if (c.equals("Up Pressed")) {
int i = textColumn.indexOf(selectedEntry);
if (i > 0) {
select((TextEntry) textColumn.getComponent(i-1));
}
} else if (c.equals("Down Pressed")) {
int i = textColumn.indexOf(selectedEntry);
if (i < textColumn.getComponentCount()-1) {
select((TextEntry) textColumn.getComponent(i+1));
}
} else if (c.equals("Space Pressed")) {
if (selectedEntry.isEmpty()) {
showEditor(false);
} else {
if (selectedEntry.isExpanded()) {
selectedEntry.setExpanded(false);
} else {
selectedEntry.setExpanded(true);
}
}
} else if (c.equals("Backspace Pressed")) {
deleteSelectedEntry();
} else if (c.equals("Func-A Pressed")) {
showEditor(false);
} else if (c.equals("Func-M Pressed")) {
if (selectedEntry.isComment()) {
showCommentEditor(true);
} else {
showEditor(true);
}
} else if (c.equals("Func-X Pressed")) {
cutSelectedEntry();
} else if (c.equals("Func-C Pressed")) {
copySelectedEntry();
} else if (c.equals("Func-V Pressed")) {
pasteFromClipboard();
} else if (c.equals("Func-O Pressed")) {
openFile();
} else if (c.equals("Func-S Pressed")) {
saveFile();
}
}
private void refreshKeyStrokeListener() {
// The different keystroke listeners somehow interfere with each other so that this
// work-around is needed:
// mainColumn.remove(keyStrokeListener);
// mainColumn.add(keyStrokeListener);
}
private void showEditor(boolean edit) {
if (edit && selectedEntry.isEmpty()) return;
ACEEditorMenuCreator menuCreator = new ACEEditorMenuCreator(this, lexiconHandler);
ChartParser cp = new ChartParser(ACEEditorGrammar.grammar, "text");
cp.setDynamicLexicon(lexiconHandler);
PreditorWindow preditor = new PreditorWindow("ACE Text Editor", cp);
preditor.setMenuCreator(menuCreator);
menuCreator.setPreditorWindow(preditor);
preditor.addActionListener(this);
this.editMode = edit;
if (edit) {
preditor.addText(selectedEntry.getText() + " ");
}
showWindow(preditor);
}
private void showCommentEditor(boolean edit) {
this.editMode = edit;
if (edit) {
TextAreaWindow w = new TextAreaWindow("Comment Editor", this);
w.setText(selectedEntry.getText().substring(2));
showWindow(w);
} else {
showWindow(new TextAreaWindow("Comment Editor", this));
}
}
private void deleteSelectedEntry() {
if (selectedEntry != finalEntry) {
int i = textColumn.indexOf(selectedEntry);
TextEntry nextEntry = (TextEntry) textColumn.getComponent(i+1);
textColumn.remove(selectedEntry);
select(nextEntry);
}
}
private void copySelectedEntry() {
clipboard = selectedEntry.copy();
menuBar.setEnabled("Paste", true);
menuBar.update();
}
private void cutSelectedEntry() {
if (selectedEntry != finalEntry) {
copySelectedEntry();
deleteSelectedEntry();
}
}
private void pasteFromClipboard() {
if (clipboard != null) {
TextEntry newEntry = clipboard.copy();
textColumn.add(newEntry, textColumn.indexOf(selectedEntry));
select(newEntry);
}
}
private void openFile() {
UploadWindow uw = new UploadWindow(
"Open File",
"Warning: This will delete the current content.\nChoose a file to open:",
null,
this
);
uw.setActionCommand("Upload File");
uw.setMaxFileSize(getMaxUploadFileSize());
showWindow(uw);
}
private void saveFile() {
final String f = getFullText();
AbstractDownloadProvider provider = new AbstractDownloadProvider() {
private static final long serialVersionUID = 898782345234987345L;
public String getContentType() {
return "text/plain";
}
public String getFileName() {
return "text.ace.txt";
}
public long getSize() {
return f.length();
}
public void writeFile(OutputStream out) throws IOException {
out.write(f.getBytes());
out.close();
}
};
getApplicationInstance().enqueueCommand(new DownloadCommand(provider));
}
private void loadLexicon(boolean replace) {
String title = "Load Lexicon";
String message = "Choose a lexicon file to load:";
String actionCommand = "Load Lexicon";
if (replace) {
title = "Replace Lexicon";
message = "Warning: This will delete the current content.\n" + message;
actionCommand = "Replace Lexicon";
}
UploadWindow uw = new UploadWindow(title, message, null, this);
uw.setActionCommand(actionCommand);
uw.setMaxFileSize(getMaxUploadFileSize());
showWindow(uw);
}
private void saveLexicon() {
final String f = lexiconHandler.getLexiconFileContent();
AbstractDownloadProvider provider = new AbstractDownloadProvider() {
private static final long serialVersionUID = 1932606314346272768L;
public String getContentType() {
return "text/plain";
}
public String getFileName() {
return "text.lex.pl";
}
public long getSize() {
return f.length();
}
public void writeFile(OutputStream out) throws IOException {
out.write(f.getBytes());
out.close();
}
};
getApplicationInstance().enqueueCommand(new DownloadCommand(provider));
}
/**
* Returns information about ACE Editor, like the version number and the release date. This
* information is read from the file "aceeditor.properties".
*
* @param key The key string.
* @return The value for the given key.
*/
public static String getInfo(String key) {
if (properties == null) {
String f = "ch/uzh/ifi/attempto/aceeditor/aceeditor.properties";
InputStream in = Thread.currentThread().getContextClassLoader().getResourceAsStream(f);
properties = new Properties();
try {
properties.load(in);
} catch (Exception ex) {
ex.printStackTrace();
}
}
return properties.getProperty(key);
}
private ApplicationInstance getApplication() {
return EchoThread.getActiveApplication();
}
}