/**************************************************************************
OmegaT - Computer Assisted Translation (CAT) tool
with fuzzy matching, translation memory, keyword search,
glossaries, and translation leveraging into updated projects.
Copyright (C) 2013 Zoltan Bartko, Aaron Madlon-Kay
2014-2015 Aaron Madlon-Kay
Home page: http://www.omegat.org/
Support center: http://groups.yahoo.com/group/OmegaT/
This file is part of OmegaT.
OmegaT is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
OmegaT 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
**************************************************************************/
package org.omegat.gui.editor.autocompleter;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JLabel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder;
import javax.swing.border.MatteBorder;
import javax.swing.text.BadLocationException;
import org.omegat.gui.editor.EditorTextArea3;
import org.omegat.gui.editor.TagAutoCompleterView;
import org.omegat.gui.editor.autotext.AutotextAutoCompleterView;
import org.omegat.gui.editor.chartable.CharTableAutoCompleterView;
import org.omegat.gui.editor.history.HistoryCompleter;
import org.omegat.gui.editor.history.HistoryPredictor;
import org.omegat.gui.glossary.GlossaryAutoCompleterView;
import org.omegat.util.Log;
import org.omegat.util.OStrings;
import org.omegat.util.Preferences;
import org.omegat.util.StringUtil;
import org.omegat.util.gui.StaticUIUtils;
/**
* The controller part of the auto-completer
*
* @author Zoltan Bartko (bartkozoltan@bartkozoltan.com)
* @author Aaron Madlon-Kay
*/
public class AutoCompleter implements IAutoCompleter {
private final static int MIN_VIEWPORT_HEIGHT = 50;
private final static int MAX_POPUP_WIDTH = 500;
JPopupMenu popup = new JPopupMenu();
private EditorTextArea3 editor;
private AutoCompleterKeys keys;
public final static int PAGE_ROW_COUNT = 10;
boolean didPopUpAutomatically = false;
/**
* a list of the views associated with this auto-completer
*/
List<AbstractAutoCompleterView> views = new ArrayList<AbstractAutoCompleterView>();
/**
* the current view
*/
int currentView = -1;
JScrollPane scroll;
JLabel viewLabel;
public AutoCompleter(EditorTextArea3 editor) {
this.editor = editor;
scroll = new JScrollPane();
scroll.setBorder(new EmptyBorder(0, 0, 0, 0));
scroll.setPreferredSize(new Dimension(200, 200));
scroll.setColumnHeaderView(null);
scroll.setFocusable(false);
scroll.getVerticalScrollBar().setFocusable(false);
scroll.getHorizontalScrollBar().setFocusable(false);
// add any views here
addView(new GlossaryAutoCompleterView());
addView(new AutotextAutoCompleterView());
addView(new TagAutoCompleterView());
addView(new CharTableAutoCompleterView());
addView(new HistoryCompleter());
addView(new HistoryPredictor());
viewLabel = new JLabel();
viewLabel.setBorder(new CompoundBorder(
new MatteBorder(1, 0, 0, 0, UIManager.getColor("OmegaTBorder.color")),
new EmptyBorder(5, 5, 5, 5)));
viewLabel.setOpaque(true);
popup.setBorder(new MatteBorder(1, 1, 1, 1, UIManager.getColor("OmegaTBorder.color")));
popup.setLayout(new BorderLayout());
popup.add(scroll, BorderLayout.CENTER);
popup.add(viewLabel, BorderLayout.SOUTH);
resetKeys();
}
@Override
public void addView(AbstractAutoCompleterView view) {
view.setParent(this);
views.add(view);
}
EditorTextArea3 getEditor() {
return editor;
}
/**
* Process the autocompletion keys
* @param e the key event to process
* @return true if a key has been processed, false if otherwise.
*/
public boolean processKeys(KeyEvent e) {
KeyStroke s = KeyStroke.getKeyStrokeForEvent(e);
if (!isVisible() && s.equals(keys.trigger)) {
if (!editor.isInActiveTranslation(editor.getCaretPosition())) {
return false;
}
if (!canBecomeVisible()) {
return false;
}
updatePopup(false);
setVisible(true);
return true;
}
if (isVisible()) {
if (getCurrentView().processKeys(e)) {
return true;
}
if (s.equals(keys.confirmAndClose)) {
doSelection();
return true;
}
if (s.equals(keys.confirmWithoutClose)) {
acceptedListItem(getSelectedValue());
updatePopup(false);
return true;
}
if (s.equals(keys.close)) {
setVisible(false);
return true;
}
if (s.equals(keys.prevView)) {
selectPreviousView();
return true;
}
if (s.equals(keys.nextView)) {
selectNextView();
return true;
}
}
// otherwise
return false;
}
public void doSelection() {
acceptedListItem(getSelectedValue());
if (getCurrentView().shouldCloseOnSelection()) {
setVisible(false);
}
}
/**
* Returns the currently selected value.
* @return
*/
private AutoCompleterItem getSelectedValue() {
return views.get(currentView).getSelectedValue();
}
/**
* Show the popup list.
*/
public void updatePopup(boolean onlyIfVisible) {
if (onlyIfVisible && !isVisible()) {
return;
}
if (!canBecomeVisible()) {
return;
}
AbstractAutoCompleterView view = getCurrentView();
view.updateViewData();
scroll.setPreferredSize(new Dimension(Math.min(view.getPreferredWidth(), MAX_POPUP_WIDTH),
Math.max(view.getPreferredHeight(), MIN_VIEWPORT_HEIGHT)));
popup.validate();
popup.pack();
if (isVisible()) {
Point p = getDisplayPoint();
popup.show(editor, p.x, p.y);
}
}
/**
* Determine the x,y coordinate at which to place the popup.
*/
private Point getDisplayPoint() {
int x = 0;
int y = editor.getHeight();
int fontSize = editor.getFont().getSize();
try {
int pos = Math.min(editor.getCaret().getDot(), editor.getCaret().getMark());
x = editor.getUI().modelToView(editor, pos).x;
y = editor.getUI().modelToView(editor, editor.getCaret().getDot()).y + fontSize;
} catch(BadLocationException e) {
// this should never happen!!!
Log.log(e);
}
return new Point(x, y);
}
/**
* Replace the text in the editor with the accepted item.
* @param selected
*/
protected void acceptedListItem(AutoCompleterItem selected) {
if (selected == null) {
return;
}
int offset = editor.getCaretPosition();
String selection = editor.getSelectedText();
if (StringUtil.isEmpty(selection)) {
editor.setSelectionStart(offset - selected.replacementLength);
editor.setSelectionEnd(offset);
}
editor.replaceSelection(selected.payload);
if (selected.cursorAdjust != 0) {
editor.getCaret().setDot(editor.getCaretPosition() + selected.cursorAdjust);
}
if (selected.keepSelection) {
editor.replaceSelection(selection);
}
}
/**
* get the view number of the next view
* @return the number
*/
private int nextViewNumber(int start) {
for (int n = 1; n <= views.size(); n++) {
int index = (start + n) % views.size();
if (views.get(index).isEnabled()) {
return index;
}
}
return -1;
}
/**
* Get the view number of the previous view.
* @return
*/
private int prevViewNumber(int start) {
for (int n = 1; n <= views.size(); n++) {
int index = (currentView + views.size() - n) % views.size();
if (views.get(index).isEnabled()) {
return index;
}
}
return -1;
}
/**
* Update the view label
*/
private void updateViewLabel() {
StringBuilder sb = new StringBuilder("<html>");
sb.append("<b>");
sb.append(getCurrentView().getName());
sb.append("</b>");
if (views.size() != 1) {
int nextViewN = nextViewNumber(currentView);
if (views.size() >= 2 && nextViewN != -1) {
sb.append("<br>");
sb.append(OStrings.getString("AC_NEXT_VIEW",
StaticUIUtils.getKeyStrokeText(keys.nextView),
views.get(nextViewN).getName()));
}
int prevViewN = prevViewNumber(currentView);
if (views.size() > 2 && prevViewN != -1) {
sb.append("<br>");
sb.append(OStrings.getString("AC_PREV_VIEW",
StaticUIUtils.getKeyStrokeText(keys.prevView),
views.get(prevViewN).getName()));
}
}
sb.append("</html>");
viewLabel.setText(sb.toString());
}
public AbstractAutoCompleterView getCurrentView() {
return views.get(currentView);
}
/** go to the next view */
private boolean selectNextView() {
int nextViewN = nextViewNumber(currentView);
if (nextViewN == -1) {
return false;
}
currentView = nextViewN;
activateView(true);
return true;
}
/** activate the current view */
private void activateView(boolean onlyIfVisible) {
scroll.setViewportView(getCurrentView().getViewContent());
updateViewLabel();
updatePopup(onlyIfVisible);
}
/** select the previous view */
private boolean selectPreviousView() {
int prevViewN = prevViewNumber(currentView);
if (prevViewN == -1) {
return false;
}
currentView = prevViewN;
activateView(true);
return true;
}
public boolean isVisible() {
return popup.isVisible();
}
public void setVisible(boolean isVisible) {
if (isVisible) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
Point p = getDisplayPoint();
popup.show(editor, p.x, p.y);
editor.requestFocus();
}
});
} else {
popup.setVisible(false);
didPopUpAutomatically = false;
}
}
private boolean canBecomeVisible() {
return currentView != -1 || selectNextView();
}
/**
* get the key text
* @param base
* @param modifier
* @return
*/
public String keyText(int base, int modifier) {
return KeyEvent.getKeyModifiersText(modifier) + "+" + KeyEvent.getKeyText(base);
}
public void textDidChange() {
if (isVisible() && !didPopUpAutomatically) {
updatePopup(true);
return;
}
if (!Preferences.isPreference(Preferences.AC_SHOW_SUGGESTIONS_AUTOMATICALLY)) {
return;
}
if (!canBecomeVisible()) {
return;
}
// Cycle through each view, stopping and showing it if it has some relevant content.
int i = currentView;
while (true) {
if (views.get(i).shouldPopUp()) {
currentView = i;
didPopUpAutomatically = true;
activateView(false);
setVisible(true);
return;
}
i = nextViewNumber(i);
if (i == currentView) {
// We made it full circle with no results.
break;
}
}
// If we get here, no views had relevant content, so we close the popup.
setVisible(false);
}
@Override
final public void resetKeys() {
keys = new AutoCompleterKeys();
if (canBecomeVisible()) {
updateViewLabel();
}
}
AutoCompleterKeys getKeys() {
return keys;
}
}