// 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.preditor;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import nextapp.echo.app.Alignment;
import nextapp.echo.app.Border;
import nextapp.echo.app.Color;
import nextapp.echo.app.Column;
import nextapp.echo.app.Extent;
import nextapp.echo.app.Font;
import nextapp.echo.app.Grid;
import nextapp.echo.app.Insets;
import nextapp.echo.app.Row;
import nextapp.echo.app.event.ActionEvent;
import nextapp.echo.app.event.ActionListener;
import nextapp.echo.app.event.WindowPaneEvent;
import nextapp.echo.app.event.WindowPaneListener;
import nextapp.echo.app.layout.GridLayoutData;
import ch.uzh.ifi.attempto.base.ConcreteOption;
import ch.uzh.ifi.attempto.base.DefaultTextOperator;
import ch.uzh.ifi.attempto.base.LoggerContext;
import ch.uzh.ifi.attempto.base.NextTokenOptions;
import ch.uzh.ifi.attempto.base.PredictiveParser;
import ch.uzh.ifi.attempto.base.TextContainer;
import ch.uzh.ifi.attempto.base.TextElement;
import ch.uzh.ifi.attempto.base.TextOperator;
import ch.uzh.ifi.attempto.echocomp.EchoThread;
import ch.uzh.ifi.attempto.echocomp.GeneralButton;
import ch.uzh.ifi.attempto.echocomp.Label;
import ch.uzh.ifi.attempto.echocomp.LocaleResources;
import ch.uzh.ifi.attempto.echocomp.Style;
import ch.uzh.ifi.attempto.echocomp.TabSensitiveTextField;
import ch.uzh.ifi.attempto.echocomp.TextField;
import echopoint.DirectHtml;
//import static ch.uzh.ifi.attempto.echocomp.KeyStrokes.*;
/**
* This class represents a predictive editor window. The predictive editor enables easy creation of
* texts that comply with a certain grammar. The users can create such a text word-by-word by
* clicking on one of different menu items. The menu items are structured into menu blocks each of
* which has a name that is displayed above the menu block.
*
* @author Tobias Kuhn
*/
public class PreditorWindow extends nextapp.echo.app.WindowPane implements ActionListener, WindowPaneListener {
private static final long serialVersionUID = -7815494421993305554L;
static {
LocaleResources.loadBundle("ch/uzh/ifi/attempto/echocomp/text");
LocaleResources.loadBundle("ch/uzh/ifi/attempto/preditor/text");
}
private final TextContainer textContainer = new TextContainer();
private MenuCreator menuCreator;
private TextOperator textOperator;
private PredictiveParser parser;
private List<ActionListener> actionListeners = new ArrayList<ActionListener>();
private LoggerContext loggerContext;
private final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(this.getClass());
private MenuBlockManager menuBlockManager;
private MenuBlock enlargedMenuBlock;
private DirectHtml textArea = new DirectHtml();
private TabSensitiveTextField textField;
private TextField dummyTextField;
private Column menuBlockArea;
private GeneralButton deleteButton, clearButton, okButton, cancelButton;
private String textAreaStartText = "";
private String textAreaEndText = "<span style=\"color: rgb(150, 150, 150)\"> ...</span>";
// TODO: reactive key combinations
// private KeyStrokeListener keyStrokeListener = new KeyStrokeListener();
private boolean isInitialized = false;
/**
* Creates a new predictive editor window using the given predictive parser.
*
* @param title The title of the window.
* @param parser The predictive parser to be used. Do not modify this object while the
* preditor window is active!
*/
public PreditorWindow(String title, PredictiveParser parser) {
this.parser = parser;
this.menuBlockManager = new MenuBlockManager(this);
addWindowPaneListener(this);
setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
setModal(true);
setTitle(title);
setTitleFont(new Font(Style.fontTypeface, Font.ITALIC, new Extent(13)));
setWidth(new Extent(753));
setHeight(new Extent(524));
setResizable(false);
setTitleBackground(Style.windowTitleBackground);
setStyleName("Default");
Grid grid = new Grid(1);
grid.setColumnWidth(0, new Extent(730));
add(grid);
GridLayoutData layout = new GridLayoutData();
layout.setAlignment(new Alignment(Alignment.LEFT, Alignment.TOP));
Column textAreaColumn = new Column();
textAreaColumn.setInsets(new Insets(10, 10, 10, 0));
textAreaColumn.add(textArea);
textAreaColumn.setLayoutData(layout);
grid.setRowHeight(0, new Extent(68));
grid.add(textAreaColumn);
Column textColumn = new Column();
textColumn.setInsets(new Insets(10, 10, 0, 0));
textColumn.setCellSpacing(new Extent(10));
Row textAreaButtonBar = new Row();
textAreaButtonBar.setAlignment(new Alignment(Alignment.RIGHT, Alignment.CENTER));
textAreaButtonBar.setInsets(new Insets(0, 5, 10, 0));
textAreaButtonBar.setCellSpacing(new Extent(5));
clearButton = new GeneralButton(getLocalized("preditor_button_clear"), this, 100);
clearButton.setVisible(false);
textAreaButtonBar.add(clearButton);
deleteButton = new GeneralButton(getLocalized("preditor_button_delete"), this, 100);
textAreaButtonBar.add(deleteButton);
textColumn.add(textAreaButtonBar);
Column textFieldColumn = new Column();
textFieldColumn.setCellSpacing(new Extent(1));
Label textFieldLabel = new Label(getLocalized("preditor_textfield_label"), Font.ITALIC, 11);
textFieldColumn.add(textFieldLabel);
textField = new TabSensitiveTextField(this);
textField.setWidth(new Extent(708));
textField.setDisabledBackground(Style.lightDisabled);
Row textFieldRow = new Row();
textFieldRow.add(textField);
dummyTextField = new TextField();
dummyTextField.setWidth(new Extent(1));
dummyTextField.setBorder(new Border(0, Color.BLACK, 0));
dummyTextField.setBackground(Color.WHITE);
textFieldRow.add(dummyTextField);
textFieldColumn.add(textFieldRow);
// keyStrokeListener.addKeyCombination(VK_TAB, "Tab");
// keyStrokeListener.addKeyCombination(VK_ESCAPE, "Esc");
// keyStrokeListener.addKeyCombination(VK_BACK_SPACE | CONTROL_MASK, "Ctrl-Backspace");
// keyStrokeListener.addActionListener(this);
// textFieldColumn.add(keyStrokeListener);
textColumn.add(textFieldColumn);
grid.setRowHeight(1, new Extent(88));
grid.add(textColumn);
menuBlockArea = new Column();
menuBlockArea.setInsets(new Insets(10, 15, 0, 0));
grid.setRowHeight(2, new Extent(275));
grid.add(menuBlockArea);
Row buttonBar = new Row();
buttonBar.setAlignment(new Alignment(Alignment.RIGHT, Alignment.TOP));
buttonBar.setInsets(new Insets(10, 10, 10, 0));
buttonBar.setCellSpacing(new Extent(5));
okButton = new GeneralButton(getLocalized("general_action_ok"), this, 100);
buttonBar.add(okButton);
cancelButton = new GeneralButton(getLocalized("general_action_cancel"), this, 100);
buttonBar.add(cancelButton);
grid.setRowHeight(3, new Extent(30));
grid.add(buttonBar);
update();
}
/**
* Sets the menu creator. {@link DefaultMenuCreator} is used by default.
*
* @param menuCreator The menu creator.
*/
public void setMenuCreator(MenuCreator menuCreator) {
this.menuCreator = menuCreator;
update();
}
/**
* Returns the menu creator.
*
* @return The menu creator.
*/
public MenuCreator getMenuCreator() {
if (menuCreator == null) {
setMenuCreator(new DefaultMenuCreator());
}
return menuCreator;
}
/**
* Sets the text operator. {@link DefaultTextOperator} is used by default.
*
* @param textOperator The text operator.
*/
public void setTextOperator(TextOperator textOperator) {
this.textOperator = textOperator;
textContainer.setTextOperator(textOperator);
}
/**
* Returns the text operator.
*
* @return The text operator.
*/
public TextOperator getTextOperator() {
if (textOperator == null) {
setTextOperator(new DefaultTextOperator());
}
return textOperator;
}
/**
* Shows or hides the "clear" button.
*
* @param visible true to show the "clear" button; false to hide it.
*/
public void setClearButtonVisible(boolean visible) {
clearButton.setVisible(visible);
}
/**
* Sets the text to be shown in the text area in front of the text entered by the user. The
* default is an empty string.
*
* @param textAreaStartText The text, possibly enriched with HTML tags.
*/
public void setTextAreaStartText(String textAreaStartText) {
this.textAreaStartText = textAreaStartText;
}
/**
* Sets the text to be shown in the text area at the end of the text entered by the user. The
* default are three gray dots "...".
*
* @param textAreaEndText The text, possibly enriched with HTML tags.
*/
public void setTextAreaEndText(String textAreaEndText) {
this.textAreaEndText = textAreaEndText;
}
/**
* Returns a copy of the text container object that contains the (partial) text that has been
* entered.
*
* @return A copy of the text container object.
*/
public TextContainer getTextContainer() {
return textContainer.clone();
}
/**
* Returns the number of tokens of the current (partial) text.
*
* @return The number of tokens.
*/
public int getTokenCount() {
return parser.getTokenCount();
}
/**
* Returns whether the given token is a possible next token.
*
* @param token The token.
* @return true if it is a possible next token.
*/
public boolean isPossibleNextToken(String token) {
return parser.isPossibleNextToken(token);
}
/**
* Adds the text element to the end of the text.
*
* @param te The text element to be added.
*/
public void addTextElement(TextElement te) {
textElementSelected(te);
textField.setText("");
update();
}
/**
* Reads the text and adds it to the end of the current text as far as possible.
*
* @param text The text to be added.
*/
public void addText(String text) {
handleTextInput(text, true);
update();
}
private void textElementSelected(TextElement te) {
textContainer.addElement(te);
parser.addToken(te.getOriginalText());
log("words added: " + te);
}
private void update() {
if (!isInitialized) return;
updateMenuBlockContents();
menuBlockArea.removeAll();
menuBlockManager.setFilter(textField.getText());
if (enlargedMenuBlock != null) {
// One enlarged menu block
MenuBlockContent mbc = enlargedMenuBlock.getContent();
int cs = menuCreator.getColorShift(mbc.getName());
enlargedMenuBlock = new MenuBlock(708, 240, cs, this);
enlargedMenuBlock.setContent(mbc);
enlargedMenuBlock.setEnlarged(true);
menuBlockArea.add(enlargedMenuBlock);
} else {
menuBlockArea.add(menuBlockManager.createGUI());
}
textField.setEnabled(menuBlockManager.getMenuBlockCount() > 0 || !textField.getText().equals(""));
EchoThread.getActiveApplication().setFocusedComponent(textField);
clearButton.setEnabled(getTokenCount() > 0);
deleteButton.setEnabled(getTokenCount() > 0);
}
private void updateMenuBlockContents() {
int ref = parser.getReference();
String t = "";
TextElement prev = null;
for (int i = 0; i < getTokenCount() ; i++) {
TextElement te = textContainer.getTextElement(i);
String glue = "";
if (prev != null) {
glue = getTextOperator().getGlue(prev, te);
}
if (ref > -1 && (ref == i || i == getTokenCount()-1)) {
t += glue + "<u>" + te.getText() + "</u>";
} else {
t += glue + te.getText();
}
prev = te;
}
if (t.startsWith(" ")) t = t.substring(1);
textArea.setText(
"<div style=\"font-family: Verdana,Arial,Helvetica,Sans-Serif; font-size: 12px\">" +
textAreaStartText +
t +
textAreaEndText +
"</div>"
);
menuBlockManager.clear();
NextTokenOptions options = parser.getNextTokenOptions();
HashMap<String, MenuBlockContent> contentsMap = new HashMap<String, MenuBlockContent>();
for (MenuItem m : getMenuCreator().createSpecialMenuItems(options)) {
addMenuItem(m, contentsMap);
}
for (ConcreteOption o : options.getConcreteOptions()) {
addMenuItem(getMenuCreator().createMenuEntry(o), contentsMap);
}
for (String mg : getMenuCreator().getMenuGroupOrdering()) {
if (contentsMap.containsKey(mg)) {
menuBlockManager.addMenuBlockContent(contentsMap.get(mg));
}
}
for (String mg : contentsMap.keySet()) {
if (!getMenuCreator().getMenuGroupOrdering().contains(mg)) {
menuBlockManager.addMenuBlockContent(contentsMap.get(mg));
}
}
}
private void addMenuItem(MenuItem menuItem, Map<String, MenuBlockContent> contentsMap) {
String menuGroup = menuItem.getMenuGroup();
MenuBlockContent mbc;
if (contentsMap.containsKey(menuGroup)) {
mbc = contentsMap.get(menuGroup);
} else {
mbc = new MenuBlockContent(menuGroup);
mbc.setComparator(getMenuCreator().getMenuItemComparator());
mbc.setActionListener(this);
contentsMap.put(menuGroup, mbc);
}
mbc.addItem(menuItem);
}
private void handleTextInput(boolean enterPressed) {
handleTextInput(textField.getText(), enterPressed);
}
private void handleTextInput(String text, boolean enterPressed) {
List<String> subtokens = getTextOperator().splitIntoTokens(text);
boolean force = enterPressed && (text.equals(menuBlockManager.getFilter()) || text.endsWith(" "));
handleTokenInput(subtokens, force, true);
}
private void handleTokenInput(List<String> subtokens, boolean force, boolean caseSensitive) {
if (subtokens.size() == 0) {
textField.setText("");
return;
}
String filter = "";
for (String s : subtokens) filter += s + " ";
menuBlockManager.setFilter(filter);
String text = "";
TextElement textElement = null;
List<String> rest = null;
List<String> s = new ArrayList<String>(subtokens);
while (s.size() > 0) {
if (text.length() > 0) text += " ";
text += s.remove(0);
TextElement te = null;
if (caseSensitive) {
te = getTextOperator().createTextElement(text);
} else {
String t = proposeToken(text);
if (t != null) {
te = getTextOperator().createTextElement(t);
}
}
if (te != null && parser.isPossibleNextToken(te.getOriginalText())) {
textElement = te;
rest = new ArrayList<String>(s);
}
}
if (textElement != null) {
if ((rest.isEmpty() && force) || (!rest.isEmpty() &&
menuBlockManager.getMenuEntryCount() == 0)) {
textElementSelected(textElement);
updateMenuBlockContents();
handleTokenInput(rest, force, caseSensitive);
return;
}
}
if (caseSensitive) {
handleTokenInput(subtokens, force, false);
} else {
textField.setText(text);
}
}
private String proposeToken(String text) {
text = text.toLowerCase().replaceAll("\\s+", "_");
for (ConcreteOption o : parser.getNextTokenOptions().getConcreteOptions()) {
String t = o.getWord().toLowerCase().replaceAll("\\s+", "_");
if (t.equals(text)) {
return o.getWord();
}
}
return null;
}
/**
* Adds a new action-listener.
*
* @param actionListener The new action-listener.
*/
public void addActionListener(ActionListener actionListener) {
actionListeners.add(actionListener);
}
/**
* Removes the action-listener.
*
* @param actionListener The action-listener to be removed.
*/
public void removeActionListener(ActionListener actionListener) {
actionListeners.remove(actionListener);
}
/**
* Removes all action-listeners.
*/
public void removeAllActionListeners() {
actionListeners.clear();
}
private void notifyActionListeners(ActionEvent event) {
for (ActionListener al : actionListeners) {
al.actionPerformed(event);
}
}
public void actionPerformed(ActionEvent e) {
Object src = e.getSource();
String c = e.getActionCommand();
if (enlargedMenuBlock != null) {
enlargedMenuBlock.setEnlarged(false);
enlargedMenuBlock = null;
}
boolean tabKeyPressed = false;
if (src == cancelButton) {
log("pressed: cancel");
notifyActionListeners(new ActionEvent(this, "Cancel"));
return;
} else if (src == okButton) {
log("pressed: ok");
handleTextInput(true);
update();
notifyActionListeners(new ActionEvent(this, "OK"));
return;
} else if (src == deleteButton) {
log("pressed: < delete");
removeLastToken();
} else if (src == clearButton) {
log("pressed: clear");
clearTokens();
} else if (src == textField) {
if (getApplicationInstance().getFocusedComponent() == dummyTextField) {
log("pressed: tab-key");
handleTextInput(false);
tabKeyPressed = true;
} else {
log("pressed: enter-key");
if (textField.getText().equals("") && menuBlockManager.getFilter().equals("")) {
notifyActionListeners(new ActionEvent(this, "Enter"));
return;
} else {
handleTextInput(true);
}
}
} else if (src instanceof MenuEntry) {
TextElement te = ((MenuEntry) e.getSource()).getTextElement();
log("pressed: menu-entry " + te.getText());
textElementSelected(te);
textField.setText("");
} else if ("enlarge".equals(c) && src instanceof MenuBlock) {
enlargedMenuBlock = (MenuBlock) src;
} else if ("Esc".equals(c)) {
log("pressed: escape key");
notifyActionListeners(new ActionEvent(this, "Escape"));
return;
} else if ("Ctrl-Backspace".equals(c)) {
log("pressed: ctrl-backspace");
if (getTokenCount() > 0) {
textContainer.removeLastElement();
parser.removeToken();
textField.setText("");
}
}
update();
if (tabKeyPressed) {
String s = menuBlockManager.getStartString();
if (s != null) textField.setText(s);
}
}
/**
* Removes the last token.
*/
public void removeLastToken() {
if (getTokenCount() > 0) {
textContainer.removeLastElement();
parser.removeToken();
textField.setText("");
}
}
/**
* Removes all tokens.
*/
public void clearTokens() {
textContainer.removeAllElements();
parser.removeAllTokens();
textField.setText("");
}
/**
* Returns true if the current text is a complete statement.
*
* @return true if the current text is a complete statement.
*/
public boolean isTextComplete() {
return parser.isComplete();
}
/**
* Returns the predictive parser. Do not modify this object while the preditor window is
* active!
*
* @return The predictive parser.
*/
public PredictiveParser getPredictiveParser() {
return parser;
}
/**
* Returns a localized string.
*
* @param key The text key.
* @return The text.
*/
protected String getLocalized(String key) {
return LocaleResources.getString(key);
}
public void windowPaneClosing(WindowPaneEvent e) {
log("pressed: close window");
notifyActionListeners(new ActionEvent(this, "Close"));
}
public void init() {
isInitialized = true;
update();
super.init();
}
/**
* Sets the logger context.
*
* @param loggerContext The logger context object or null.
*/
public void setLoggerContext(LoggerContext loggerContext) {
this.loggerContext = loggerContext;
}
private void log(String text) {
if (loggerContext != null) {
loggerContext.propagateWithinThread();
org.slf4j.MDC.put("type", "pred");
log.info(text);
}
}
public String toString() {
return "sentence: " + textContainer.getText();
}
}