• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.googlecode.android_scripting.activity;
18 
19 import android.app.Activity;
20 import android.app.AlertDialog;
21 import android.app.Dialog;
22 import android.content.DialogInterface;
23 import android.content.DialogInterface.OnClickListener;
24 import android.content.Intent;
25 import android.content.SharedPreferences;
26 import android.content.SharedPreferences.Editor;
27 import android.media.AudioManager;
28 import android.os.Bundle;
29 import android.preference.PreferenceManager;
30 import android.text.Editable;
31 import android.text.InputFilter;
32 import android.text.InputType;
33 import android.text.Selection;
34 import android.text.Spanned;
35 import android.text.TextWatcher;
36 import android.text.style.UnderlineSpan;
37 import android.view.KeyEvent;
38 import android.view.Menu;
39 import android.view.MenuItem;
40 import android.view.View;
41 import android.widget.CheckBox;
42 import android.widget.EditText;
43 import android.widget.Toast;
44 
45 import com.googlecode.android_scripting.BaseApplication;
46 import com.googlecode.android_scripting.Constants;
47 import com.googlecode.android_scripting.FileUtils;
48 import com.googlecode.android_scripting.Log;
49 import com.googlecode.android_scripting.R;
50 import com.googlecode.android_scripting.ScriptStorageAdapter;
51 import com.googlecode.android_scripting.interpreter.Interpreter;
52 import com.googlecode.android_scripting.interpreter.InterpreterConfiguration;
53 
54 import java.io.File;
55 import java.io.IOException;
56 import java.util.ArrayList;
57 import java.util.Arrays;
58 import java.util.List;
59 import java.util.Vector;
60 import java.util.regex.Matcher;
61 import java.util.regex.Pattern;
62 
63 /**
64  * A text editor for scripts.
65  *
66  */
67 public class ScriptEditor extends Activity implements OnClickListener {
68   private static final int DIALOG_FIND_REPLACE = 2;
69   private static final int DIALOG_LINE = 1;
70   private EditText mNameText;
71   private EditText mContentText;
72   private boolean mScheduleMoveLeft;
73   private String mLastSavedContent;
74   private SharedPreferences mPreferences;
75   private InterpreterConfiguration mConfiguration;
76   private ContentTextWatcher mWatcher;
77   private EditHistory mHistory;
78   private File mScript;
79   private EditText mLineNo;
80 
81   private boolean mIsUndoOrRedo = false;
82   private boolean mEnableAutoClose;
83   private boolean mAutoIndent;
84 
85   private EditText mSearchFind;
86   private EditText mSearchReplace;
87   private CheckBox mSearchCase;
88   private CheckBox mSearchWord;
89   private CheckBox mSearchAll;
90   private CheckBox mSearchStart;
91 
92   private static enum MenuId {
93     SAVE, SAVE_AND_RUN, PREFERENCES, API_BROWSER, HELP, SHARE, GOTO, SEARCH;
getId()94     public int getId() {
95       return ordinal() + Menu.FIRST;
96     }
97   }
98 
99   private static enum RequestCode {
100     RPC_HELP
101   }
102 
readIntPref(String key, int defaultValue, int maxValue)103   private int readIntPref(String key, int defaultValue, int maxValue) {
104     int val;
105     try {
106       val = Integer.parseInt(mPreferences.getString(key, Integer.toString(defaultValue)));
107     } catch (NumberFormatException e) {
108       val = defaultValue;
109     }
110     val = Math.max(0, Math.min(val, maxValue));
111     return val;
112   }
113 
114   @Override
onCreate(Bundle savedInstanceState)115   protected void onCreate(Bundle savedInstanceState) {
116     super.onCreate(savedInstanceState);
117     setContentView(R.layout.script_editor);
118     mNameText = (EditText) findViewById(R.id.script_editor_title);
119     mContentText = (EditText) findViewById(R.id.script_editor_body);
120     mHistory = new EditHistory();
121     mWatcher = new ContentTextWatcher(mHistory);
122     mPreferences = PreferenceManager.getDefaultSharedPreferences(this);
123     updatePreferences();
124 
125     mScript = new File(getIntent().getStringExtra(Constants.EXTRA_SCRIPT_PATH));
126     mNameText.setText(mScript.getName());
127     mNameText.setSelected(true);
128     // NOTE: This appears to be the only way to get Android to put the cursor to the beginning of
129     // the EditText field.
130     mNameText.setSelection(1);
131     mNameText.extendSelection(0);
132     mNameText.setSelection(0);
133     mLastSavedContent = getIntent().getStringExtra(Constants.EXTRA_SCRIPT_CONTENT);
134     if (mLastSavedContent == null) {
135       try {
136         mLastSavedContent = FileUtils.readToString(mScript);
137       } catch (IOException e) {
138         Log.e("Failed to read script.", e);
139         mLastSavedContent = "";
140       } finally {
141       }
142     }
143 
144     mContentText.setText(mLastSavedContent);
145     InputFilter[] oldFilters = mContentText.getFilters();
146     List<InputFilter> filters = new ArrayList<InputFilter>(oldFilters.length + 1);
147     filters.addAll(Arrays.asList(oldFilters));
148     filters.add(new ContentInputFilter());
149     mContentText.setFilters(filters.toArray(oldFilters));
150     mContentText.addTextChangedListener(mWatcher);
151     mConfiguration = ((BaseApplication) getApplication()).getInterpreterConfiguration();
152     // Disables volume key beep.
153     setVolumeControlStream(AudioManager.STREAM_MUSIC);
154     mLineNo = new EditText(this);
155     mLineNo.setInputType(InputType.TYPE_CLASS_NUMBER);
156     int lastLocation = mPreferences.getInt("lasteditpos." + mScript, -1);
157     if (lastLocation >= 0) {
158       mContentText.requestFocus();
159       mContentText.setSelection(lastLocation);
160     }
161   }
162 
163   @Override
onResume()164   protected void onResume() {
165     super.onResume();
166     updatePreferences();
167   }
168 
updatePreferences()169   private void updatePreferences() {
170     mContentText.setTextSize(readIntPref("editor_fontsize", 10, 30));
171     mEnableAutoClose = mPreferences.getBoolean("enableAutoClose", true);
172     mAutoIndent = mPreferences.getBoolean("editor_auto_indent", false);
173     mContentText.setHorizontallyScrolling(mPreferences.getBoolean("editor_no_wrap", false));
174   }
175 
176   @Override
onCreateOptionsMenu(Menu menu)177   public boolean onCreateOptionsMenu(Menu menu) {
178     super.onCreateOptionsMenu(menu);
179     menu.add(0, MenuId.SAVE.getId(), 0, "Save & Exit").setIcon(android.R.drawable.ic_menu_save);
180     menu.add(0, MenuId.SAVE_AND_RUN.getId(), 0, "Save & Run").setIcon(
181         android.R.drawable.ic_media_play);
182     menu.add(0, MenuId.PREFERENCES.getId(), 0, "Preferences").setIcon(
183         android.R.drawable.ic_menu_preferences);
184     menu.add(0, MenuId.API_BROWSER.getId(), 0, "API Browser").setIcon(
185         android.R.drawable.ic_menu_info_details);
186     menu.add(0, MenuId.SHARE.getId(), 0, "Share").setIcon(android.R.drawable.ic_menu_share);
187     menu.add(0, MenuId.GOTO.getId(), 0, "GoTo").setIcon(android.R.drawable.ic_menu_directions);
188     menu.add(0, MenuId.SEARCH.getId(), 0, "Find").setIcon(android.R.drawable.ic_menu_search);
189     return true;
190   }
191 
192   @Override
onOptionsItemSelected(MenuItem item)193   public boolean onOptionsItemSelected(MenuItem item) {
194     if (item.getItemId() == MenuId.SAVE.getId()) {
195       save();
196       finish();
197     } else if (item.getItemId() == MenuId.SAVE_AND_RUN.getId()) {
198       save();
199       Interpreter interpreter =
200           mConfiguration.getInterpreterForScript(mNameText.getText().toString());
201       if (interpreter != null) { // We may be editing an unknown type.
202         Intent intent = new Intent(this, ScriptingLayerService.class);
203         intent.setAction(Constants.ACTION_LAUNCH_FOREGROUND_SCRIPT);
204         intent.putExtra(Constants.EXTRA_SCRIPT_PATH, mScript.getAbsolutePath());
205         startService(intent);
206       } else {
207         // TODO(damonkohler): Should remove menu option.
208         Toast.makeText(this, "Can't run this type.", Toast.LENGTH_SHORT).show();
209       }
210       finish();
211     } else if (item.getItemId() == MenuId.PREFERENCES.getId()) {
212       startActivity(new Intent(this, Preferences.class));
213     } else if (item.getItemId() == MenuId.API_BROWSER.getId()) {
214       Intent intent = new Intent(this, ApiBrowser.class);
215       intent.putExtra(Constants.EXTRA_SCRIPT_PATH, mNameText.getText().toString());
216       intent.putExtra(Constants.EXTRA_INTERPRETER_NAME,
217           mConfiguration.getInterpreterForScript(mNameText.getText().toString()).getName());
218       intent.putExtra(Constants.EXTRA_SCRIPT_TEXT, mContentText.getText().toString());
219       startActivityForResult(intent, RequestCode.RPC_HELP.ordinal());
220     } else if (item.getItemId() == MenuId.SHARE.getId()) {
221       Intent intent = new Intent(Intent.ACTION_SEND);
222       intent.putExtra(Intent.EXTRA_TEXT, mContentText.getText().toString());
223       intent.putExtra(Intent.EXTRA_SUBJECT, "Share " + mNameText.getText().toString());
224       intent.setType("text/plain");
225       startActivity(Intent.createChooser(intent, "Send Script to:"));
226     } else if (item.getItemId() == MenuId.GOTO.getId()) {
227       showDialog(DIALOG_LINE);
228     } else if (item.getItemId() == MenuId.SEARCH.getId()) {
229       showDialog(DIALOG_FIND_REPLACE);
230     }
231     return super.onOptionsItemSelected(item);
232   }
233 
234   @Override
onActivityResult(int requestCode, int resultCode, Intent data)235   protected void onActivityResult(int requestCode, int resultCode, Intent data) {
236     super.onActivityResult(requestCode, resultCode, data);
237     RequestCode request = RequestCode.values()[requestCode];
238 
239     if (resultCode == RESULT_OK) {
240       switch (request) {
241       case RPC_HELP:
242         String rpcText = data.getStringExtra(Constants.EXTRA_RPC_HELP_TEXT);
243         insertContent(rpcText);
244         break;
245       default:
246         break;
247       }
248     } else {
249       switch (request) {
250       case RPC_HELP:
251         break;
252       default:
253         break;
254       }
255     }
256   }
257 
save()258   private void save() {
259     int start = mContentText.getSelectionStart();
260     mLastSavedContent = mContentText.getText().toString();
261     mScript = new File(mScript.getParent(), mNameText.getText().toString());
262     ScriptStorageAdapter.writeScript(mScript, mLastSavedContent);
263     Toast.makeText(this, "Saved " + mNameText.getText().toString(), Toast.LENGTH_SHORT).show();
264     Editor e = mPreferences.edit();
265     e.putInt("lasteditpos." + mScript, start);
266     e.commit();
267   }
268 
insertContent(String text)269   private void insertContent(String text) {
270     int selectionStart = Math.min(mContentText.getSelectionStart(), mContentText.getSelectionEnd());
271     int selectionEnd = Math.max(mContentText.getSelectionStart(), mContentText.getSelectionEnd());
272     mContentText.getEditableText().replace(selectionStart, selectionEnd, text);
273   }
274 
275   @Override
onKeyDown(int keyCode, KeyEvent event)276   public boolean onKeyDown(int keyCode, KeyEvent event) {
277     if (keyCode == KeyEvent.KEYCODE_BACK && hasContentChanged()) {
278       AlertDialog.Builder alert = new AlertDialog.Builder(this);
279       setVolumeControlStream(AudioManager.STREAM_MUSIC);
280       alert.setCancelable(false);
281       alert.setTitle("Confirm exit");
282       alert.setMessage("Would you like to save?");
283       alert.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
284         public void onClick(DialogInterface dialog, int whichButton) {
285           save();
286           finish();
287         }
288       });
289       alert.setNegativeButton("No", new DialogInterface.OnClickListener() {
290         public void onClick(DialogInterface dialog, int whichButton) {
291           finish();
292         }
293       });
294       alert.setNeutralButton("Cancel", new DialogInterface.OnClickListener() {
295         public void onClick(DialogInterface dialog, int whichButton) {
296         }
297       });
298       alert.show();
299       return true;
300     } else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
301       redo();
302       return true;
303     } else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
304       undo();
305       return true;
306     } else if (keyCode == KeyEvent.KEYCODE_SEARCH) {
307       showDialog(DIALOG_FIND_REPLACE);
308       return true;
309     } else {
310       return super.onKeyDown(keyCode, event);
311     }
312   }
313 
314   @Override
onCreateDialog(int id, Bundle args)315   protected Dialog onCreateDialog(int id, Bundle args) {
316     AlertDialog.Builder b = new AlertDialog.Builder(this);
317     if (id == DIALOG_LINE) {
318       b.setTitle("Goto Line");
319       b.setView(mLineNo);
320       b.setPositiveButton("Ok", new OnClickListener() {
321 
322         @Override
323         public void onClick(DialogInterface dialog, int which) {
324           gotoLine(Integer.parseInt(mLineNo.getText().toString()));
325         }
326       });
327       b.setNegativeButton("Cancel", null);
328       return b.create();
329     } else if (id == DIALOG_FIND_REPLACE) {
330       View v = getLayoutInflater().inflate(R.layout.findreplace, null);
331       mSearchFind = (EditText) v.findViewById(R.id.searchFind);
332       mSearchReplace = (EditText) v.findViewById(R.id.searchReplace);
333       mSearchAll = (CheckBox) v.findViewById(R.id.searchAll);
334       mSearchCase = (CheckBox) v.findViewById(R.id.searchCase);
335       mSearchStart = (CheckBox) v.findViewById(R.id.searchStart);
336       mSearchWord = (CheckBox) v.findViewById(R.id.searchWord);
337       b.setTitle("Search and Replace");
338       b.setView(v);
339       b.setPositiveButton("Find", this);
340       b.setNeutralButton("Next", this);
341       b.setNegativeButton("Replace", this);
342       return b.create();
343     }
344 
345     return super.onCreateDialog(id, args);
346   }
347 
348   @Override
onPrepareDialog(int id, Dialog dialog, Bundle args)349   protected void onPrepareDialog(int id, Dialog dialog, Bundle args) {
350     if (id == DIALOG_LINE) {
351       mLineNo.setText(String.valueOf(getLineNo()));
352     } else if (id == DIALOG_FIND_REPLACE) {
353       mSearchStart.setChecked(false);
354     }
355     super.onPrepareDialog(id, dialog, args);
356   }
357 
getLineNo()358   protected int getLineNo() {
359     int pos = mContentText.getSelectionStart();
360     String text = mContentText.getText().toString();
361     int i = 0;
362     int n = 1;
363     while (i < pos) {
364       int j = text.indexOf("\n", i);
365       if (j < 0) {
366         break;
367       }
368       i = j + 1;
369       if (i < pos) {
370         n += 1;
371       }
372     }
373     return n;
374   }
375 
gotoLine(int line)376   protected void gotoLine(int line) {
377     String text = mContentText.getText().toString();
378     if (text.length() < 1) {
379       return;
380     }
381     int i = 0;
382     int n = 1;
383     while (i < text.length() && n < line) {
384       int j = text.indexOf("\n", i);
385       if (j < 0) {
386         break;
387       }
388       i = j + 1;
389       n += 1;
390     }
391     mContentText.setSelection(Math.min(text.length() - 1, i));
392   }
393 
394   @Override
onUserLeaveHint()395   protected void onUserLeaveHint() {
396     if (hasContentChanged()) {
397       save();
398     }
399   }
400 
hasContentChanged()401   private boolean hasContentChanged() {
402     return !mLastSavedContent.equals(mContentText.getText().toString());
403   }
404 
405   private final class ContentInputFilter implements InputFilter {
406     @Override
filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend)407     public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart,
408         int dend) {
409       if (end - start == 1) {
410         Interpreter ip = mConfiguration.getInterpreterForScript(mNameText.getText().toString());
411         String auto = null;
412         if (ip != null && mEnableAutoClose) {
413           auto = ip.getLanguage().autoClose(source.charAt(start));
414         }
415         // Auto indent code?
416         if (auto == null && source.charAt(start) == '\n' && mAutoIndent) {
417           int i = dstart - 1;
418           int spaces = 0;
419           while ((i >= 0) && dest.charAt(i) != '\n') {
420             i -= 1; // Find start of line.
421           }
422           i += 1;
423           while (i < dest.length() && dest.charAt(i++) == ' ') {
424             spaces += 1;
425           }
426           if (spaces > 0) {
427             return String.format("\n%" + spaces + "s", " ");
428           }
429         }
430         if (auto != null) {
431           mScheduleMoveLeft = true;
432           return auto;
433         }
434       }
435       return null;
436     }
437   }
438 
439   private final class ContentTextWatcher implements TextWatcher {
440     private final EditHistory mmEditHistory;
441     private CharSequence mmBeforeChange;
442     private CharSequence mmAfterChange;
443 
ContentTextWatcher(EditHistory history)444     private ContentTextWatcher(EditHistory history) {
445       mmEditHistory = history;
446     }
447 
448     @Override
onTextChanged(CharSequence s, int start, int before, int count)449     public void onTextChanged(CharSequence s, int start, int before, int count) {
450       if (!mIsUndoOrRedo) {
451         mmAfterChange = s.subSequence(start, start + count);
452         mmEditHistory.add(new EditItem(start, mmBeforeChange, mmAfterChange));
453       }
454     }
455 
456     @Override
beforeTextChanged(CharSequence s, int start, int count, int after)457     public void beforeTextChanged(CharSequence s, int start, int count, int after) {
458       if (!mIsUndoOrRedo) {
459         mmBeforeChange = s.subSequence(start, start + count);
460       }
461     }
462 
463     @Override
afterTextChanged(Editable s)464     public void afterTextChanged(Editable s) {
465       if (mScheduleMoveLeft) {
466         mScheduleMoveLeft = false;
467         Selection.moveLeft(mContentText.getText(), mContentText.getLayout());
468       }
469     }
470   }
471 
472   /**
473    * Keeps track of all the edit history of a text.
474    */
475   private final class EditHistory {
476     int mmPosition = 0;
477     private final Vector<EditItem> mmHistory = new Vector<EditItem>();
478 
479     /**
480      * Adds a new edit operation to the history at the current position. If executed after a call to
481      * getPrevious() removes all the future history (elements with positions >= current history
482      * position).
483      *
484      */
add(EditItem item)485     private void add(EditItem item) {
486       mmHistory.setSize(mmPosition);
487       mmHistory.add(item);
488       mmPosition++;
489     }
490 
491     /**
492      * Traverses the history backward by one position, returns and item at that position.
493      */
getPrevious()494     private EditItem getPrevious() {
495       if (mmPosition == 0) {
496         return null;
497       }
498       mmPosition--;
499       return mmHistory.get(mmPosition);
500     }
501 
502     /**
503      * Traverses the history forward by one position, returns and item at that position.
504      */
getNext()505     private EditItem getNext() {
506       if (mmPosition == mmHistory.size()) {
507         return null;
508       }
509       EditItem item = mmHistory.get(mmPosition);
510       mmPosition++;
511       return item;
512     }
513   }
514 
515   /**
516    * Represents a single edit operation.
517    */
518   private final class EditItem {
519     private final int mmIndex;
520     private final CharSequence mmBefore;
521     private final CharSequence mmAfter;
522 
523     /**
524      * Constructs EditItem of a modification that was applied at position start and replaced
525      * CharSequence before with CharSequence after.
526      */
EditItem(int start, CharSequence before, CharSequence after)527     public EditItem(int start, CharSequence before, CharSequence after) {
528       mmIndex = start;
529       mmBefore = before;
530       mmAfter = after;
531     }
532   }
533 
undo()534   private void undo() {
535     EditItem edit = mHistory.getPrevious();
536     if (edit == null) {
537       return;
538     }
539     Editable text = mContentText.getText();
540     int start = edit.mmIndex;
541     int end = start + (edit.mmAfter != null ? edit.mmAfter.length() : 0);
542     mIsUndoOrRedo = true;
543     text.replace(start, end, edit.mmBefore);
544     mIsUndoOrRedo = false;
545     // This will get rid of underlines inserted when editor tries to come up with a suggestion.
546     for (Object o : text.getSpans(0, text.length(), UnderlineSpan.class)) {
547       text.removeSpan(o);
548     }
549     Selection.setSelection(text, edit.mmBefore == null ? start : (start + edit.mmBefore.length()));
550   }
551 
redo()552   private void redo() {
553     EditItem edit = mHistory.getNext();
554     if (edit == null) {
555       return;
556     }
557     Editable text = mContentText.getText();
558     int start = edit.mmIndex;
559     int end = start + (edit.mmBefore != null ? edit.mmBefore.length() : 0);
560     mIsUndoOrRedo = true;
561     text.replace(start, end, edit.mmAfter);
562     mIsUndoOrRedo = false;
563     for (Object o : text.getSpans(0, text.length(), UnderlineSpan.class)) {
564       text.removeSpan(o);
565     }
566     Selection.setSelection(text, edit.mmAfter == null ? start : (start + edit.mmAfter.length()));
567   }
568 
569   @Override
onClick(DialogInterface dialog, int which)570   public void onClick(DialogInterface dialog, int which) {
571     int start = mContentText.getSelectionStart();
572     int end = mContentText.getSelectionEnd();
573     String original = mContentText.getText().toString();
574     if (start == end || which != AlertDialog.BUTTON_NEGATIVE) {
575       end = original.length();
576     }
577     if (which == AlertDialog.BUTTON_NEUTRAL) {
578       start += 1;
579     }
580     if (mSearchStart.isChecked()) {
581       start = 0;
582       end = original.length();
583     }
584     String findText = mSearchFind.getText().toString();
585     String replaceText = mSearchReplace.getText().toString();
586     String search = Pattern.quote(findText);
587     int flags = 0;
588     if (!mSearchCase.isChecked()) {
589       flags |= Pattern.CASE_INSENSITIVE;
590     }
591     if (mSearchWord.isChecked()) {
592       search = "\\b" + search + "\\b";
593     }
594     Pattern p = Pattern.compile(search, flags);
595     Matcher m = p.matcher(original);
596     m.region(start, end);
597     if (!m.find()) {
598       Toast.makeText(this, "Search not found.", Toast.LENGTH_SHORT).show();
599       return;
600     }
601     int foundpos = m.start();
602     if (which != AlertDialog.BUTTON_NEGATIVE) { // Find
603       mContentText.setSelection(foundpos, foundpos + findText.length());
604     } else { // Replace
605       String s;
606       // Seems to be a bug in the android 2.2 implementation of replace... regions not returning
607       // whole string.
608       m = p.matcher(original.substring(start, end));
609       String replace = Matcher.quoteReplacement(replaceText);
610       if (mSearchAll.isChecked()) {
611         s = m.replaceAll(replace);
612       } else {
613         s = m.replaceFirst(replace);
614       }
615       mContentText.setText(original.substring(0, start) + s + original.substring(end));
616       mContentText.setSelection(foundpos, foundpos + replaceText.length());
617     }
618     mContentText.requestFocus();
619   }
620 }
621