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