package org.basex.gui.layout;
import static org.basex.core.Text.*;
import static org.basex.gui.layout.BaseXKeys.*;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Font;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentEvent;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.util.Arrays;
import javax.swing.AbstractButton;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.border.MatteBorder;
import org.basex.gui.GUI;
import org.basex.gui.GUICommand;
import org.basex.gui.GUIConstants;
import org.basex.gui.GUIConstants.Fill;
import org.basex.io.IO;
import org.basex.util.TokenBuilder;
import org.basex.util.Undo;
import static org.basex.util.Token.*;
/**
* This class provides a text viewer and editor, using the
* {@link BaseXTextRenderer} class to render the text.
*
* @author BaseX Team 2005-12, BSD License
* @author Christian Gruen
*/
public class BaseXEditor extends BaseXPanel {
/** Text array to be written. */
transient BaseXTextTokens text = new BaseXTextTokens(EMPTY);
/** Renderer reference. */
final BaseXTextRenderer rend;
/** Undo history; if set to {@code null}, text will be read-only. */
final transient Undo undo;
/** Scrollbar reference. */
private final BaseXBar scroll;
/** Search field. */
private BaseXTextField find;
/**
* Default constructor.
* @param edit editable flag
* @param win parent window
*/
public BaseXEditor(final boolean edit, final Window win) {
super(win);
setFocusable(true);
setFocusTraversalKeysEnabled(!edit);
BaseXLayout.addInteraction(this, win);
addMouseMotionListener(this);
addMouseWheelListener(this);
addComponentListener(this);
addMouseListener(this);
addKeyListener(this);
addFocusListener(new FocusAdapter() {
@Override
public void focusGained(final FocusEvent e) {
if(isEnabled()) cursor(true);
}
@Override
public void focusLost(final FocusEvent e) {
cursor(false);
rend.cursor(false);
rend.repaint();
}
});
layout(new BorderLayout(4, 0));
scroll = new BaseXBar(this);
rend = new BaseXTextRenderer(text, scroll);
add(rend, BorderLayout.CENTER);
add(scroll, BorderLayout.EAST);
Undo un = null;
if(edit) {
setBackground(Color.white);
setBorder(new MatteBorder(1, 1, 0, 0, GUIConstants.color(6)));
un = new Undo();
} else {
mode(Fill.NONE);
}
undo = un;
new BaseXPopup(this, edit ?
new GUICommand[] { new UndoCmd(), new RedoCmd(), null, new CutCmd(),
new CopyCmd(), new PasteCmd(), new DelCmd(), null, new AllCmd() } :
new GUICommand[] { new CopyCmd(), null, new AllCmd() });
}
/**
* Sets the output text.
* @param t output text
*/
public void setText(final byte[] t) {
setText(t, t.length);
if(undo != null) undo.reset(t);
}
/**
* Adds a search dialog.
* @param f search field
*/
public final void setSearch(final BaseXTextField f) {
f.setSearch(this);
find = f;
}
/**
* Returns the cursor coordinates.
* @return line/column
*/
public final String pos() {
final int[] pos = rend.pos();
return pos[0] + " : " + pos[1];
}
/**
* Finds the specified term.
* @param t output text
* @param b backward browsing
*/
final void find(final String t, final boolean b) {
scroll(rend.find(t, b));
}
/**
* Displays the search term.
* @param y vertical position
*/
final void scroll(final int y) {
// updates the visible area
final int p = scroll.pos();
final int m = y + rend.fontH() * 3 - getHeight();
if(y != 0 && (p < m || p > y)) scroll.pos(y - getHeight() / 2);
repaint();
}
/**
* Sets the output text.
* @param t output text
* @param s text size
*/
public final void setText(final byte[] t, final int s) {
// remove invalid characters and compare old with new string
int ns = 0;
final int ts = text.size();
final byte[] tt = text.text;
boolean eq = true;
for(int r = 0; r < s; ++r) {
final byte b = t[r];
// support characters, highlighting codes, tabs and newlines
if(b >= ' ' || b <= TokenBuilder.MARK || b == 0x09 || b == 0x0A) {
t[ns++] = t[r];
}
eq &= ns < ts && ns < s && t[ns] == tt[ns];
}
eq &= ns == ts;
// new text is different...
if(!eq) {
text = new BaseXTextTokens(t, ns);
rend.setText(text);
scroll.pos(0);
}
if(undo != null) undo.store(t.length != ns ? Arrays.copyOf(t, ns) : t, 0);
SwingUtilities.invokeLater(calc);
}
/**
* Sets a syntax highlighter, based on the file format.
* @param file file reference
*/
protected final void setSyntax(final IO file) {
setSyntax(file.name().endsWith(IO.JSONSUFFIX) ? new JSONSyntax() :
file.isXML() ? new XMLSyntax() : new XQuerySyntax());
}
/**
* Sets a syntax highlighter.
* @param s syntax reference
*/
public final void setSyntax(final BaseXSyntax s) {
rend.setSyntax(s);
}
/**
* Sets a new cursor position.
* @param p cursor position
*/
public final void setCaret(final int p) {
text.setCaret(p);
showCursor(1);
cursor(true);
}
/**
* Jumps to the end of the text.
*/
public final void scrollToEnd() {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
text.pos(text.size());
text.setCaret();
showCursor(2);
}
});
}
/**
* Returns the output text.
* @return output text
*/
public final byte[] getText() {
return text.toArray();
}
@Override
public final void setFont(final Font f) {
super.setFont(f);
if(rend != null) {
rend.setFont(f);
rend.repaint();
}
}
/**
* Refreshes the syntax highlighting.
* @param s start of optional error mark
*/
public final void error(final int s) {
text.error(s);
rend.repaint();
}
@Override
public final void setEnabled(final boolean e) {
super.setEnabled(e);
rend.setEnabled(e);
scroll.setEnabled(e);
cursor(e);
}
/**
* Selects the whole text.
*/
final void selectAll() {
text.pos(0);
text.startMark();
text.pos(text.size());
text.endMark();
rend.repaint();
}
// MOUSE INTERACTIONS =======================================================
@Override
public final void mouseEntered(final MouseEvent e) {
gui.cursor(GUIConstants.CURSORTEXT);
}
@Override
public final void mouseExited(final MouseEvent e) {
gui.cursor(GUIConstants.CURSORARROW);
}
@Override
public void mouseReleased(final MouseEvent e) {
if(SwingUtilities.isLeftMouseButton(e)) rend.stopSelect();
}
@Override
public void mouseClicked(final MouseEvent e) {
if(!SwingUtilities.isMiddleMouseButton(e)) return;
if(!paste()) return;
finish();
repaint();
}
@Override
public final void mouseDragged(final MouseEvent e) {
if(!SwingUtilities.isLeftMouseButton(e)) return;
// selection mode
rend.select(scroll.pos(), e.getPoint(), true);
final int y = Math.max(20, Math.min(e.getY(), getHeight() - 20));
if(y != e.getY()) scroll.pos(scroll.pos() + e.getY() - y);
}
@Override
public final void mousePressed(final MouseEvent e) {
if(!isEnabled() || !isFocusable()) return;
requestFocusInWindow();
cursor(true);
if(SwingUtilities.isMiddleMouseButton(e)) copy();
if(SwingUtilities.isLeftMouseButton(e)) {
final int c = e.getClickCount();
if(c == 1) {
// selection mode
rend.select(scroll.pos(), e.getPoint(), false);
} else if(c == 2) {
selectWord();
} else {
selectLine();
}
} else if(!text.marked()) {
rend.select(scroll.pos(), e.getPoint(), false);
}
}
/**
* Selects the word at the cursor position.
*/
private void selectWord() {
text.pos(text.cursor());
final boolean ch = ftChar(text.prev(true));
while(text.pos() > 0) {
final int c = text.prev(true);
if(c == '\n' || ch != ftChar(c)) break;
}
if(text.pos() != 0) text.next(true);
text.startMark();
while(text.pos() < text.size()) {
final int c = text.curr();
if(c == '\n' || ch != ftChar(c)) break;
text.next(true);
}
text.endMark();
}
/**
* Selects the word at the cursor position.
*/
private void selectLine() {
text.pos(text.cursor());
text.bol(true);
text.startMark();
text.forward(Integer.MAX_VALUE, true);
text.endMark();
}
// KEY INTERACTIONS =======================================================
@Override
public void keyPressed(final KeyEvent e) {
if(modifier(e)) return;
// operations that change the focus are put first..
if(PREVTAB.is(e)) {
transferFocusBackward();
return;
}
if(NEXTTAB.is(e)) {
transferFocus();
return;
}
if(FIND.is(e)) {
if(find != null) find.requestFocusInWindow();
return;
}
// re-animate cursor
cursor(true);
// operations without cursor movement...
final int fh = rend.fontH();
if(SCROLLDOWN.is(e)) {
scroll.pos(scroll.pos() + fh);
return;
}
if(SCROLLUP.is(e)) {
scroll.pos(scroll.pos() - fh);
return;
}
if(COPY1.is(e) || COPY2.is(e)) {
copy();
return;
}
// set cursor position and reset last column
text.pos(text.cursor());
if(!PREVLINE.is(e) && !NEXTLINE.is(e)) lastCol = -1;
if(FINDNEXT.is(e) || FINDPREV.is(e) || FINDNEXT2.is(e) || FINDPREV2.is(e)) {
scroll(rend.find(FINDPREV.is(e) || FINDPREV2.is(e), true));
return;
}
if(SELECTALL.is(e)) {
selectAll();
text.setCaret();
return;
}
final boolean marking = e.isShiftDown() &&
!DELNEXT.is(e) && !DELPREV.is(e) && !PASTE2.is(e) && !COMMENT.is(e)
// necessary on Macs as the shift button is pressed for REDO
&& !REDOSTEP.is(e);
final boolean nomark = !text.marked();
if(marking && nomark) text.startMark();
boolean down = true;
boolean consumed = true;
// operations that consider the last text mark..
final byte[] txt = text.text;
if(NEXTWORD.is(e)) {
text.nextToken(marking);
} else if(PREVWORD.is(e)) {
text.prevToken(marking);
down = false;
} else if(TEXTSTART.is(e)) {
if(!marking) text.noMark();
text.pos(0);
down = false;
} else if(TEXTEND.is(e)) {
if(!marking) text.noMark();
text.pos(text.size());
} else if(LINESTART.is(e)) {
text.bol(marking);
down = false;
} else if(LINEEND.is(e)) {
text.forward(Integer.MAX_VALUE, marking);
} else if(NEXTPAGE.is(e)) {
down(getHeight() / fh, marking);
} else if(PREVPAGE.is(e)) {
up(getHeight() / fh, marking);
down = false;
} else if(NEXT.is(e)) {
text.next(marking);
} else if(PREV.is(e)) {
text.prev(marking);
down = false;
} else if(PREVLINE.is(e)) {
up(1, marking);
down = false;
} else if(NEXTLINE.is(e)) {
down(1, marking);
} else {
consumed = false;
}
if(marking) {
// refresh scroll position
text.endMark();
text.checkMark();
} else if(undo != null) {
// edit operations...
if(CUT1.is(e) || CUT2.is(e)) {
cut();
} else if(PASTE1.is(e) || PASTE2.is(e)) {
paste();
} else if(UNDOSTEP.is(e)) {
undo();
} else if(REDOSTEP.is(e)) {
redo();
} else if(COMMENT.is(e)) {
text.comment(rend.getSyntax());
} else if(DELLINEEND.is(e) || DELNEXTWORD.is(e) || DELNEXT.is(e)) {
if(nomark) {
if(text.pos() == text.size()) return;
text.startMark();
if(DELNEXTWORD.is(e)) {
text.nextToken(true);
} else if(DELLINEEND.is(e)) {
text.forward(Integer.MAX_VALUE, true);
} else {
text.next(true);
}
text.endMark();
}
undo.cursor(text.cursor());
text.delete();
} else if(DELLINESTART.is(e) || DELPREVWORD.is(e) || DELPREV.is(e)) {
if(nomark) {
if(text.pos() == 0) return;
text.startMark();
if(DELPREVWORD.is(e)) {
text.prevToken(true);
} else if(DELLINESTART.is(e)) {
text.bol(true);
} else {
text.prev();
}
text.endMark();
}
undo.cursor(text.cursor());
text.delete();
down = false;
} else {
consumed = false;
}
}
if(consumed) e.consume();
text.setCaret();
if(txt != text.text) rend.calc();
showCursor(down ? 2 : 0);
}
/**
* Displays the currently edited text area.
* @param align vertical alignment
*/
final void showCursor(final int align) {
// updates the visible area
final int p = scroll.pos();
final int y = rend.cursorY();
final int m = y + rend.fontH() * 3 - getHeight();
if(p < m || p > y) {
scroll.pos(align == 0 ? y : align == 1 ? y - getHeight() / 2 : m);
rend.repaint();
}
}
/**
* Moves the cursor down. The current column position is remembered in
* {@link #lastCol} and, if possible, restored.
* @param l number of lines to move cursor
* @param mark mark flag
*/
private void down(final int l, final boolean mark) {
if(!mark) text.noMark();
final int x = text.bol(mark);
if(lastCol == -1) lastCol = x;
for(int i = 0; i < l; ++i) {
text.forward(Integer.MAX_VALUE, mark);
text.next(mark);
}
text.forward(lastCol, mark);
if(text.pos() == text.size()) lastCol = -1;
}
/**
* Moves the cursor up.
* @param l number of lines to move cursor
* @param mark mark flag
*/
private void up(final int l, final boolean mark) {
if(!mark) text.noMark();
final int x = text.bol(mark);
if(lastCol == -1) lastCol = x;
if(text.pos() == 0) {
lastCol = -1;
return;
}
for(int i = 0; i < l; ++i) {
text.prev(mark);
text.bol(mark);
}
text.forward(lastCol, mark);
}
/** Last horizontal position. */
private int lastCol = -1;
@Override
public final void keyTyped(final KeyEvent e) {
if(undo == null || control(e) || DELNEXT.is(e) || DELPREV.is(e) ||
ESCAPE.is(e)) return;
text.pos(text.cursor());
boolean indent = false;
if(text.marked()) {
// count number of lines to indent
if(TAB.is(e)) {
final int s = Math.min(text.pos(), text.start());
final int l = Math.max(text.pos(), text.start()) - 1;
for(int p = s; p <= l && p < text.size(); p++) {
indent |= text.text[p] == '\n';
}
if(indent) text.indent(s, l, e.isShiftDown());
}
if(!indent) text.delete();
}
if(ENTER.is(e)) {
// adopt indentation from previous line
final StringBuilder sb = new StringBuilder(1).append(e.getKeyChar());
int s = 0, t = 0;
for(int p = text.pos() - 1; p >= 0; p--) {
final byte b = text.text[p];
if(b == '\n') {
break;
} else if(b == '\t') {
t++;
} else if(b == ' ') {
s++;
} else {
t = 0;
s = 0;
}
}
for(int p = 0; p < t; p++) sb.append('\t');
for(int p = 0; p < s; p++) sb.append(' ');
text.add(sb.toString());
} else if(!indent) {
text.add(String.valueOf(e.getKeyChar()));
}
text.setCaret();
rend.calc();
showCursor(2);
e.consume();
}
@Override
public void keyReleased(final KeyEvent e) {
if(undo != null) undo.store(text.toArray(), text.cursor());
}
/**
* Releases a key or mouse. Can be overwritten to react on events.
* @param force force querying
*/
@SuppressWarnings("unused")
protected void release(final boolean force) { }
// EDITOR COMMANDS ==========================================================
/**
* Undoes the text.
*/
final void undo() {
if(undo == null) return;
text = new BaseXTextTokens(undo.prev());
rend.setText(text);
text.pos(undo.cursor());
text.setCaret();
}
/**
* Redoes the text.
*/
final void redo() {
if(undo == null) return;
text = new BaseXTextTokens(undo.next());
rend.setText(text);
text.pos(undo.cursor());
text.setCaret();
}
/**
* Cuts the selected text to the clipboard.
*/
final void cut() {
text.pos(text.cursor());
if(copy()) delete();
}
/**
* Copies the selected text to the clipboard.
* @return true if text was copied
*/
final boolean copy() {
final String txt = text.copy();
if(txt.isEmpty()) {
text.noMark();
return false;
}
// copy selection to clipboard
final Clipboard clip = Toolkit.getDefaultToolkit().getSystemClipboard();
clip.setContents(new StringSelection(txt), null);
return true;
}
/**
* Pastes the clipboard text.
* @return success flag
*/
final boolean paste() {
return paste(clip());
}
/**
* Pastes the specified string.
* @param txt string to be pasted
* @return success flag
*/
final boolean paste(final String txt) {
if(txt == null || undo == null) return false;
text.pos(text.cursor());
undo.cursor(text.cursor());
if(text.marked()) text.delete();
text.add(txt);
undo.store(text.toArray(), text.cursor());
return true;
}
/**
* Deletes the selected text.
*/
final void delete() {
if(undo == null) return;
text.pos(text.cursor());
undo.cursor(text.cursor());
text.delete();
undo.store(text.toArray(), text.cursor());
text.setCaret();
}
/**
* Returns the clipboard text.
* @return text
*/
static final String clip() {
// copy selection to clipboard
final Clipboard clip = Toolkit.getDefaultToolkit().getSystemClipboard();
final Transferable tr = clip.getContents(null);
if(tr != null) {
for(final Object o : BaseXLayout.contents(tr)) return o.toString();
}
return "";
}
/**
* Finishes a command.
*/
void finish() {
text.setCaret();
rend.calc();
showCursor(2);
release(false);
}
/** Cursor. */
private final Timer cursor = new Timer(500, new ActionListener() {
@Override
public void actionPerformed(final ActionEvent e) {
rend.cursor(!rend.cursor());
rend.repaint();
}
});
/**
* Handles the cursor thread; interrupts the old thread as soon as
* new one has been started.
* @param start start/stop flag
*/
final void cursor(final boolean start) {
cursor.stop();
if(start) cursor.start();
rend.cursor(start);
rend.repaint();
}
@Override
public final void mouseWheelMoved(final MouseWheelEvent e) {
scroll.pos(scroll.pos() + e.getUnitsToScroll() * 20);
rend.repaint();
}
@Override
public final void componentResized(final ComponentEvent e) {
scroll.pos(0);
SwingUtilities.invokeLater(calc);
}
/** Calculation counter. */
private final transient Runnable calc = new Runnable() {
@Override
public void run() {
rend.calc();
rend.repaint();
}
};
/** Text command. */
abstract static class TextCmd implements GUICommand {
@Override
public boolean checked() {
return false;
}
@Override
public String help() {
return null;
}
@Override
public String key() {
return null;
}
}
/** Undo command. */
class UndoCmd extends TextCmd {
@Override
public void execute(final GUI main) {
undo();
finish();
}
@Override
public void refresh(final GUI main, final AbstractButton button) {
button.setEnabled(!undo.first());
}
@Override
public String label() {
return UNDO;
}
}
/** Redo command. */
class RedoCmd extends TextCmd {
@Override
public void execute(final GUI main) {
redo();
finish();
}
@Override
public void refresh(final GUI main, final AbstractButton button) {
button.setEnabled(!undo.last());
}
@Override
public String label() {
return REDO;
}
}
/** Cut command. */
class CutCmd extends TextCmd {
@Override
public void execute(final GUI main) {
cut();
finish();
}
@Override
public void refresh(final GUI main, final AbstractButton button) {
button.setEnabled(text.marked());
}
@Override
public String label() {
return CUT;
}
}
/** Copy command. */
class CopyCmd extends TextCmd {
@Override
public void execute(final GUI main) {
copy();
}
@Override
public void refresh(final GUI main, final AbstractButton button) {
button.setEnabled(text.marked());
}
@Override
public String label() {
return COPY;
}
}
/** Paste command. */
class PasteCmd extends TextCmd {
@Override
public void execute(final GUI main) {
if(paste()) finish();
}
@Override
public void refresh(final GUI main, final AbstractButton button) {
button.setEnabled(clip() != null);
}
@Override
public String label() {
return PASTE;
}
}
/** Delete command. */
class DelCmd extends TextCmd {
@Override
public void execute(final GUI main) {
delete();
finish();
}
@Override
public void refresh(final GUI main, final AbstractButton button) {
button.setEnabled(text.marked());
}
@Override
public String label() {
return DELETE;
}
}
/** Select all command. */
class AllCmd extends TextCmd {
@Override
public void execute(final GUI main) {
selectAll();
}
@Override
public void refresh(final GUI main, final AbstractButton button) {
}
@Override
public String label() {
return SELECT_ALL;
}
}
}