/*
* Copyright (c) 2010-2016, Sikuli.org, sikulix.com
* Released under the MIT License.
*
*/
package org.sikuli.ide;
import org.sikuli.basics.PreferencesUser;
import java.awt.*;
import java.awt.datatransfer.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;
import java.util.List;
import java.util.prefs.PreferenceChangeEvent;
import java.util.prefs.PreferenceChangeListener;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.*;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.*;
import java.nio.charset.Charset;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.sikuli.basics.Settings;
import org.sikuli.basics.Debug;
import org.sikuli.basics.FileManager;
import org.sikuli.idesupport.IIndentationLogic;
import org.sikuli.script.Location;
import org.sikuli.script.Image;
import org.sikuli.script.ImagePath;
import org.sikuli.script.Runner;
import org.sikuli.script.Sikulix;
import org.sikuli.scriptrunner.IScriptRunner;
import org.sikuli.scriptrunner.ScriptingSupport;
import org.sikuli.syntaxhighlight.ResolutionException;
import org.sikuli.syntaxhighlight.grammar.Lexer;
import org.sikuli.syntaxhighlight.grammar.Token;
import org.sikuli.syntaxhighlight.grammar.TokenType;
import org.sikuli.util.SikulixFileChooser;
public class EditorPane extends JTextPane implements KeyListener, CaretListener {
private static final String me = "EditorPane: ";
private static final int lvl = 3;
private static void log(int level, String message, Object... args) {
Debug.logx(level, me + message, args);
}
private static TransferHandler transferHandler = null;
private static final Map<String, Lexer> lexers = new HashMap<String, Lexer>();
private PreferencesUser pref;
private File scriptSource = null;
private File scriptParent = null;
private File scriptImages = null;
private String scriptType = null;
private boolean scriptIsTemp = false;
private boolean scriptIsDirty = false;
private DirtyHandler dirtyHandler;
private File _editingFile;
// private String scriptType = null;
private String _srcBundlePath = null;
private boolean _srcBundleTemp = false;
private String sikuliContentType;
private EditorCurrentLineHighlighter _highlighter = null;
private EditorUndoManager _undo = null;
// TODO: move to SikuliDocument ????
private IIndentationLogic _indentationLogic = null;
private boolean hasErrorHighlight = false;
public boolean showThumbs;
static Pattern patPngStr = Pattern.compile("(\"[^\"]+?\\.(?i)(png|jpg|jpeg)\")");
static Pattern patCaptureBtn = Pattern.compile("(\"__CLICK-TO-CAPTURE__\")");
static Pattern patPatternStr = Pattern.compile(
"\\b(Pattern\\s*\\(\".*?\"\\)(\\.\\w+\\([^)]*\\))+)");
static Pattern patRegionStr = Pattern.compile(
"\\b(Region\\s*\\((-?[\\d\\s],?)+\\))");
static Pattern patLocationStr = Pattern.compile(
"\\b(Location\\s*\\((-?[\\d\\s],?)+\\))");
//TODO what is it for???
private int _caret_last_x = -1;
private boolean _can_update_caret_last_x = true;
private SikuliIDEPopUpMenu popMenuImage;
private SikuliEditorKit editorKit;
private EditorViewFactory editorViewFactory;
private SikuliIDE sikuliIDE = null;
private int caretPosition = -1;
//<editor-fold defaultstate="collapsed" desc="Initialization">
public EditorPane(SikuliIDE ide) {
pref = PreferencesUser.getInstance();
showThumbs = !pref.getPrefMorePlainText();
sikuliIDE = ide;
log(lvl, "EditorPane: creating new pane (constructor)");
}
public void saveCaretPosition() {
caretPosition = getCaretPosition();
}
public void restoreCaretPosition() {
if (caretPosition < 0) {
return;
}
if (caretPosition < getDocument().getLength()) {
setCaretPosition(caretPosition);
} else {
setCaretPosition(getDocument().getLength() - 1);
}
caretPosition = -1;
}
public void initBeforeLoad(String scriptType) {
initBeforeLoad(scriptType, false);
}
public void reInit(String scriptType) {
initBeforeLoad(scriptType, true);
}
private void initBeforeLoad(String scriptType, boolean reInit) {
String scrType = null;
boolean paneIsEmpty = false;
log(lvl, "initBeforeLoad: %s", scriptType);
if (scriptType == null) {
scriptType = Runner.EDEFAULT;
paneIsEmpty = true;
}
if (Runner.EPYTHON.equals(scriptType)) {
scrType = Runner.CPYTHON;
_indentationLogic = SikuliIDE.getIDESupport(scriptType).getIndentationLogic();
_indentationLogic.setTabWidth(pref.getTabWidth());
} else if (Runner.ERUBY.equals(scriptType)) {
scrType = Runner.CRUBY;
_indentationLogic = null;
}
//TODO should know, that scripttype not changed here to avoid unnecessary new setups
if (scrType != null) {
sikuliContentType = scrType;
editorKit = new SikuliEditorKit();
editorViewFactory = (EditorViewFactory) editorKit.getViewFactory();
setEditorKit(editorKit);
setContentType(scrType);
if (_indentationLogic != null) {
pref.addPreferenceChangeListener(new PreferenceChangeListener() {
@Override
public void preferenceChange(PreferenceChangeEvent event) {
if (event.getKey().equals("TAB_WIDTH")) {
_indentationLogic.setTabWidth(Integer.parseInt(event.getNewValue()));
}
}
});
}
}
if (transferHandler == null) {
transferHandler = new MyTransferHandler();
}
setTransferHandler(transferHandler);
if (_highlighter == null) {
_highlighter = new EditorCurrentLineHighlighter(this);
addCaretListener(_highlighter);
initKeyMap();
addKeyListener(this);
addCaretListener(this);
}
setFont(new Font(pref.getFontName(), Font.PLAIN, pref.getFontSize()));
setMargin(new Insets(3, 3, 3, 3));
setBackground(Color.WHITE);
if (!Settings.isMac()) {
setSelectionColor(new Color(170, 200, 255));
}
updateDocumentListeners("initBeforeLoad");
popMenuImage = new SikuliIDEPopUpMenu("POP_IMAGE", this);
if (!popMenuImage.isValidMenu()) {
popMenuImage = null;
}
if (paneIsEmpty || reInit) {
// this.setText(String.format(Settings.TypeCommentDefault, getSikuliContentType()));
this.setText("");
}
SikuliIDE.getStatusbar().setCurrentContentType(getSikuliContentType());
log(lvl, "InitTab: (%s)", getSikuliContentType());
if (!ScriptingSupport.hasTypeRunner(getSikuliContentType())) {
Sikulix.popup("No installed runner supports (" + getSikuliContentType() + ")\n"
+ "Trying to run the script will crash IDE!", "... serious problem detected!");
}
}
public String getSikuliContentType() {
return sikuliContentType;
}
public SikuliIDEPopUpMenu getPopMenuImage() {
return popMenuImage;
}
private void updateDocumentListeners(String source) {
log(lvl, "updateDocumentListeners from: %s", source);
if (dirtyHandler == null) {
dirtyHandler = new DirtyHandler();
}
getDocument().addDocumentListener(dirtyHandler);
getDocument().addUndoableEditListener(getUndoManager());
}
public EditorUndoManager getUndoManager() {
if (_undo == null) {
_undo = new EditorUndoManager();
}
return _undo;
}
public IIndentationLogic getIndentationLogic() {
return _indentationLogic;
}
private void initKeyMap() {
InputMap map = this.getInputMap();
int shift = InputEvent.SHIFT_MASK;
int ctrl = InputEvent.CTRL_MASK;
map.put(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, shift), SikuliEditorKit.deIndentAction);
map.put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, ctrl), SikuliEditorKit.deIndentAction);
}
@Override
public void keyPressed(java.awt.event.KeyEvent ke) {
}
@Override
public void keyReleased(java.awt.event.KeyEvent ke) {
}
@Override
public void keyTyped(java.awt.event.KeyEvent ke) {
//TODO implement code completion * checkCompletion(ke);
}
//</editor-fold>
public String loadFile(boolean accessingAsFile) throws IOException {
File file = new SikulixFileChooser(sikuliIDE, accessingAsFile).load();
if (file == null) {
return null;
}
String fname = FileManager.slashify(file.getAbsolutePath(), false);
int i = sikuliIDE.isAlreadyOpen(fname);
if (i > -1) {
log(lvl, "loadFile: Already open in IDE: " + fname);
return null;
}
loadFile(fname);
if (_editingFile == null) {
return null;
}
return _editingFile.getParent();
}
public void loadFile(String filename) {
log(lvl, "loadfile: %s", filename);
filename = FileManager.slashify(filename, false);
File script = new File(filename);
_editingFile = Runner.getScriptFile(script);
if (_editingFile != null) {
setSrcBundle(FileManager.slashify(_editingFile.getParent(), true));
scriptType = _editingFile.getAbsolutePath().substring(_editingFile.getAbsolutePath().lastIndexOf(".") + 1);
initBeforeLoad(scriptType);
if (!readScript(_editingFile)) {
_editingFile = null;
}
updateDocumentListeners("loadFile");
setDirty(false);
}
if (_editingFile == null) {
_srcBundlePath = null;
} else {
_srcBundleTemp = false;
}
}
private boolean readScript(Object script) {
InputStreamReader isr;
try {
if (script instanceof String) {
isr = new InputStreamReader(
new ByteArrayInputStream(((String) script).getBytes(Charset.forName("utf-8"))),
Charset.forName("utf-8"));
} else if (script instanceof File) {
isr = new InputStreamReader(
new FileInputStream((File) script),
Charset.forName("utf-8"));
} else {
log(-1, "readScript: not supported %s as %s", script, script.getClass());
return false;
}
this.read(new BufferedReader(isr), null);
} catch (Exception ex) {
log(-1, "read returned %s", ex.getMessage());
return false;
}
return true;
}
@Override
public void read(Reader in, Object desc) throws IOException {
super.read(in, desc);
Document doc = getDocument();
Element root = doc.getDefaultRootElement();
parse(root);
restoreCaretPosition();
}
public boolean hasEditingFile() {
return _editingFile != null;
}
public String saveFile() throws IOException {
if (_editingFile == null) {
return saveAsFile(Settings.isMac());
} else {
writeSrcFile();
return getCurrentShortFilename();
}
}
public String saveAsFile(boolean accessingAsFile) throws IOException {
File file = new SikulixFileChooser(sikuliIDE, accessingAsFile).save();
if (file == null) {
return null;
}
String bundlePath = FileManager.slashify(file.getAbsolutePath(), false);
if (!file.getAbsolutePath().endsWith(".sikuli")) {
bundlePath += ".sikuli";
}
if (FileManager.exists(bundlePath)) {
int res = JOptionPane.showConfirmDialog(
null, SikuliIDEI18N._I("msgFileExists", bundlePath),
SikuliIDEI18N._I("dlgFileExists"), JOptionPane.YES_NO_OPTION);
if (res != JOptionPane.YES_OPTION) {
return null;
}
FileManager.deleteFileOrFolder(bundlePath);
}
FileManager.mkdir(bundlePath);
try {
saveAsBundle(bundlePath, (sikuliIDE.getCurrentFileTabTitle()));
if (Settings.isMac()) {
if (!Settings.handlesMacBundles) {
makeBundle(bundlePath, accessingAsFile);
}
}
} catch (IOException iOException) {
}
return getCurrentShortFilename();
}
private void makeBundle(String path, boolean asFile) {
String isBundle = asFile ? "B" : "b";
String result = Sikulix.run(new String[]{"#SetFile", "-a", isBundle, path});
if (!result.isEmpty()) {
log(-1, "makeBundle: return: " + result);
}
if (asFile) {
if (!FileManager.writeStringToFile("/Applications/SikuliX-IDE.app",
(new File(path, ".LSOverride")).getAbsolutePath())) {
log(-1, "makeBundle: not possible: .LSOverride");
}
} else {
new File(path, ".LSOverride").delete();
}
}
private void saveAsBundle(String bundlePath, String current) throws IOException {
//TODO allow other file types
log(lvl, "saveAsBundle: " + getSrcBundle());
bundlePath = FileManager.slashify(bundlePath, true);
if (_srcBundlePath != null) {
if (!ScriptingSupport.transferScript(_srcBundlePath, bundlePath)) {
log(-1, "saveAsBundle: did not work - ");
}
}
ImagePath.remove(_srcBundlePath);
if (_srcBundleTemp) {
FileManager.deleteTempDir(_srcBundlePath);
_srcBundleTemp = false;
}
setSrcBundle(bundlePath);
_editingFile = createSourceFile(bundlePath, "." + Runner.typeEndings.get(sikuliContentType));
writeSrcFile();
reparse();
}
private File createSourceFile(String bundlePath, String ext) {
if (ext != null) {
String name = new File(bundlePath).getName();
name = name.substring(0, name.lastIndexOf("."));
return new File(bundlePath, name + ext);
} else {
return new File(bundlePath);
}
}
private void writeSrcFile() throws IOException {
log(lvl, "writeSrcFile: " + _editingFile.getName());
this.write(new BufferedWriter(new OutputStreamWriter(
new FileOutputStream(_editingFile.getAbsolutePath()), "UTF8")));
if (PreferencesUser.getInstance().getAtSaveMakeHTML()) {
convertSrcToHtml(getSrcBundle());
} else {
String snameDir = new File(_editingFile.getAbsolutePath()).getParentFile().getName();
String sname = snameDir.replace(".sikuli", "") + ".html";
(new File(snameDir, sname)).delete();
}
if (PreferencesUser.getInstance().getAtSaveCleanBundle()) {
if (!sikuliContentType.equals(Runner.CPYTHON)) {
log(lvl, "delete-not-used-images for %s using Python string syntax", sikuliContentType);
}
cleanBundle();
}
setDirty(false);
}
private void cleanBundle() {
log(3, "cleanBundle");
Set<String> foundImages = parseforImages().keySet();
if (foundImages.contains("uncomplete_comment_error")) {
log(-1, "cleanBundle aborted (uncomplete_comment_error)");
} else {
FileManager.deleteNotUsedImages(getBundlePath(), foundImages);
log(3, "cleanBundle finished");
}
}
private Map<String, List<Integer>> parseforImages() {
String pbundle = FileManager.slashify(getSrcBundle(), false);
log(3, "parseforImages: in \n%s", pbundle);
String scriptText = getText();
Lexer lexer = getLexer();
Map<String, List<Integer>> images = new HashMap<String, List<Integer>>();
parseforImagesWalk(pbundle, lexer, scriptText, 0, images, 0);
log(3, "parseforImages finished");
return images;
}
private void parseforImagesWalk(String pbundle, Lexer lexer,
String text, int pos, Map<String, List<Integer>> images, Integer line) {
//log(3, "parseforImagesWalk");
Iterable<Token> tokens = lexer.getTokens(text);
boolean inString = false;
String current;
String innerText;
String[] possibleImage = new String[]{""};
String[] stringType = new String[]{""};
for (Token t : tokens) {
current = t.getValue();
if (current.endsWith("\n")) {
line++;
if (inString) {
boolean answer = Sikulix.popAsk(String.format("Possible incomplete string in line %d\n" +
"\"%s\"\n" +
"Yes: No images will be deleted!\n" +
"No: Ignore and continue", line, text), "Delete images on save");
if (answer) {
log(-1, "DeleteImagesOnSave: possible incomplete string in line %d", line);
images.clear();
images.put("uncomplete_comment_error", null);
}
break;
}
}
if (t.getType() == TokenType.Comment) {
//log(3, "parseforImagesWalk::Comment");
innerText = t.getValue().substring(1);
parseforImagesWalk(pbundle, lexer, innerText, t.getPos() + 1, images, line);
continue;
}
if (t.getType() == TokenType.String_Doc) {
//log(3, "parseforImagesWalk::String_Doc");
innerText = t.getValue().substring(3, t.getValue().length() - 3);
parseforImagesWalk(pbundle, lexer, innerText, t.getPos() + 3, images, line);
continue;
}
if (!inString) {
inString = parseforImagesGetName(current, inString, possibleImage, stringType);
continue;
}
if (!parseforImagesGetName(current, inString, possibleImage, stringType)) {
inString = false;
parseforImagesCollect(pbundle, possibleImage[0], pos + t.getPos(), images);
continue;
}
}
}
private boolean parseforImagesGetName(String current, boolean inString,
String[] possibleImage, String[] stringType) {
//log(3, "parseforImagesGetName (inString: %s) %s", inString, current);
if (!inString) {
if (!current.isEmpty() && (current.contains("\"") || current.contains("'"))) {
possibleImage[0] = "";
stringType[0] = current.substring(current.length() - 1, current.length());
return true;
}
}
if (!current.isEmpty() && "'\"".contains(current) && stringType[0].equals(current)) {
return false;
}
if (inString) {
possibleImage[0] += current;
}
return inString;
}
private void parseforImagesCollect(String pbundle, String img, int pos, Map<String, List<Integer>> images) {
String fimg;
//log(3, "parseforImagesCollect");
if (img.endsWith(".png") || img.endsWith(".jpg") || img.endsWith(".jpeg")) {
fimg = FileManager.slashify(img, false);
if (fimg.contains("/")) {
if (!fimg.contains(pbundle)) {
return;
}
img = new File(fimg).getName();
}
if (images.containsKey(img)) {
images.get(img).add(pos);
} else {
List<Integer> poss = new ArrayList<Integer>();
poss.add(pos);
images.put(img, poss);
}
}
}
private Lexer getLexer() {
//TODO this only works for cleanbundle to find the image strings
String scriptType = "python";
if (null != lexers.get(scriptType)) {
return lexers.get(scriptType);
}
try {
Lexer lexer = Lexer.getByName(scriptType);
lexers.put(scriptType, lexer);
return lexer;
} catch (ResolutionException ex) {
return null;
}
}
public String exportAsZip() {
File file = new SikulixFileChooser(sikuliIDE).export();
if (file == null) {
return null;
}
String zipPath = file.getAbsolutePath();
if (!file.getAbsolutePath().endsWith(".skl")) {
zipPath += ".skl";
}
if (new File(zipPath).exists()) {
if (!Sikulix.popAsk(String.format("Overwrite existing file?\n%s", zipPath),
"Exporting packed SikuliX Script")) {
return null;
}
}
String pSource = _editingFile.getParent();
try {
writeSrcFile();
zipDir(pSource, zipPath, _editingFile.getName());
log(lvl, "Exported packed SikuliX Script to:\n%s", zipPath);
} catch (Exception ex) {
log(-1, "Exporting packed SikuliX Script did not work:\n%s", zipPath);
return null;
}
return zipPath;
}
private static void zipDir(String dir, String zipPath, String fScript) throws IOException {
ZipOutputStream zos = null;
try {
zos = new ZipOutputStream(new FileOutputStream(zipPath));
File zipDir = new File(dir);
String[] dirList = zipDir.list();
byte[] readBuffer = new byte[1024];
int bytesIn;
ZipEntry anEntry = null;
String ending = fScript.substring(fScript.length() - 3);
String sName = new File(zipPath).getName();
sName = sName.substring(0, sName.length() - 4) + ending;
for (int i = 0; i < dirList.length; i++) {
File f = new File(zipDir, dirList[i]);
if (f.isFile()) {
if (fScript.equals(f.getName())) {
anEntry = new ZipEntry(sName);
} else {
anEntry = new ZipEntry(f.getName());
}
FileInputStream fis = new FileInputStream(f);
zos.putNextEntry(anEntry);
while ((bytesIn = fis.read(readBuffer)) != -1) {
zos.write(readBuffer, 0, bytesIn);
}
fis.close();
}
}
} catch (Exception ex) {
String msg = "";
msg = ex.getMessage() + "";
} finally {
zos.close();
}
}
public boolean close() throws IOException {
log(lvl, "Tab close clicked");
if (isDirty()) {
Object[] options = {SikuliIDEI18N._I("yes"), SikuliIDEI18N._I("no"), SikuliIDEI18N._I("cancel")};
int ans = JOptionPane.showOptionDialog(this,
SikuliIDEI18N._I("msgAskSaveChanges", getCurrentShortFilename()),
SikuliIDEI18N._I("dlgAskCloseTab"),
JOptionPane.YES_NO_CANCEL_OPTION,
JOptionPane.WARNING_MESSAGE,
null,
options, options[0]);
if (ans == JOptionPane.CANCEL_OPTION
|| ans == JOptionPane.CLOSED_OPTION) {
return false;
} else if (ans == JOptionPane.YES_OPTION) {
if (saveFile() == null) {
return false;
}
} else {
// sikuliIDE.getTabPane().resetLastClosed();
}
setDirty(false);
}
if (_srcBundlePath != null) {
ImagePath.remove(_srcBundlePath);
if (_srcBundleTemp) {
FileManager.deleteTempDir(_srcBundlePath);
}
}
return true;
}
private void setSrcBundle(String newBundlePath) {
try {
newBundlePath = new File(newBundlePath).getCanonicalPath();
} catch (Exception ex) {
return;
}
_srcBundlePath = newBundlePath;
ImagePath.setBundlePath(_srcBundlePath);
}
public String getSrcBundle() {
if (_srcBundlePath == null) {
File tmp = FileManager.createTempDir();
setSrcBundle(FileManager.slashify(tmp.getAbsolutePath(), true));
_srcBundleTemp = true;
}
return _srcBundlePath;
}
// used at ButtonRun.run
public String getBundlePath() {
return _srcBundlePath;
}
public boolean isSourceBundleTemp() {
return _srcBundleTemp;
}
public String getCurrentSrcDir() {
if (_srcBundlePath != null) {
if (_editingFile == null || _srcBundleTemp) {
return FileManager.normalize(_srcBundlePath);
} else {
return _editingFile.getParent();
}
} else {
return null;
}
}
public String getCurrentShortFilename() {
if (_srcBundlePath != null) {
File f = new File(_srcBundlePath);
return f.getName();
}
return "Untitled";
}
public File getCurrentFile() {
return getCurrentFile(true);
}
public File getCurrentFile(boolean shouldSave) {
if (shouldSave && _editingFile == null && isDirty()) {
try {
saveAsFile(Settings.isMac());
} catch (IOException e) {
log(-1, "getCurrentFile: Problem while trying to save %s\n%s",
_editingFile.getAbsolutePath(), e.getMessage());
}
}
return _editingFile;
}
public String getCurrentFilename() {
if (_editingFile == null) {
return null;
}
return _editingFile.getAbsolutePath();
}
//TODO convertSrcToHtml has to be completely revised
private void convertSrcToHtml(String bundle) {
IScriptRunner runner = ScriptingSupport.getRunner(null, "jython");
if (runner != null) {
runner.doSomethingSpecial("convertSrcToHtml", new String[]{bundle});
}
}
public File copyFileToBundle(String filename) {
File f = new File(filename);
String bundlePath = getSrcBundle();
if (f.exists()) {
try {
File newFile = FileManager.smartCopy(filename, bundlePath);
return newFile;
} catch (IOException e) {
log(-1, "copyFileToBundle: Problem while trying to save %s\n%s",
filename, e.getMessage());
return f;
}
}
return null;
}
public Image getImageInBundle(String filename) {
return Image.createThumbNail(filename);
}
//<editor-fold defaultstate="collapsed" desc="Dirty handling">
public boolean isDirty() {
return scriptIsDirty;
}
public void setDirty(boolean flag) {
if (scriptIsDirty == flag) {
return;
}
scriptIsDirty = flag;
sikuliIDE.setCurrentFileTabTitleDirty(scriptIsDirty);
}
private class DirtyHandler implements DocumentListener {
@Override
public void changedUpdate(DocumentEvent ev) {
log(lvl + 1, "change update");
//setDirty(true);
}
@Override
public void insertUpdate(DocumentEvent ev) {
log(lvl + 1, "insert update");
setDirty(true);
}
@Override
public void removeUpdate(DocumentEvent ev) {
log(lvl + 1, "remove update");
setDirty(true);
}
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="Caret handling">
//TODO not used
@Override
public void caretUpdate(CaretEvent evt) {
/* seems not to be used
* if (_can_update_caret_last_x) {
* _caret_last_x = -1;
* } else {
* _can_update_caret_last_x = true;
* }
*/
}
public int getLineNumberAtCaret(int caretPosition) {
Element root = getDocument().getDefaultRootElement();
return root.getElementIndex(caretPosition) + 1;
}
public Element getLineAtCaret(int caretPosition) {
Element root = getDocument().getDefaultRootElement();
Element result;
if (caretPosition == -1) {
result = root.getElement(root.getElementIndex(getCaretPosition()));
} else {
result = root.getElement(root.getElementIndex(root.getElementIndex(caretPosition)));
}
return result;
}
public String getLineTextAtCaret() {
Element elem = getLineAtCaret(-1);
Document doc = elem.getDocument();
Element subElem;
String text;
String line = "";
int start = elem.getStartOffset();
int end = elem.getEndOffset();
for (int i = 0; i < elem.getElementCount(); i++) {
text = "";
subElem = elem.getElement(i);
start = subElem.getStartOffset();
end = subElem.getEndOffset();
if (subElem.getName().contains("component")) {
text = StyleConstants.getComponent(subElem.getAttributes()).toString();
} else {
try {
text = doc.getText(start, end - start);
} catch (Exception ex) {
}
}
line += text;
}
return line.trim();
}
public Element getLineAtPoint(MouseEvent me) {
Point p = me.getLocationOnScreen();
Point pp = getLocationOnScreen();
p.translate(-pp.x, -pp.y);
int pos = viewToModel(p);
Element root = getDocument().getDefaultRootElement();
int e = root.getElementIndex(pos);
if (e == -1) {
return null;
}
return root.getElement(e);
}
public boolean jumpTo(int lineNo, int column) {
log(lvl + 1, "jumpTo pos: " + lineNo + "," + column);
try {
int off = getLineStartOffset(lineNo - 1) + column - 1;
int lineCount = getDocument().getDefaultRootElement().getElementCount();
if (lineNo < lineCount) {
int nextLine = getLineStartOffset(lineNo);
if (off >= nextLine) {
off = nextLine - 1;
}
}
if (off >= 0) {
setCaretPosition(off);
}
} catch (BadLocationException ex) {
jumpTo(lineNo);
return false;
}
return true;
}
public boolean jumpTo(int lineNo) {
log(lvl + 1, "jumpTo line: " + lineNo);
try {
setCaretPosition(getLineStartOffset(lineNo - 1));
} catch (BadLocationException ex) {
return false;
}
return true;
}
public int getLineStartOffset(int line) throws BadLocationException {
// line starting from 0
Element map = getDocument().getDefaultRootElement();
if (line < 0) {
throw new BadLocationException("Negative line", -1);
} else if (line >= map.getElementCount()) {
throw new BadLocationException("No such line", getDocument().getLength() + 1);
} else {
Element lineElem = map.getElement(line);
return lineElem.getStartOffset();
}
}
//<editor-fold defaultstate="collapsed" desc="TODO only used for UnitTest">
public void jumpTo(String funcName) throws BadLocationException {
log(lvl + 1, "jumpTo function: " + funcName);
Element root = getDocument().getDefaultRootElement();
int pos = getFunctionStartOffset(funcName, root);
if (pos >= 0) {
setCaretPosition(pos);
} else {
throw new BadLocationException("Can't find function " + funcName, -1);
}
}
private int getFunctionStartOffset(String func, Element node) throws BadLocationException {
Document doc = getDocument();
int count = node.getElementCount();
Pattern patDef = Pattern.compile("def\\s+" + func + "\\s*\\(");
for (int i = 0; i < count; i++) {
Element elm = node.getElement(i);
if (elm.isLeaf()) {
int start = elm.getStartOffset(), end = elm.getEndOffset();
String line = doc.getText(start, end - start);
Matcher matcher = patDef.matcher(line);
if (matcher.find()) {
return start;
}
} else {
int p = getFunctionStartOffset(func, elm);
if (p >= 0) {
return p;
}
}
}
return -1;
}
//</editor-fold>
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="replace text patterns with image buttons">
public boolean reparse(String oldName, String newName, boolean fileOverWritten) {
boolean success;
if (fileOverWritten) {
Image.unCacheBundledImage(newName);
}
Map<String, List<Integer>> images = parseforImages();
oldName = new File(oldName).getName();
List<Integer> poss = images.get(oldName);
if (images.containsKey(oldName) && poss.size() > 0) {
Collections.sort(poss, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
if (o1 > o2) return -1;
return 1;
}
});
reparseRenameImages(poss, oldName, new File(newName).getName());
}
return reparse();
}
private boolean reparseRenameImages(List<Integer> poss, String oldName, String newName) {
StringBuilder text = new StringBuilder(getText());
int lenOld = oldName.length();
for (int pos : poss) {
text.replace(pos - lenOld, pos, newName);
}
setText(text.toString());
return true;
}
public boolean reparse() {
String paneContent = this.getText();
if (paneContent.length() < 7) {
return true;
}
if (readScript(paneContent)) {
updateDocumentListeners("reparse");
return true;
}
return false;
}
//TODO not clear what this is for LOL (SikuliIDEPopUpMenu.doSetType)
public boolean reparseCheckContent() {
if (this.getText().contains(ScriptingSupport.TypeCommentToken)) {
return true;
}
return false;
}
private void parse(Element node) {
if (!showThumbs) {
// do not show any thumbnails
return;
}
int count = node.getElementCount();
for (int i = 0; i < count; i++) {
Element elm = node.getElement(i);
log(lvl + 1, elm.toString());
if (elm.isLeaf()) {
int start = elm.getStartOffset(), end = elm.getEndOffset();
parseRange(start, end);
} else {
parse(elm);
}
}
}
public String parseLineText(String line) {
if (line.startsWith("#")) {
Pattern aName = Pattern.compile("^#[A-Za-z0-9_]+ =$");
Matcher mN = aName.matcher(line);
if (mN.find()) {
return line.substring(1).split(" ")[0];
}
return "";
}
Matcher mR = patRegionStr.matcher(line);
String asOffset = ".asOffset()";
if (mR.find()) {
if (line.length() >= mR.end() + asOffset.length()) {
if (line.substring(mR.end()).contains(asOffset)) {
return line.substring(mR.start(), mR.end()) + asOffset;
}
}
return line.substring(mR.start(), mR.end());
}
Matcher mL = patLocationStr.matcher(line);
if (mL.find()) {
return line.substring(mL.start(), mL.end());
}
Matcher mP = patPatternStr.matcher(line);
if (mP.find()) {
return line.substring(mP.start(), mP.end());
}
Matcher mI = patPngStr.matcher(line);
if (mI.find()) {
return line.substring(mI.start(), mI.end());
}
return "";
}
private int parseRange(int start, int end) {
if (!showThumbs) {
// do not show any thumbnails
return end;
}
try {
end = parseLine(start, end, patCaptureBtn);
end = parseLine(start, end, patPatternStr);
end = parseLine(start, end, patRegionStr);
end = parseLine(start, end, patPngStr);
} catch (BadLocationException e) {
log(-1, "parseRange: Problem while trying to parse line\n%s", e.getMessage());
}
return end;
}
private int parseLine(int startOff, int endOff, Pattern ptn) throws BadLocationException {
if (endOff <= startOff) {
return endOff;
}
Document doc = getDocument();
while (true) {
String line = doc.getText(startOff, endOff - startOff);
Matcher m = ptn.matcher(line);
//System.out.println("["+line+"]");
if (m.find()) {
int len = m.end() - m.start();
boolean replaced = replaceWithImage(startOff + m.start(), startOff + m.end(), ptn);
if (replaced) {
startOff += m.start() + 1;
endOff -= len - 1;
} else {
startOff += m.end() + 1;
}
} else {
break;
}
}
return endOff;
}
private boolean replaceWithImage(int startOff, int endOff, Pattern ptn)
throws BadLocationException {
Document doc = getDocument();
String imgStr = doc.getText(startOff, endOff - startOff);
JComponent comp = null;
if (ptn == patPatternStr || ptn == patPngStr) {
if (pref.getPrefMoreImageThumbs()) {
comp = EditorPatternButton.createFromString(this, imgStr, null);
} else {
comp = EditorPatternLabel.labelFromString(this, imgStr);
}
} else if (ptn == patRegionStr) {
if (pref.getPrefMoreImageThumbs()) {
comp = EditorRegionButton.createFromString(this, imgStr);
} else {
comp = EditorRegionLabel.labelFromString(this, imgStr);
}
} else if (ptn == patCaptureBtn) {
comp = EditorPatternLabel.labelFromString(this, "");
}
if (comp != null) {
this.select(startOff, endOff);
this.insertComponent(comp);
return true;
}
return false;
}
public String getRegionString(int x, int y, int w, int h) {
return String.format("Region(%d,%d,%d,%d)", x, y, w, h);
}
public String getPatternString(String ifn, float sim, Location off, Image img) {
//TODO ifn really needed??
if (ifn == null) {
return "\"" + EditorPatternLabel.CAPTURE + "\"";
}
String imgName = new File(ifn).getName();
if (img != null) {
imgName = img.getName();
}
String pat = "Pattern(\"" + imgName + "\")";
String ret = "";
if (sim > 0) {
if (sim >= 0.99F) {
ret += ".exact()";
} else if (sim != 0.7F) {
ret += String.format(Locale.ENGLISH, ".similar(%.2f)", sim);
}
}
if (off != null && (off.x != 0 || off.y != 0)) {
ret += ".targetOffset(" + off.x + "," + off.y + ")";
}
if (!ret.equals("")) {
ret = pat + ret;
} else {
ret = "\"" + imgName + "\"";
}
return ret;
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="content insert append">
public void insertString(String str) {
int sel_start = getSelectionStart();
int sel_end = getSelectionEnd();
if (sel_end != sel_start) {
try {
getDocument().remove(sel_start, sel_end - sel_start);
} catch (BadLocationException e) {
log(-1, "insertString: Problem while trying to insert\n%s", e.getMessage());
}
}
int pos = getCaretPosition();
insertString(pos, str);
int new_pos = getCaretPosition();
int end = parseRange(pos, new_pos);
setCaretPosition(end);
}
private void insertString(int pos, String str) {
Document doc = getDocument();
try {
doc.insertString(pos, str, null);
} catch (Exception e) {
log(-1, "insertString: Problem while trying to insert at pos\n%s", e.getMessage());
}
}
//TODO not used
public void appendString(String str) {
Document doc = getDocument();
try {
int start = doc.getLength();
doc.insertString(doc.getLength(), str, null);
int end = doc.getLength();
//end = parseLine(start, end, patHistoryBtnStr);
} catch (Exception e) {
log(-1, "appendString: Problem while trying to append\n%s", e.getMessage());
}
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="feature search">
/*
* public int search(Pattern pattern){
* return search(pattern, true);
* }
*
* public int search(Pattern pattern, boolean forward){
* if(!pattern.equals(_lastSearchPattern)){
* _lastSearchPattern = pattern;
* Document doc = getDocument();
* int pos = getCaretPosition();
* Debug.log("caret: " + pos);
* try{
* String body = doc.getText(pos, doc.getLength()-pos);
* _lastSearchMatcher = pattern.matcher(body);
* }
* catch(BadLocationException e){
* e.printStackTrace();
* }
* }
* return continueSearch(forward);
* }
*/
/*
* public int search(String str){
* return search(str, true);
* }
*/
public int search(String str, int pos, boolean forward) {
boolean isCaseSensitive = true;
String toSearch = str;
if (str.startsWith("!")) {
str = str.substring(1).toUpperCase();
isCaseSensitive = false;
}
int ret = -1;
Document doc = getDocument();
Debug.log(lvl, "search: %s from %d forward: %s", str, pos, forward);
try {
String body;
int begin;
if (forward) {
int len = doc.getLength() - pos;
body = doc.getText(pos, len > 0 ? len : 0);
begin = pos;
} else {
body = doc.getText(0, pos);
begin = 0;
}
if (!isCaseSensitive) {
body = body.toUpperCase();
}
Pattern pattern = Pattern.compile(Pattern.quote(str));
Matcher matcher = pattern.matcher(body);
ret = continueSearch(matcher, begin, forward);
if (ret < 0) {
if (forward && pos != 0) {
return search(toSearch, 0, forward);
}
if (!forward && pos != doc.getLength()) {
return search(toSearch, doc.getLength(), forward);
}
}
} catch (BadLocationException e) {
log(-1, "search: did not work:\n" + e.getStackTrace());
}
return ret;
}
protected int continueSearch(Matcher matcher, int pos, boolean forward) {
boolean hasNext = false;
int start = 0, end = 0;
if (!forward) {
while (matcher.find()) {
hasNext = true;
start = matcher.start();
end = matcher.end();
}
} else {
hasNext = matcher.find();
if (!hasNext) {
return -1;
}
start = matcher.start();
end = matcher.end();
}
if (hasNext) {
Document doc = getDocument();
getCaret().setDot(pos + end);
getCaret().moveDot(pos + start);
getCaret().setSelectionVisible(true);
return pos + start;
}
return -1;
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="Transfer code incl. images between code panes">
private class MyTransferHandler extends TransferHandler {
private static final String me = "EditorPaneTransferHandler: ";
Map<String, String> _copiedImgs = new HashMap<String, String>();
@Override
public void exportToClipboard(JComponent comp, Clipboard clip, int action) {
super.exportToClipboard(comp, clip, action);
}
@Override
protected void exportDone(JComponent source,
Transferable data,
int action) {
if (action == TransferHandler.MOVE) {
JTextPane aTextPane = (JTextPane) source;
int sel_start = aTextPane.getSelectionStart();
int sel_end = aTextPane.getSelectionEnd();
Document doc = aTextPane.getDocument();
try {
doc.remove(sel_start, sel_end - sel_start);
} catch (BadLocationException e) {
log(-1, "MyTransferHandler: exportDone: Problem while trying to remove text\n%s", e.getMessage());
}
}
}
@Override
public int getSourceActions(JComponent c) {
return COPY_OR_MOVE;
}
@Override
protected Transferable createTransferable(JComponent c) {
JTextPane aTextPane = (JTextPane) c;
SikuliEditorKit kit = ((SikuliEditorKit) aTextPane.getEditorKit());
Document doc = aTextPane.getDocument();
int sel_start = aTextPane.getSelectionStart();
int sel_end = aTextPane.getSelectionEnd();
StringWriter writer = new StringWriter();
try {
_copiedImgs.clear();
kit.write(writer, doc, sel_start, sel_end - sel_start, _copiedImgs);
return new StringSelection(writer.toString());
} catch (Exception e) {
log(-1, "MyTransferHandler: createTransferable: Problem creating text to copy\n%s", e.getMessage());
}
return null;
}
@Override
public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) {
for (int i = 0; i < transferFlavors.length; i++) {
//System.out.println(transferFlavors[i]);
if (transferFlavors[i].equals(DataFlavor.stringFlavor)) {
return true;
}
}
return false;
}
@Override
public boolean importData(JComponent comp, Transferable t) {
DataFlavor htmlFlavor = DataFlavor.stringFlavor;
if (canImport(comp, t.getTransferDataFlavors())) {
try {
String transferString = (String) t.getTransferData(htmlFlavor);
EditorPane targetTextPane = (EditorPane) comp;
for (Map.Entry<String, String> entry : _copiedImgs.entrySet()) {
String imgName = entry.getKey();
String imgPath = entry.getValue();
File destFile = targetTextPane.copyFileToBundle(imgPath);
String newName = destFile.getName();
if (!newName.equals(imgName)) {
String ptnImgName = "\"" + imgName + "\"";
newName = "\"" + newName + "\"";
transferString = transferString.replaceAll(ptnImgName, newName);
Debug.info("MyTransferHandler: importData:" + ptnImgName + " exists. Rename it to " + newName);
}
}
targetTextPane.insertString(transferString);
} catch (Exception e) {
log(-1, "MyTransferHandler: importData: Problem pasting text\n%s", e.getMessage());
}
return true;
}
return false;
}
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="currently not used">
private String _tabString = " ";
private void setTabSize(int charactersPerTab) {
FontMetrics fm = this.getFontMetrics(this.getFont());
int charWidth = fm.charWidth('w');
int tabWidth = charWidth * charactersPerTab;
TabStop[] tabs = new TabStop[10];
for (int j = 0; j < tabs.length; j++) {
int tab = j + 1;
tabs[j] = new TabStop(tab * tabWidth);
}
TabSet tabSet = new TabSet(tabs);
SimpleAttributeSet attributes = new SimpleAttributeSet();
StyleConstants.setFontSize(attributes, 18);
StyleConstants.setFontFamily(attributes, "Osaka-Mono");
StyleConstants.setTabSet(attributes, tabSet);
int length = getDocument().getLength();
getStyledDocument().setParagraphAttributes(0, length, attributes, true);
}
private void setTabs(int spaceForTab) {
String t = "";
for (int i = 0; i < spaceForTab; i++) {
t += " ";
}
_tabString = t;
}
private void expandTab() throws BadLocationException {
int pos = getCaretPosition();
Document doc = getDocument();
doc.remove(pos - 1, 1);
doc.insertString(pos - 1, _tabString, null);
}
private Class _historyBtnClass;
private void setHistoryCaptureButton(ButtonCapture btn) {
_historyBtnClass = btn.getClass();
}
private void indent(int startLine, int endLine, int level) {
Document doc = getDocument();
String strIndent = "";
if (level > 0) {
for (int i = 0; i < level; i++) {
strIndent += " ";
}
} else {
Debug.error("negative indentation not supported yet!!");
}
for (int i = startLine; i < endLine; i++) {
try {
int off = getLineStartOffset(i);
if (level > 0) {
doc.insertString(off, strIndent, null);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
private void checkCompletion(java.awt.event.KeyEvent ke) throws BadLocationException {
Document doc = getDocument();
Element root = doc.getDefaultRootElement();
int pos = getCaretPosition();
int lineIdx = root.getElementIndex(pos);
Element line = root.getElement(lineIdx);
int start = line.getStartOffset(), len = line.getEndOffset() - start;
String strLine = doc.getText(start, len - 1);
Debug.log(9, "[" + strLine + "]");
if (strLine.endsWith("find") && ke.getKeyChar() == '(') {
ke.consume();
doc.insertString(pos, "(", null);
ButtonCapture btnCapture = new ButtonCapture(this, line);
insertComponent(btnCapture);
doc.insertString(pos + 2, ")", null);
}
}
//</editor-fold>
}