Java tutorial
/* * Copyright (C) 2014 Vlad Mihalachi * * This file is part of Text Editor 8000. * * Text Editor 8000 is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Text Editor 8000 is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.maskyn.fileeditorpro.activity; import android.app.ProgressDialog; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Configuration; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Typeface; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.os.IBinder; import android.preference.PreferenceManager; import android.provider.DocumentsContract; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.view.MenuItemCompat; import android.support.v7.app.ActionBarActivity; import android.support.v7.app.ActionBarDrawerToggle; import android.support.v7.widget.ShareActionProvider; import android.support.v7.widget.Toolbar; import android.text.Editable; import android.text.InputType; import android.text.Selection; import android.text.Spannable; import android.text.TextPaint; import android.text.TextUtils; import android.text.TextWatcher; import android.text.method.KeyListener; import android.text.style.ForegroundColorSpan; import android.text.style.UnderlineSpan; import android.util.AttributeSet; import android.util.Log; import android.view.Gravity; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.inputmethod.InputMethodManager; import android.widget.AdapterView; import android.widget.EditText; import android.widget.HorizontalScrollView; import android.widget.ListView; import android.widget.Toast; import com.faizmalkani.floatingactionbutton.FloatingActionButton; import com.spazedog.lib.rootfw4.RootFW; import com.spazedog.lib.rootfw4.utils.io.FileReader; import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang3.ArrayUtils; import java.io.BufferedReader; import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.maskyn.fileeditorpro.R; import com.maskyn.fileeditorpro.adapter.AdapterDrawer; import com.maskyn.fileeditorpro.dialogfragment.ChangelogDialog; import com.maskyn.fileeditorpro.dialogfragment.EditTextDialog; import com.maskyn.fileeditorpro.dialogfragment.FileInfoDialog; import com.maskyn.fileeditorpro.dialogfragment.FindTextDialog; import com.maskyn.fileeditorpro.dialogfragment.NewFileDetailsDialog; import com.maskyn.fileeditorpro.dialogfragment.NumberPickerDialog; import com.maskyn.fileeditorpro.dialogfragment.SaveFileDialog; import com.maskyn.fileeditorpro.preferences.PreferenceChangeType; import com.maskyn.fileeditorpro.preferences.PreferenceHelper; import com.maskyn.fileeditorpro.task.SaveFileTask; import com.maskyn.fileeditorpro.texteditor.EditTextPadding; import com.maskyn.fileeditorpro.texteditor.FileUtils; import com.maskyn.fileeditorpro.texteditor.LineUtils; import com.maskyn.fileeditorpro.texteditor.PageSystem; import com.maskyn.fileeditorpro.texteditor.PageSystemButtons; import com.maskyn.fileeditorpro.texteditor.Patterns; import com.maskyn.fileeditorpro.texteditor.SearchResult; import com.maskyn.fileeditorpro.util.AccessStorageApi; import com.maskyn.fileeditorpro.util.AccessoryView; import com.maskyn.fileeditorpro.util.AnimationUtils; import com.maskyn.fileeditorpro.util.AppInfoHelper; import com.maskyn.fileeditorpro.util.Device; import com.maskyn.fileeditorpro.util.GreatUri; import com.maskyn.fileeditorpro.util.IHomeActivity; import com.maskyn.fileeditorpro.util.MimeTypes; import com.maskyn.fileeditorpro.util.ThemeUtils; import com.maskyn.fileeditorpro.util.ViewUtils; import com.maskyn.fileeditorpro.views.CustomDrawerLayout; import com.maskyn.fileeditorpro.views.DialogHelper; import com.maskyn.fileeditorpro.views.GoodScrollView; public abstract class MainActivity extends ActionBarActivity implements IHomeActivity, FindTextDialog.SearchDialogInterface, GoodScrollView.ScrollInterface, PageSystem.PageSystemInterface, PageSystemButtons.PageButtonsInterface, NumberPickerDialog.INumberPickerDialog, SaveFileDialog.ISaveDialog, AdapterView.OnItemClickListener, AdapterDrawer.Callbacks, AccessoryView.IAccessoryView, EditTextDialog.EditDialogListener { //region VARIABLES private static final int READ_REQUEST_CODE = 42, CREATE_REQUEST_CODE = 43, SAVE_AS_REQUEST_CODE = 44, ID_SELECT_ALL = android.R.id.selectAll, ID_CUT = android.R.id.cut, ID_COPY = android.R.id.copy, ID_PASTE = android.R.id.paste, SELECT_FILE_CODE = 121, SELECT_FOLDER_CODE = 122, SYNTAX_DELAY_MILLIS_SHORT = 250, SYNTAX_DELAY_MILLIS_LONG = 1500, ID_UNDO = R.id.im_undo, ID_REDO = R.id.im_redo, CHARS_TO_COLOR = 2500; private final Handler updateHandler = new Handler(); private final Runnable colorRunnable_duringEditing = new Runnable() { @Override public void run() { mEditor.replaceTextKeepCursor(null); } }; private final Runnable colorRunnable_duringScroll = new Runnable() { @Override public void run() { mEditor.replaceTextKeepCursor(null); } }; private boolean fileOpened = false; private static String fileExtension = ""; /* * This class provides a handy way to tie together the functionality of * {@link DrawerLayout} and the framework <code>ActionBar</code> to implement the recommended * design for navigation drawers. */ private ActionBarDrawerToggle mDrawerToggle; /* * The Drawer Layout */ private CustomDrawerLayout mDrawerLayout; private static GoodScrollView verticalScroll; private static GreatUri greatUri = new GreatUri(Uri.EMPTY, "", ""); private Editor mEditor; private HorizontalScrollView horizontalScroll; private static SearchResult searchResult; private static PageSystem pageSystem; private PageSystemButtons pageSystemButtons; private static String currentEncoding = "UTF-16"; private Toolbar toolbar; /* Navigation Drawer */ private AdapterDrawer arrayAdapter; private LinkedList<GreatUri> greatUris; //endregion //region Activity facts @Override protected void onCreate(Bundle savedInstanceState) { // set the windows background ThemeUtils.setWindowsBackground(this); // super!! super.onCreate(savedInstanceState); // setup the layout setContentView(R.layout.activity_home); toolbar = (Toolbar) findViewById(R.id.my_awesome_toolbar); setSupportActionBar(toolbar); // setup the navigation drawer setupNavigationDrawer(); // reset text editor setupTextEditor(); hideTextEditor(); /* First Time we open this activity */ if (savedInstanceState == null) { // Open mDrawerLayout.openDrawer(Gravity.START); // Set the default title getSupportActionBar().setTitle(getString(R.string.nome_app_turbo_editor)); } // parse the intent parseIntent(getIntent()); // show a dialog with the changelog showChangeLog(); } @Override protected final void onPostCreate(Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); mDrawerToggle.syncState(); } @Override public void onResume() { super.onResume(); // Refresh the list view refreshList(); } @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); parseIntent(intent); } @Override public void onPause() { super.onPause(); if (PreferenceHelper.getAutoSave(getBaseContext()) && mEditor.canSaveFile()) { saveTheFile(false); mEditor.fileSaved(); // so it doesn't ask to save in onDetach } } @Override protected void onDestroy() { try { closeKeyBoard(); } catch (NullPointerException e) { e.printStackTrace(); } super.onDestroy(); } @Override public final void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); mDrawerToggle.onConfigurationChanged(newConfig); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK) { onBackPressed(); return true; } else if (keyCode == KeyEvent.KEYCODE_MENU) { return false; } else { if (mEditor == null) mEditor = (Editor) findViewById(R.id.editor); // this will happen on first key pressed on hard-keyboard only. Once myInputField // gets the focus again, it will automatically receive further key presses. try { if (fileOpened && mEditor != null && !mEditor.hasFocus()) { mEditor.requestFocus(); mEditor.onKeyDown(keyCode, event); return true; } } catch (NullPointerException ex) { } } return false; } @Override public void onBackPressed() { try { // if we should ignore the back button if (PreferenceHelper.getIgnoreBackButton(this)) return; if (mDrawerLayout.isDrawerOpen(Gravity.START) && fileOpened) { mDrawerLayout.closeDrawer(Gravity.START); } else if (mDrawerLayout.isDrawerOpen(Gravity.END) && fileOpened) { mDrawerLayout.closeDrawer(Gravity.END); } else if (fileOpened && mEditor.canSaveFile()) { new SaveFileDialog(greatUri, pageSystem.getAllText(mEditor.getText().toString()), currentEncoding) .show(getFragmentManager(), "dialog"); } else if (fileOpened) { // remove editor fragment hideTextEditor(); // Set the default title getSupportActionBar().setTitle(getString(R.string.nome_app_turbo_editor)); closedTheFile(); mDrawerLayout.openDrawer(Gravity.START); mDrawerLayout.closeDrawer(Gravity.END); } else { showInterstitial(); super.onBackPressed(); } } catch (Exception e) { // maybe something is null, who knows } } @Override protected void onActivityResult(int requestCode, int resultCode, final Intent intent) { super.onActivityResult(requestCode, resultCode, intent); if (resultCode == RESULT_OK) { if (requestCode == SELECT_FILE_CODE) { final Uri data = intent.getData(); final GreatUri newUri = new GreatUri(data, AccessStorageApi.getPath(this, data), AccessStorageApi.getName(this, data)); newFileToOpen(newUri, ""); } else if (requestCode == SELECT_FOLDER_CODE) { FileFilter fileFilter = new FileFilter() { public boolean accept(File file) { return file.isFile(); } }; final Uri data = intent.getData(); File dir = new File(data.getPath()); File[] fileList = dir.listFiles(fileFilter); for (int i = 0; i < fileList.length; i++) { Uri particularUri = Uri.parse("file://" + fileList[i].getPath()); final GreatUri newUri = new GreatUri(particularUri, AccessStorageApi.getPath(this, particularUri), AccessStorageApi.getName(this, particularUri)); greatUris.add(newUri); refreshList(newUri, true, false); arrayAdapter.selectPosition(newUri); } if (fileList.length > 0) { Uri particularUri = Uri.parse("file://" + fileList[0].getPath()); final GreatUri newUri = new GreatUri(particularUri, AccessStorageApi.getPath(this, particularUri), AccessStorageApi.getName(this, particularUri)); newFileToOpen(newUri, ""); } } else { final Uri data = intent.getData(); final GreatUri newUri = new GreatUri(data, AccessStorageApi.getPath(this, data), AccessStorageApi.getName(this, data)); // grantUriPermission(getPackageName(), data, Intent.FLAG_GRANT_READ_URI_PERMISSION); final int takeFlags = intent.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); // Check for the freshest data. getContentResolver().takePersistableUriPermission(data, takeFlags); if (requestCode == READ_REQUEST_CODE || requestCode == CREATE_REQUEST_CODE) { newFileToOpen(newUri, ""); } if (requestCode == SAVE_AS_REQUEST_CODE) { new SaveFileTask(this, newUri, pageSystem.getAllText(mEditor.getText().toString()), currentEncoding, new SaveFileTask.SaveFileInterface() { @Override public void fileSaved(Boolean success) { savedAFile(greatUri, false); newFileToOpen(newUri, ""); } }).execute(); } } } } @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { // Send the event that a file was selected newFileToOpen(greatUris.get(position), ""); } //endregion //region MENU @Override public boolean onCreateOptionsMenu(Menu menu) { if (fileOpened && searchResult != null) getMenuInflater().inflate(R.menu.fragment_editor_search, menu); else if (fileOpened) getMenuInflater().inflate(R.menu.fragment_editor, menu); return super.onCreateOptionsMenu(menu); } @Override public boolean onPrepareOptionsMenu(Menu menu) { if (fileOpened && searchResult != null) { MenuItem imReplace = menu.findItem(R.id.im_replace); MenuItem imReplaceAll = menu.findItem(R.id.im_replace_all); MenuItem imPrev = menu.findItem(R.id.im_previous_item); MenuItem imNext = menu.findItem(R.id.im_next_item); if (imReplace != null) imReplace.setVisible(searchResult.canReplaceSomething()); if (imReplaceAll != null) imReplaceAll.setVisible(searchResult.canReplaceSomething()); if (imPrev != null) imPrev.setVisible(searchResult.hasPrevious()); if (imNext != null) imNext.setVisible(searchResult.hasNext()); } else if (fileOpened) { MenuItem imSave = menu.findItem(R.id.im_save); MenuItem imUndo = menu.findItem(R.id.im_undo); MenuItem imRedo = menu.findItem(R.id.im_redo); if (mEditor != null) { if (imSave != null) imSave.setVisible(mEditor.canSaveFile()); if (imUndo != null) imUndo.setVisible(mEditor.getCanUndo()); if (imRedo != null) imRedo.setVisible(mEditor.getCanRedo()); } else { imSave.setVisible(false); imUndo.setVisible(false); imRedo.setVisible(false); } MenuItem imMarkdown = menu.findItem(R.id.im_view_markdown); boolean isMarkdown = Arrays.asList(MimeTypes.MIME_MARKDOWN) .contains(FilenameUtils.getExtension(greatUri.getFileName())); if (imMarkdown != null) imMarkdown.setVisible(isMarkdown); MenuItem imShare = menu.findItem(R.id.im_share); if (imMarkdown != null) { ShareActionProvider shareAction = (ShareActionProvider) MenuItemCompat.getActionProvider(imShare); Intent shareIntent = new Intent(); shareIntent.setAction(Intent.ACTION_SEND); shareIntent.putExtra(Intent.EXTRA_STREAM, greatUri.getUri()); shareIntent.setType("text/plain"); shareAction.setShareIntent(shareIntent); } } return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { int i = item.getItemId(); if (mDrawerToggle.onOptionsItemSelected(item)) { Toast.makeText(getBaseContext(), "drawer click", Toast.LENGTH_SHORT).show(); mDrawerLayout.closeDrawer(Gravity.END); return true; } else if (i == R.id.im_save_normaly) { saveTheFile(false); } else if (i == R.id.im_save_as) { saveTheFile(true); } else if (i == R.id.im_rename) { EditTextDialog.newInstance(EditTextDialog.Actions.Rename, greatUri.getFileName()) .show(getFragmentManager().beginTransaction(), "dialog"); } else if (i == R.id.im_undo) { mEditor.onTextContextMenuItem(ID_UNDO); } else if (i == R.id.im_redo) { mEditor.onTextContextMenuItem(ID_REDO); } else if (i == R.id.im_search) { FindTextDialog.newInstance(mEditor.getText().toString()).show(getFragmentManager().beginTransaction(), "dialog"); } else if (i == R.id.im_cancel) { searchResult = null; invalidateOptionsMenu(); } else if (i == R.id.im_replace) { replaceText(false); } else if (i == R.id.im_replace_all) { replaceText(true); } else if (i == R.id.im_next_item) { nextResult(); } else if (i == R.id.im_previous_item) { previousResult(); } else if (i == R.id.im_goto_line) { int min = mEditor.getLineUtils().firstReadLine(); int max = mEditor.getLineUtils().lastReadLine(); NumberPickerDialog.newInstance(NumberPickerDialog.Actions.GoToLine, min, min, max) .show(getFragmentManager().beginTransaction(), "dialog"); } else if (i == R.id.im_view_it_on_browser) { Intent browserIntent; try { browserIntent = new Intent(Intent.ACTION_VIEW); browserIntent.setDataAndType(greatUri.getUri(), "*/*"); startActivity(browserIntent); } catch (ActivityNotFoundException ex2) { // } } else if (i == R.id.im_view_markdown) { Intent browserIntent = new Intent(MainActivity.this, MarkdownActivity.class); browserIntent.putExtra("text", pageSystem.getAllText(mEditor.getText().toString())); startActivity(browserIntent); } else if (i == R.id.im_info) { FileInfoDialog.newInstance(greatUri.getUri()).show(getFragmentManager().beginTransaction(), "dialog"); } else if (i == R.id.im_donate) { final String appPackageName = "com.maskyn.fileeditorpro"; try { startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + appPackageName))); } catch (android.content.ActivityNotFoundException anfe) { startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=" + appPackageName))); } } return super.onOptionsItemSelected(item); } //endregion // region OTHER THINGS void replaceText(boolean all) { if (all) { if (searchResult.isRegex) mEditor.setText(pageSystem.getAllText(mEditor.getText().toString()) .replaceAll(searchResult.whatToSearch, searchResult.textToReplace)); else mEditor.setText(pageSystem.getAllText(mEditor.getText().toString()) .replace(searchResult.whatToSearch, searchResult.textToReplace)); searchResult = null; invalidateOptionsMenu(); } else { int start = searchResult.foundIndex.get(searchResult.index); int end = start + searchResult.textLength; mEditor.setText(mEditor.getText().replace(start, end, searchResult.textToReplace)); searchResult.doneReplace(); invalidateOptionsMenu(); if (searchResult.hasNext()) nextResult(); else if (searchResult.hasPrevious()) previousResult(); } } void nextResult() { if (searchResult.index == mEditor.getLineCount() - 1) // last result of page { return; } if (searchResult.index < searchResult.numberOfResults() - 1) { // equal zero is not good searchResult.index++; final int line = mEditor.getLineUtils().getLineFromIndex( searchResult.foundIndex.get(searchResult.index), mEditor.getLineCount(), mEditor.getLayout()); verticalScroll.post(new Runnable() { @Override public void run() { int y = mEditor.getLayout().getLineTop(line); if (y > 100) y -= 100; else y = 0; verticalScroll.scrollTo(0, y); } }); mEditor.setFocusable(true); mEditor.requestFocus(); mEditor.setSelection(searchResult.foundIndex.get(searchResult.index), searchResult.foundIndex.get(searchResult.index) + searchResult.textLength); } invalidateOptionsMenu(); } void previousResult() { if (searchResult.index == 0) return; if (searchResult.index > 0) { searchResult.index--; final int line = LineUtils.getLineFromIndex(searchResult.foundIndex.get(searchResult.index), mEditor.getLineCount(), mEditor.getLayout()); verticalScroll.post(new Runnable() { @Override public void run() { int y = mEditor.getLayout().getLineTop(line); if (y > 100) y -= 100; else y = 0; verticalScroll.scrollTo(0, y); } }); mEditor.setFocusable(true); mEditor.requestFocus(); mEditor.setSelection(searchResult.foundIndex.get(searchResult.index), searchResult.foundIndex.get(searchResult.index) + searchResult.textLength); } invalidateOptionsMenu(); } private void saveTheFile(boolean saveAs) { if (!saveAs && greatUri != null && greatUri.getUri() != null && greatUri.getUri() != Uri.EMPTY) new SaveFileTask(this, greatUri, pageSystem.getAllText(mEditor.getText().toString()), currentEncoding, new SaveFileTask.SaveFileInterface() { @Override public void fileSaved(Boolean success) { savedAFile(greatUri, true); } }).execute(); else { if (Device.hasKitKatApi() && PreferenceHelper.getUseStorageAccessFramework(this)) { Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); intent.setType("*/*"); intent.putExtra(Intent.EXTRA_TITLE, greatUri.getFileName()); startActivityForResult(intent, SAVE_AS_REQUEST_CODE); } else { new NewFileDetailsDialog(greatUri, pageSystem.getAllText(mEditor.getText().toString()), currentEncoding).show(getFragmentManager().beginTransaction(), "dialog"); } } } /** * Setup the navigation drawer */ private void setupNavigationDrawer() { mDrawerLayout = (CustomDrawerLayout) findViewById(R.id.drawer_layout); /* Action Bar final ActionBar ab = toolbar; ab.setDisplayHomeAsUpEnabled(true); ab.setHomeButtonEnabled(true);*/ /* Navigation drawer */ mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout, toolbar, R.string.nome_app_turbo_editor, R.string.nome_app_turbo_editor) { @Override public void onDrawerOpened(View drawerView) { supportInvalidateOptionsMenu(); try { closeKeyBoard(); } catch (NullPointerException e) { e.printStackTrace(); } } @Override public void onDrawerClosed(View view) { supportInvalidateOptionsMenu(); } }; /* link the mDrawerToggle to the Drawer Layout */ mDrawerLayout.setDrawerListener(mDrawerToggle); //mDrawerLayout.setFocusableInTouchMode(false); ListView listView = (ListView) findViewById(android.R.id.list); listView.setEmptyView(findViewById(android.R.id.empty)); greatUris = new LinkedList<>(); arrayAdapter = new AdapterDrawer(this, greatUris, this); listView.setAdapter(arrayAdapter); listView.setOnItemClickListener(this); } private void setupTextEditor() { verticalScroll = (GoodScrollView) findViewById(R.id.vertical_scroll); horizontalScroll = (HorizontalScrollView) findViewById(R.id.horizontal_scroll); mEditor = (Editor) findViewById(R.id.editor); AccessoryView accessoryView = (AccessoryView) findViewById(R.id.accessoryView); accessoryView.setInterface(this); HorizontalScrollView parentAccessoryView = (HorizontalScrollView) findViewById(R.id.parent_accessory_view); ViewUtils.setVisible(parentAccessoryView, PreferenceHelper.getUseAccessoryView(this)); if (PreferenceHelper.getWrapContent(this)) { horizontalScroll.removeView(mEditor); verticalScroll.removeView(horizontalScroll); verticalScroll.addView(mEditor); } verticalScroll.setScrollInterface(this); pageSystem = new PageSystem(this, this, ""); pageSystemButtons = new PageSystemButtons(this, this, (FloatingActionButton) findViewById(R.id.fabPrev), (FloatingActionButton) findViewById(R.id.fabNext)); mEditor.setupEditor(); } private void showTextEditor() { fileOpened = true; findViewById(R.id.text_editor).setVisibility(View.VISIBLE); findViewById(R.id.no_file_opened_messagge).setVisibility(View.GONE); mEditor.resetVariables(); searchResult = null; invalidateOptionsMenu(); mEditor.disableTextChangedListener(); mEditor.replaceTextKeepCursor(pageSystem.getCurrentPageText()); mEditor.enableTextChangedListener(); } private void hideTextEditor() { fileOpened = false; try { findViewById(R.id.text_editor).setVisibility(View.GONE); findViewById(R.id.no_file_opened_messagge).setVisibility(View.VISIBLE); mEditor.disableTextChangedListener(); mEditor.replaceTextKeepCursor(""); mEditor.enableTextChangedListener(); } catch (Exception e) { // lol } } /** * Parses the intent */ private void parseIntent(Intent intent) { final String action = intent.getAction(); final String type = intent.getType(); if (Intent.ACTION_VIEW.equals(action) || Intent.ACTION_EDIT.equals(action) || Intent.ACTION_PICK.equals(action) && type != null) { // Post event //newFileToOpen(new File(intent // .getData().getPath()), ""); Uri uri = intent.getData(); GreatUri newUri = new GreatUri(uri, AccessStorageApi.getPath(this, uri), AccessStorageApi.getName(this, uri)); newFileToOpen(newUri, ""); } else if (Intent.ACTION_SEND.equals(action) && type != null) { if ("text/plain".equals(type)) { newFileToOpen(new GreatUri(Uri.EMPTY, "", ""), intent.getStringExtra(Intent.EXTRA_TEXT)); } } } /** * Show a dialog with the changelog */ private void showChangeLog() { final String currentVersion = AppInfoHelper.getCurrentVersion(this); final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); final String lastVersion = preferences.getString("last_version", currentVersion); preferences.edit().putString("last_version", currentVersion).apply(); if (!lastVersion.equals(currentVersion)) { ChangelogDialog.showChangeLogDialog(getFragmentManager()); } } // closes the soft keyboard private void closeKeyBoard() throws NullPointerException { // Central system API to the overall input method framework (IMF) architecture InputMethodManager inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); // Base interface for a remotable object IBinder windowToken = getCurrentFocus().getWindowToken(); // Hide type int hideType = InputMethodManager.HIDE_NOT_ALWAYS; // Hide the KeyBoard inputManager.hideSoftInputFromWindow(windowToken, hideType); } void updateTextSyntax() { if (!PreferenceHelper.getSyntaxHighlight(this) || mEditor.hasSelection() || updateHandler == null || colorRunnable_duringEditing == null) return; updateHandler.removeCallbacks(colorRunnable_duringEditing); updateHandler.removeCallbacks(colorRunnable_duringScroll); updateHandler.postDelayed(colorRunnable_duringEditing, SYNTAX_DELAY_MILLIS_LONG); } private void refreshList() { refreshList(null, false, false); } private void refreshList(@Nullable GreatUri thisUri, boolean add, boolean delete) { int max_recent_files = 15; if (add) max_recent_files--; // File paths saved in preferences String[] savedPaths = PreferenceHelper.getSavedPaths(this); int first_index_of_array = savedPaths.length > max_recent_files ? savedPaths.length - max_recent_files : 0; savedPaths = ArrayUtils.subarray(savedPaths, first_index_of_array, savedPaths.length); // File names for the list greatUris.clear(); // StringBuilder that will contain the file paths StringBuilder sb = new StringBuilder(); // for cycle to convert paths to names for (int i = 0; i < savedPaths.length; i++) { Uri particularUri = Uri.parse(savedPaths[i]); String name = AccessStorageApi.getName(this, particularUri); // Check that the file exist // if is null or empty the particular url we dont use it if (particularUri != null && !particularUri.equals(Uri.EMPTY) && !TextUtils.isEmpty(name)) { // if the particular uri is good boolean good = false; if (thisUri == null || thisUri.getUri() == null || thisUri.getUri() == Uri.EMPTY) good = true; else { if (delete == false) good = true; else if (!thisUri.getUri().equals(particularUri)) good = true; else good = false; } if (good) { greatUris.addFirst( new GreatUri(particularUri, AccessStorageApi.getPath(this, particularUri), name)); sb.append(savedPaths[i]).append(","); } } //} } // if is not null, empty, we have to add something and we dont already have this uri if (thisUri != null && !thisUri.getUri().equals(Uri.EMPTY) && add && !ArrayUtils.contains(savedPaths, thisUri.getUri().toString())) { sb.append(thisUri.getUri().toString()).append(","); greatUris.addFirst(thisUri); } // save list without empty or non existed files PreferenceHelper.setSavedPaths(this, sb); // Set adapter arrayAdapter.notifyDataSetChanged(); } //endregion //region EVENTBUS void newFileToOpen(final GreatUri newUri, final String newFileText) { if (fileOpened && mEditor != null && mEditor.canSaveFile() && greatUri != null && pageSystem != null && currentEncoding != null) { new SaveFileDialog(greatUri, pageSystem.getAllText(mEditor.getText().toString()), currentEncoding, true, newUri).show(getFragmentManager(), "dialog"); return; } new AsyncTask<Void, Void, Void>() { String message = ""; String fileText = ""; String fileName = ""; String encoding = "UTF-16"; boolean isRootRequired = false; ProgressDialog progressDialog; @Override protected void onPreExecute() { super.onPreExecute(); // Close the drawer mDrawerLayout.closeDrawer(Gravity.START); progressDialog = new ProgressDialog(MainActivity.this); progressDialog.setMessage(getString(R.string.please_wait)); progressDialog.show(); } @Override protected Void doInBackground(Void... params) { try { // if no new uri if (newUri == null || newUri.getUri() == null || newUri.getUri() == Uri.EMPTY) { fileExtension = "txt"; fileText = newFileText; } else { String filePath = newUri.getFilePath(); // if the uri has no path if (TextUtils.isEmpty(filePath)) { fileName = newUri.getFileName(); fileExtension = FilenameUtils.getExtension(fileName).toLowerCase(); readUri(newUri.getUri(), filePath, false); } // if the uri has a path else { fileName = FilenameUtils.getName(filePath); fileExtension = FilenameUtils.getExtension(fileName).toLowerCase(); isRootRequired = !newUri.isReadable(); // if we cannot read the file, root permission required if (isRootRequired) { readUri(newUri.getUri(), filePath, true); } // if we can read the file associated with the uri else { readUri(newUri.getUri(), filePath, false); } } } greatUri = newUri; } catch (Exception e) { message = e.getMessage(); fileText = ""; } while (mDrawerLayout.isDrawerOpen(Gravity.START)) { try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } } return null; } private void readUri(Uri uri, String path, boolean asRoot) throws IOException { BufferedReader buffer = null; StringBuilder stringBuilder = new StringBuilder(); String line; if (asRoot) { encoding = "UTF-8"; // Connect the shared connection if (RootFW.connect()) { FileReader reader = RootFW.getFileReader(path); buffer = new BufferedReader(reader); } } else { boolean autoencoding = PreferenceHelper.getAutoEncoding(MainActivity.this); if (autoencoding) { encoding = FileUtils.getDetectedEncoding(getContentResolver().openInputStream(uri)); if (encoding.isEmpty()) { encoding = PreferenceHelper.getEncoding(MainActivity.this); } } else { encoding = PreferenceHelper.getEncoding(MainActivity.this); } InputStream inputStream = getContentResolver().openInputStream(uri); if (inputStream != null) { buffer = new BufferedReader(new InputStreamReader(inputStream, encoding)); } } if (buffer != null) { while ((line = buffer.readLine()) != null) { stringBuilder.append(line); stringBuilder.append("\n"); } buffer.close(); fileText = stringBuilder.toString(); } if (isRootRequired) RootFW.disconnect(); } @Override protected void onPostExecute(Void result) { super.onPostExecute(result); progressDialog.hide(); if (!TextUtils.isEmpty(message)) { Toast.makeText(MainActivity.this, message, Toast.LENGTH_LONG).show(); cannotOpenFile(); } else { pageSystem = new PageSystem(MainActivity.this, MainActivity.this, fileText); currentEncoding = encoding; aFileWasSelected(greatUri); showTextEditor(); if (fileName.isEmpty()) getSupportActionBar().setTitle(R.string.new_file); else getSupportActionBar().setTitle(fileName); if (greatUri != null) { refreshList(greatUri, true, false); } } } }.execute(); } public void savedAFile(GreatUri uri, boolean updateList) { if (uri != null) { greatUri = uri; String name = uri.getFileName(); fileExtension = FilenameUtils.getExtension(name).toLowerCase(); toolbar.setTitle(name); if (updateList) { refreshList(uri, true, false); arrayAdapter.selectPosition(uri); } } mEditor.clearHistory(); mEditor.fileSaved(); invalidateOptionsMenu(); try { closeKeyBoard(); } catch (NullPointerException e) { e.printStackTrace(); } } /** * When a file can't be opened * Invoked by the EditorFragment * * @param event The event called */ void cannotOpenFile() { // mDrawerLayout.openDrawer(Gravity.LEFT); // getSupportActionBar().setTitle(getString(R.string.nome_app_turbo_editor)); // supportInvalidateOptionsMenu(); // Replace fragment hideTextEditor(); } public void aPreferenceValueWasChanged(final PreferenceChangeType type) { this.aPreferenceValueWasChanged(new ArrayList<PreferenceChangeType>() { { add(type); } }); } void aPreferenceValueWasChanged(List<PreferenceChangeType> types) { if (types.contains(PreferenceChangeType.THEME_CHANGE)) { ThemeUtils.setWindowsBackground(this); AccessoryView accessoryView = (AccessoryView) findViewById(R.id.accessoryView); accessoryView.updateTextColors(); } if (types.contains(PreferenceChangeType.WRAP_CONTENT)) { if (PreferenceHelper.getWrapContent(this)) { horizontalScroll.removeView(mEditor); verticalScroll.removeView(horizontalScroll); verticalScroll.addView(mEditor); } else { verticalScroll.removeView(mEditor); verticalScroll.addView(horizontalScroll); horizontalScroll.addView(mEditor); } } else if (types.contains(PreferenceChangeType.LINE_NUMERS)) { mEditor.disableTextChangedListener(); mEditor.replaceTextKeepCursor(null); mEditor.enableTextChangedListener(); mEditor.updatePadding(); } else if (types.contains(PreferenceChangeType.SYNTAX)) { mEditor.disableTextChangedListener(); mEditor.replaceTextKeepCursor(mEditor.getText().toString()); mEditor.enableTextChangedListener(); } else if (types.contains(PreferenceChangeType.MONOSPACE)) { if (PreferenceHelper.getUseMonospace(this)) mEditor.setTypeface(Typeface.MONOSPACE); else mEditor.setTypeface(Typeface.DEFAULT); } else if (types.contains(PreferenceChangeType.THEME_CHANGE)) { if (PreferenceHelper.getLightTheme(this)) { mEditor.setTextColor(getResources().getColor(R.color.textColorInverted)); } else { mEditor.setTextColor(getResources().getColor(R.color.textColor)); } } else if (types.contains(PreferenceChangeType.TEXT_SUGGESTIONS) || types.contains(PreferenceChangeType.READ_ONLY)) { if (PreferenceHelper.getReadOnly(this)) { mEditor.setReadOnly(true); } else { mEditor.setReadOnly(false); if (PreferenceHelper.getSuggestionActive(this)) { mEditor.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE); } else { mEditor.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD | InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE); } } // sometimes it becomes monospace after setting the input type if (PreferenceHelper.getUseMonospace(this)) mEditor.setTypeface(Typeface.MONOSPACE); else mEditor.setTypeface(Typeface.DEFAULT); } else if (types.contains(PreferenceChangeType.FONT_SIZE)) { mEditor.updatePadding(); mEditor.setTextSize(PreferenceHelper.getFontSize(this)); } else if (types.contains(PreferenceChangeType.ACCESSORY_VIEW)) { HorizontalScrollView parentAccessoryView = (HorizontalScrollView) findViewById( R.id.parent_accessory_view); ViewUtils.setVisible(parentAccessoryView, PreferenceHelper.getUseAccessoryView(this)); mEditor.updatePadding(); } else if (types.contains(PreferenceChangeType.ENCODING)) { String oldEncoding, newEncoding; oldEncoding = currentEncoding; newEncoding = PreferenceHelper.getEncoding(this); try { final byte[] oldText = mEditor.getText().toString().getBytes(oldEncoding); mEditor.disableTextChangedListener(); mEditor.replaceTextKeepCursor(new String(oldText, newEncoding)); mEditor.enableTextChangedListener(); currentEncoding = newEncoding; } catch (UnsupportedEncodingException ignored) { try { final byte[] oldText = mEditor.getText().toString().getBytes(oldEncoding); mEditor.disableTextChangedListener(); mEditor.replaceTextKeepCursor(new String(oldText, "UTF-16")); mEditor.enableTextChangedListener(); } catch (UnsupportedEncodingException ignored2) { } } } } void aFileWasSelected(GreatUri uri) { arrayAdapter.selectPosition(uri); } void closedTheFile() { arrayAdapter.selectPosition(new GreatUri(Uri.EMPTY, "", "")); } //endregion //region Calls from the layout public void OpenFile(View view) { if (Device.hasKitKatApi() && PreferenceHelper.getUseStorageAccessFramework(this)) { // ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's file // browser. Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); // Filter to only show results that can be "opened", such as a // file (as opposed to a list of contacts or timezones) intent.addCategory(Intent.CATEGORY_OPENABLE); // Filter to show only images, using the image MIME data type. // If one wanted to search for ogg vorbis files, the type would be "audio/ogg". // To search for all documents available via installed storage providers, // it would be "*/*". intent.setType("*/*"); startActivityForResult(intent, READ_REQUEST_CODE); } else { Intent subActivity = new Intent(MainActivity.this, SelectFileActivity.class); subActivity.putExtra("action", SelectFileActivity.Actions.SelectFile); AnimationUtils.startActivityWithScale(this, subActivity, true, SELECT_FILE_CODE, view); } } public void OpenFolder(View view) { //http://stackoverflow.com/questions/21544331/trying-open-a-specific-folder-in-android-using-intent //http://www.androidpub.com/1773967 ??? ? //http://vissel.tistory.com/97 ? sdcard? ? ? ? ? . //http://it77.tistory.com/34 ? ?? ?? ? //http://nekomimi.tistory.com/674 ?? , ??? ? //http://jungws55.tistory.com/227 ?? ? ? //http://stackoverflow.com/questions/8723738/open-folder-intent-chooser //http://www.blackmoonit.com/2010/02/handling-browse-for-folder-intents/ Handling Browse for Folder? intents //http://www.androidsnippets.com/pick-a-file-or-folder-with-andexplorer-intent.html Pick a file (or folder) with AndExplorer Intent //http://www.codeproject.com/Articles/547636/Android-Ready-to-use-simple-directory-chooser-dial //https://github.com/passy/Android-DirectoryChooser //https://android-arsenal.com/tag/35 Intent subActivity = new Intent(MainActivity.this, SelectFileActivity.class); subActivity.putExtra("foldermode", true); subActivity.putExtra("action", SelectFileActivity.Actions.SelectFile); AnimationUtils.startActivityWithScale(this, subActivity, true, SELECT_FOLDER_CODE, view); } public void CreateFile(View view) { if (Device.hasKitKatApi() && PreferenceHelper.getUseStorageAccessFramework(this)) { Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); intent.setType("*/*"); //intent.putExtra(Intent.EXTRA_TITLE, ".txt"); startActivityForResult(intent, CREATE_REQUEST_CODE); } else { newFileToOpen(new GreatUri(Uri.EMPTY, "", ""), ""); } } public void OpenInfo(View view) { DialogHelper.showAboutDialog(this); } public void OpenSettings(View view) { mDrawerLayout.closeDrawer(Gravity.START); mDrawerLayout.openDrawer(Gravity.END); } //endregion //region Ovverideses @Override public void nextPageClicked() { pageSystem.savePage(mEditor.getText().toString()); pageSystem.nextPage(); mEditor.disableTextChangedListener(); mEditor.replaceTextKeepCursor(pageSystem.getCurrentPageText()); mEditor.enableTextChangedListener(); verticalScroll.postDelayed(new Runnable() { @Override public void run() { verticalScroll.smoothScrollTo(0, 0); } }, 200); if (!PreferenceHelper.getPageSystemButtonsPopupShown(this)) { PreferenceHelper.setPageSystemButtonsPopupShown(this, true); Toast.makeText(this, getString(R.string.long_click_for_more_options), Toast.LENGTH_LONG).show(); } } @Override public void prevPageClicked() { pageSystem.savePage(mEditor.getText().toString()); pageSystem.prevPage(); mEditor.disableTextChangedListener(); mEditor.replaceTextKeepCursor(pageSystem.getCurrentPageText()); mEditor.enableTextChangedListener(); verticalScroll.postDelayed(new Runnable() { @Override public void run() { verticalScroll.smoothScrollTo(0, 0); } }, 200); if (!PreferenceHelper.getPageSystemButtonsPopupShown(this)) { PreferenceHelper.setPageSystemButtonsPopupShown(this, true); Toast.makeText(this, getString(R.string.long_click_for_more_options), Toast.LENGTH_LONG).show(); } } @Override public void pageSystemButtonLongClicked() { int maxPages = pageSystem.getMaxPage(); int currentPage = pageSystem.getCurrentPage(); NumberPickerDialog.newInstance(NumberPickerDialog.Actions.SelectPage, 0, currentPage, maxPages) .show(getFragmentManager().beginTransaction(), "dialog"); } @Override public boolean canReadNextPage() { return pageSystem.canReadNextPage(); } @Override public boolean canReadPrevPage() { return pageSystem.canReadPrevPage(); } @Override public void onSearchDone(SearchResult searchResult) { MainActivity.searchResult = searchResult; invalidateOptionsMenu(); final int line = LineUtils.getLineFromIndex(searchResult.foundIndex.getFirst(), mEditor.getLineCount(), mEditor.getLayout()); verticalScroll.post(new Runnable() { @Override public void run() { int y = mEditor.getLayout().getLineTop(line); if (y > 100) y -= 100; else y = 0; verticalScroll.scrollTo(0, y); } }); mEditor.setFocusable(true); mEditor.requestFocus(); mEditor.setSelection(searchResult.foundIndex.getFirst(), searchResult.foundIndex.getFirst() + searchResult.textLength); } @Override public void onPageChanged(int page) { pageSystemButtons.updateVisibility(false); searchResult = null; mEditor.clearHistory(); invalidateOptionsMenu(); } @Override public void onScrollChanged(int l, int t, int oldl, int oldt) { pageSystemButtons.updateVisibility(Math.abs(t) > 10); if (!PreferenceHelper.getSyntaxHighlight(this) || (mEditor.hasSelection() && searchResult == null) || updateHandler == null || colorRunnable_duringScroll == null) return; updateHandler.removeCallbacks(colorRunnable_duringEditing); updateHandler.removeCallbacks(colorRunnable_duringScroll); updateHandler.postDelayed(colorRunnable_duringScroll, SYNTAX_DELAY_MILLIS_SHORT); } @Override public void onNumberPickerDialogDismissed(NumberPickerDialog.Actions action, int value) { if (action == NumberPickerDialog.Actions.SelectPage) { pageSystem.savePage(mEditor.getText().toString()); pageSystem.goToPage(value); mEditor.disableTextChangedListener(); mEditor.replaceTextKeepCursor(pageSystem.getCurrentPageText()); mEditor.enableTextChangedListener(); verticalScroll.postDelayed(new Runnable() { @Override public void run() { verticalScroll.smoothScrollTo(0, 0); } }, 200); } else if (action == NumberPickerDialog.Actions.GoToLine) { int fakeLine = mEditor.getLineUtils().fakeLineFromRealLine(value); final int y = mEditor.getLineUtils().getYAtLine(verticalScroll, mEditor.getLineCount(), fakeLine); verticalScroll.postDelayed(new Runnable() { @Override public void run() { verticalScroll.smoothScrollTo(0, y); } }, 200); } } @Override public void userDoesntWantToSave(boolean openNewFile, GreatUri newUri) { mEditor.fileSaved(); if (openNewFile) newFileToOpen(newUri, ""); else cannotOpenFile(); } @Override public void CancelItem(int position, boolean andCloseOpenedFile) { refreshList(greatUris.get(position), false, true); if (andCloseOpenedFile) cannotOpenFile(); } @Override public void onButtonAccessoryViewClicked(String text) { mEditor.getText().insert(mEditor.getSelectionStart(), text); } @Override public void onEdittextDialogEnded(String result, String hint, EditTextDialog.Actions action) { if (Device.hasKitKatApi() && TextUtils.isEmpty(greatUri.getFilePath())) { Uri newUri = DocumentsContract.renameDocument(getContentResolver(), greatUri.getUri(), result); // if everything is ok if (newUri != null) { // delete current refreshList(greatUri, false, true); greatUri.setUri(newUri); greatUri.setFilePath(AccessStorageApi.getPath(this, newUri)); greatUri.setFileName(AccessStorageApi.getName(this, newUri)); new SaveFileTask(this, greatUri, pageSystem.getAllText(mEditor.getText().toString()), currentEncoding, new SaveFileTask.SaveFileInterface() { @Override public void fileSaved(Boolean success) { savedAFile(greatUri, true); } }).execute(); } else { Toast.makeText(this, R.string.file_cannot_be_renamed, Toast.LENGTH_SHORT).show(); } } else { File newFile = new File(greatUri.getParentFolder(), result); // if everything is ok if (new File(greatUri.getFilePath()).renameTo(newFile)) { // delete current refreshList(greatUri, false, true); greatUri.setUri(Uri.fromFile(newFile)); greatUri.setFilePath(newFile.getAbsolutePath()); greatUri.setFileName(newFile.getName()); new SaveFileTask(this, greatUri, pageSystem.getAllText(mEditor.getText().toString()), currentEncoding, new SaveFileTask.SaveFileInterface() { @Override public void fileSaved(Boolean success) { savedAFile(greatUri, true); } }).execute(); } else { Toast.makeText(this, R.string.file_cannot_be_renamed, Toast.LENGTH_SHORT).show(); } } /*new SaveFileTask(this, greatUri, pageSystem.getAllText(mEditor.getText().toString()), currentEncoding, new SaveFileTask.SaveFileInterface() { @Override public void fileSaved(Boolean success) { savedAFile(greatUri, true); } }).execute();*/ } //endregion public static class Editor extends EditText { //region VARIABLES private final TextPaint mPaintNumbers = new TextPaint(); /** * The edit history. */ private EditHistory mEditHistory; /** * The change listener. */ private EditTextChangeListener mChangeListener; /** * Disconnect this undo/redo from the text * view. */ private boolean enabledChangeListener; private int paddingTop; private int numbersWidth; private int lineHeight; private int lineCount, realLine, startingLine; private LineUtils lineUtils; /** * Is undo/redo being performed? This member * signals if an undo/redo operation is * currently being performed. Changes in the * text during undo/redo are not recorded * because it would mess up the undo history. */ private boolean mIsUndoOrRedo; private Matcher m; private boolean mShowUndo, mShowRedo; private boolean canSaveFile; private KeyListener keyListener; private int firstVisibleIndex, firstColoredIndex, lastVisibleIndex; private int deviceHeight; private int editorHeight; private boolean[] isGoodLineArray; private int[] realLines; private boolean wrapContent; private CharSequence textToHighlight; //endregion //region CONSTRUCTOR public Editor(final Context context, AttributeSet attrs) { super(context, attrs); } public void setupEditor() { //setLayerType(View.LAYER_TYPE_NONE, null); mEditHistory = new EditHistory(); mChangeListener = new EditTextChangeListener(); lineUtils = new LineUtils(); deviceHeight = getResources().getDisplayMetrics().heightPixels; paddingTop = EditTextPadding.getPaddingTop(getContext()); mPaintNumbers.setAntiAlias(true); mPaintNumbers.setDither(false); mPaintNumbers.setTextAlign(Paint.Align.RIGHT); mPaintNumbers.setColor(getResources().getColor(R.color.file_text)); if (PreferenceHelper.getLightTheme(getContext())) { setTextColor(getResources().getColor(R.color.textColorInverted)); } else { setTextColor(getResources().getColor(R.color.textColor)); } // update the padding of the editor updatePadding(); if (PreferenceHelper.getReadOnly(getContext())) { setReadOnly(true); } else { setReadOnly(false); if (PreferenceHelper.getSuggestionActive(getContext())) { setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE); } else { setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD | InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE); } } if (PreferenceHelper.getUseMonospace(getContext())) { setTypeface(Typeface.MONOSPACE); } else { setTypeface(Typeface.DEFAULT); } setTextSize(PreferenceHelper.getFontSize(getContext())); setFocusable(true); setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (!PreferenceHelper.getReadOnly(getContext())) { verticalScroll.tempDisableListener(1000); ((InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE)) .showSoftInput(Editor.this, InputMethodManager.SHOW_IMPLICIT); } } }); setOnFocusChangeListener(new View.OnFocusChangeListener() { @Override public void onFocusChange(View v, boolean hasFocus) { if (hasFocus && !PreferenceHelper.getReadOnly(getContext())) { verticalScroll.tempDisableListener(1000); ((InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE)) .showSoftInput(Editor.this, InputMethodManager.SHOW_IMPLICIT); } } }); setMaxHistorySize(30); resetVariables(); } public void setReadOnly(boolean value) { if (value) { keyListener = getKeyListener(); setKeyListener(null); } else { if (keyListener != null) setKeyListener(keyListener); } } public void updatePadding() { Context context = getContext(); if (PreferenceHelper.getLineNumbers(context)) { setPadding( EditTextPadding.getPaddingWithLineNumbers(context, PreferenceHelper.getFontSize(context)), EditTextPadding.getPaddingTop(context), EditTextPadding.getPaddingTop(context), 0); } else { setPadding(EditTextPadding.getPaddingWithoutLineNumbers(context), EditTextPadding.getPaddingTop(context), EditTextPadding.getPaddingTop(context), 0); } // add a padding from bottom verticalScroll.setPadding(0, 0, 0, EditTextPadding.getPaddingBottom(context)); } //region OVERRIDES @Override public void setTextSize(float size) { super.setTextSize(size); final float scale = getContext().getResources().getDisplayMetrics().density; mPaintNumbers.setTextSize((int) (size * scale * 0.65f)); numbersWidth = (int) (EditTextPadding.getPaddingWithLineNumbers(getContext(), PreferenceHelper.getFontSize(getContext())) * 0.8); lineHeight = getLineHeight(); } @Override public void onDraw(@NonNull final Canvas canvas) { if (lineCount != getLineCount() || startingLine != pageSystem.getStartingLine()) { startingLine = pageSystem.getStartingLine(); lineCount = getLineCount(); lineUtils.updateHasNewLineArray(pageSystem.getStartingLine(), lineCount, getLayout(), getText().toString()); isGoodLineArray = lineUtils.getGoodLines(); realLines = lineUtils.getRealLines(); } if (PreferenceHelper.getLineNumbers(getContext())) { wrapContent = PreferenceHelper.getWrapContent(getContext()); for (int i = 0; i < lineCount; i++) { // if last line we count it anyway if (!wrapContent || isGoodLineArray[i]) { realLine = realLines[i]; canvas.drawText(String.valueOf(realLine), numbersWidth, // they are all center aligned paddingTop + lineHeight * (i + 1), mPaintNumbers); } } } super.onDraw(canvas); } //endregion //region Other @Override public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) { if (event.isCtrlPressed()) { switch (keyCode) { case KeyEvent.KEYCODE_A: return onTextContextMenuItem(ID_SELECT_ALL); case KeyEvent.KEYCODE_X: return onTextContextMenuItem(ID_CUT); case KeyEvent.KEYCODE_C: return onTextContextMenuItem(ID_COPY); case KeyEvent.KEYCODE_V: return onTextContextMenuItem(ID_PASTE); case KeyEvent.KEYCODE_Z: if (getCanUndo()) { return onTextContextMenuItem(ID_UNDO); } case KeyEvent.KEYCODE_Y: if (getCanRedo()) { return onTextContextMenuItem(ID_REDO); } case KeyEvent.KEYCODE_S: ((MainActivity) getContext()).saveTheFile(false); return true; default: return super.onKeyDown(keyCode, event); } } else { switch (keyCode) { case KeyEvent.KEYCODE_TAB: String textToInsert = " "; int start, end; start = Math.max(getSelectionStart(), 0); end = Math.max(getSelectionEnd(), 0); getText().replace(Math.min(start, end), Math.max(start, end), textToInsert, 0, textToInsert.length()); return true; default: return super.onKeyDown(keyCode, event); } } } @Override public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) { if (event.isCtrlPressed()) { switch (keyCode) { case KeyEvent.KEYCODE_A: case KeyEvent.KEYCODE_X: case KeyEvent.KEYCODE_C: case KeyEvent.KEYCODE_V: case KeyEvent.KEYCODE_Z: case KeyEvent.KEYCODE_Y: case KeyEvent.KEYCODE_S: return true; default: return false; } } else { switch (keyCode) { case KeyEvent.KEYCODE_TAB: return true; case KeyEvent.KEYCODE_SHIFT_LEFT: case KeyEvent.KEYCODE_SHIFT_RIGHT: return super.onKeyUp(keyCode, event); default: return false; } } } /** * {@inheritDoc} */ @Override public boolean onTextContextMenuItem(final int id) { if (id == ID_UNDO) { undo(); return true; } else if (id == ID_REDO) { redo(); return true; } else { return super.onTextContextMenuItem(id); } } /** * Can undo be performed? */ public boolean getCanUndo() { return (mEditHistory.mmPosition > 0); } /** * Can redo be performed? */ public boolean getCanRedo() { return (mEditHistory.mmPosition < mEditHistory.mmHistory.size()); } /** * Perform undo. */ public void undo() { EditItem edit = mEditHistory.getPrevious(); if (edit == null) { return; } Editable text = getEditableText(); int start = edit.mmStart; int end = start + (edit.mmAfter != null ? edit.mmAfter.length() : 0); mIsUndoOrRedo = true; text.replace(start, end, edit.mmBefore); mIsUndoOrRedo = false; // This will get rid of underlines inserted when editor tries to come // up with a suggestion. for (Object o : text.getSpans(0, text.length(), UnderlineSpan.class)) { text.removeSpan(o); } Selection.setSelection(text, edit.mmBefore == null ? start : (start + edit.mmBefore.length())); } /** * Perform redo. */ public void redo() { EditItem edit = mEditHistory.getNext(); if (edit == null) { return; } Editable text = getEditableText(); int start = edit.mmStart; int end = start + (edit.mmBefore != null ? edit.mmBefore.length() : 0); mIsUndoOrRedo = true; text.replace(start, end, edit.mmAfter); mIsUndoOrRedo = false; // This will get rid of underlines inserted when editor tries to come // up with a suggestion. for (Object o : text.getSpans(0, text.length(), UnderlineSpan.class)) { text.removeSpan(o); } Selection.setSelection(text, edit.mmAfter == null ? start : (start + edit.mmAfter.length())); } /** * Set the maximum history size. If size is * negative, then history size is only limited * by the device memory. */ public void setMaxHistorySize(int maxHistorySize) { mEditHistory.setMaxHistorySize(maxHistorySize); } public void resetVariables() { mEditHistory.clear(); enabledChangeListener = false; lineCount = 0; realLine = 0; startingLine = 0; mIsUndoOrRedo = false; mShowUndo = false; mShowRedo = false; canSaveFile = false; firstVisibleIndex = 0; firstColoredIndex = 0; } public boolean canSaveFile() { return canSaveFile; } public void fileSaved() { canSaveFile = false; } public void replaceTextKeepCursor(String textToUpdate) { int cursorPos; int cursorPosEnd; if (textToUpdate != null) { cursorPos = 0; cursorPosEnd = 0; } else { cursorPos = getSelectionStart(); cursorPosEnd = getSelectionEnd(); } disableTextChangedListener(); if (PreferenceHelper.getSyntaxHighlight(getContext())) { setText(highlight(textToUpdate == null ? getEditableText() : Editable.Factory.getInstance().newEditable(textToUpdate), textToUpdate != null)); } else { setText(textToUpdate == null ? getEditableText() : textToUpdate); } enableTextChangedListener(); int newCursorPos; boolean cursorOnScreen = cursorPos >= firstVisibleIndex && cursorPos <= lastVisibleIndex; if (cursorOnScreen) { // if the cursor is on screen newCursorPos = cursorPos; // we dont change its position } else { newCursorPos = firstVisibleIndex; // else we set it to the first visible pos } if (newCursorPos > -1 && newCursorPos <= length()) { if (cursorPosEnd != cursorPos) setSelection(cursorPos, cursorPosEnd); else setSelection(newCursorPos); } } //endregion //region UNDO REDO public void disableTextChangedListener() { enabledChangeListener = false; removeTextChangedListener(mChangeListener); } public Editable highlight(Editable editable, boolean newText) { editable.clearSpans(); if (editable.length() == 0) { return editable; } editorHeight = getHeight(); if (!newText && editorHeight > 0) { firstVisibleIndex = getLayout() .getLineStart(lineUtils.getFirstVisibleLine(verticalScroll, editorHeight, lineCount)); lastVisibleIndex = getLayout().getLineEnd( lineUtils.getLastVisibleLine(verticalScroll, editorHeight, lineCount, deviceHeight) - 1); } else { firstVisibleIndex = 0; lastVisibleIndex = CHARS_TO_COLOR; } firstColoredIndex = firstVisibleIndex - (CHARS_TO_COLOR / 5); // normalize if (firstColoredIndex < 0) firstColoredIndex = 0; if (lastVisibleIndex > editable.length()) lastVisibleIndex = editable.length(); if (firstColoredIndex > lastVisibleIndex) firstColoredIndex = lastVisibleIndex; textToHighlight = editable.subSequence(firstColoredIndex, lastVisibleIndex); if (TextUtils.isEmpty(fileExtension)) fileExtension = ""; if (fileExtension.contains("htm") || fileExtension.contains("xml")) { color(Patterns.HTML_TAGS, editable, textToHighlight, firstColoredIndex); color(Patterns.HTML_ATTRS, editable, textToHighlight, firstColoredIndex); color(Patterns.GENERAL_STRINGS, editable, textToHighlight, firstColoredIndex); color(Patterns.XML_COMMENTS, editable, textToHighlight, firstColoredIndex); } else if (fileExtension.equals("css")) { //color(CSS_STYLE_NAME, editable); color(Patterns.CSS_ATTRS, editable, textToHighlight, firstColoredIndex); color(Patterns.CSS_ATTR_VALUE, editable, textToHighlight, firstColoredIndex); color(Patterns.SYMBOLS, editable, textToHighlight, firstColoredIndex); color(Patterns.GENERAL_COMMENTS, editable, textToHighlight, firstColoredIndex); } else if (Arrays.asList(MimeTypes.MIME_CODE).contains(fileExtension)) { switch (fileExtension) { case "lua": color(Patterns.LUA_KEYWORDS, editable, textToHighlight, firstColoredIndex); break; case "py": color(Patterns.PY_KEYWORDS, editable, textToHighlight, firstColoredIndex); break; default: color(Patterns.GENERAL_KEYWORDS, editable, textToHighlight, firstColoredIndex); break; } color(Patterns.NUMBERS_OR_SYMBOLS, editable, textToHighlight, firstColoredIndex); color(Patterns.GENERAL_STRINGS, editable, textToHighlight, firstColoredIndex); color(Patterns.GENERAL_COMMENTS, editable, textToHighlight, firstColoredIndex); if (fileExtension.equals("php")) color(Patterns.PHP_VARIABLES, editable, textToHighlight, firstColoredIndex); } else if (Arrays.asList(MimeTypes.MIME_SQL).contains(fileExtension)) { color(Patterns.SYMBOLS, editable, textToHighlight, firstColoredIndex); color(Patterns.GENERAL_STRINGS, editable, textToHighlight, firstColoredIndex); color(Patterns.SQL_KEYWORDS, editable, textToHighlight, firstColoredIndex); } else { if (!(Arrays.asList(MimeTypes.MIME_MARKDOWN).contains(fileExtension))) color(Patterns.GENERAL_KEYWORDS, editable, textToHighlight, firstColoredIndex); color(Patterns.NUMBERS_OR_SYMBOLS, editable, textToHighlight, firstColoredIndex); color(Patterns.GENERAL_STRINGS, editable, textToHighlight, firstColoredIndex); if (fileExtension.equals("prop") || fileExtension.contains("conf") || (Arrays.asList(MimeTypes.MIME_MARKDOWN).contains(fileExtension))) color(Patterns.GENERAL_COMMENTS_NO_SLASH, editable, textToHighlight, firstColoredIndex); else color(Patterns.GENERAL_COMMENTS, editable, textToHighlight, firstColoredIndex); if ((Arrays.asList(MimeTypes.MIME_MARKDOWN).contains(fileExtension))) color(Patterns.LINK, editable, textToHighlight, firstColoredIndex); } return editable; } public void enableTextChangedListener() { if (!enabledChangeListener) { addTextChangedListener(mChangeListener); enabledChangeListener = true; } } public LineUtils getLineUtils() { return lineUtils; } private void color(Pattern pattern, Editable allText, CharSequence textToHighlight, int start) { int color = 0; if (pattern.equals(Patterns.HTML_TAGS) || pattern.equals(Patterns.GENERAL_KEYWORDS) || pattern.equals(Patterns.SQL_KEYWORDS) || pattern.equals(Patterns.PY_KEYWORDS) || pattern.equals(Patterns.LUA_KEYWORDS) ) { color = getResources().getColor(R.color.syntax_keyword); } else if (pattern.equals(Patterns.HTML_ATTRS) || pattern.equals(Patterns.CSS_ATTRS) || pattern.equals(Patterns.LINK)) { color = getResources().getColor(R.color.syntax_attr); } else if (pattern.equals(Patterns.CSS_ATTR_VALUE)) { color = getResources().getColor(R.color.syntax_attr_value); } else if (pattern.equals(Patterns.XML_COMMENTS) || pattern.equals(Patterns.GENERAL_COMMENTS) || pattern.equals(Patterns.GENERAL_COMMENTS_NO_SLASH)) { color = getResources().getColor(R.color.syntax_comment); } else if (pattern.equals(Patterns.GENERAL_STRINGS)) { color = getResources().getColor(R.color.syntax_string); } else if (pattern.equals(Patterns.NUMBERS) || pattern.equals(Patterns.SYMBOLS) || pattern.equals(Patterns.NUMBERS_OR_SYMBOLS)) { color = getResources().getColor(R.color.syntax_number); } else if (pattern.equals(Patterns.PHP_VARIABLES)) { color = getResources().getColor(R.color.syntax_variable); } m = pattern.matcher(textToHighlight); while (m.find()) { allText.setSpan(new ForegroundColorSpan(color), start + m.start(), start + m.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } } /** * Clear history. */ public void clearHistory() { mEditHistory.clear(); mShowUndo = getCanUndo(); mShowRedo = getCanRedo(); } /** * Store preferences. */ public void storePersistentState(SharedPreferences.Editor editor, String prefix) { // Store hash code of text in the editor so that we can check if the // editor contents has changed. editor.putString(prefix + ".hash", String.valueOf(getText().toString().hashCode())); editor.putInt(prefix + ".maxSize", mEditHistory.mmMaxHistorySize); editor.putInt(prefix + ".position", mEditHistory.mmPosition); editor.putInt(prefix + ".size", mEditHistory.mmHistory.size()); int i = 0; for (EditItem ei : mEditHistory.mmHistory) { String pre = prefix + "." + i; editor.putInt(pre + ".start", ei.mmStart); editor.putString(pre + ".before", ei.mmBefore.toString()); editor.putString(pre + ".after", ei.mmAfter.toString()); i++; } } /** * Restore preferences. * * @param prefix The preference key prefix * used when state was stored. * @return did restore succeed? If this is * false, the undo history will be empty. */ public boolean restorePersistentState(SharedPreferences sp, String prefix) throws IllegalStateException { boolean ok = doRestorePersistentState(sp, prefix); if (!ok) { mEditHistory.clear(); } return ok; } private boolean doRestorePersistentState(SharedPreferences sp, String prefix) { String hash = sp.getString(prefix + ".hash", null); if (hash == null) { // No state to be restored. return true; } if (Integer.valueOf(hash) != getText().toString().hashCode()) { return false; } mEditHistory.clear(); mEditHistory.mmMaxHistorySize = sp.getInt(prefix + ".maxSize", -1); int count = sp.getInt(prefix + ".size", -1); if (count == -1) { return false; } for (int i = 0; i < count; i++) { String pre = prefix + "." + i; int start = sp.getInt(pre + ".start", -1); String before = sp.getString(pre + ".before", null); String after = sp.getString(pre + ".after", null); if (start == -1 || before == null || after == null) { return false; } mEditHistory.add(new EditItem(start, before, after)); } mEditHistory.mmPosition = sp.getInt(prefix + ".position", -1); return mEditHistory.mmPosition != -1; } /** * Class that listens to changes in the text. */ private final class EditTextChangeListener implements TextWatcher { /** * The text that will be removed by the * change event. */ private CharSequence mBeforeChange; /** * The text that was inserted by the change * event. */ private CharSequence mAfterChange; public void beforeTextChanged(CharSequence s, int start, int count, int after) { if (mIsUndoOrRedo) { return; } mBeforeChange = s.subSequence(start, start + count); } public void onTextChanged(CharSequence s, int start, int before, int count) { if (mIsUndoOrRedo) { return; } mAfterChange = s.subSequence(start, start + count); mEditHistory.add(new EditItem(start, mBeforeChange, mAfterChange)); } public void afterTextChanged(Editable s) { boolean showUndo = getCanUndo(); boolean showRedo = getCanRedo(); if (!canSaveFile) canSaveFile = getCanUndo(); if (showUndo != mShowUndo || showRedo != mShowRedo) { mShowUndo = showUndo; mShowRedo = showRedo; ((MainActivity) getContext()).invalidateOptionsMenu(); } ((MainActivity) getContext()).updateTextSyntax(); } } //endregion //region EDIT HISTORY /** * Keeps track of all the edit history of a * text. */ private final class EditHistory { /** * The list of edits in chronological * order. */ private final LinkedList<EditItem> mmHistory = new LinkedList<>(); /** * The position from which an EditItem will * be retrieved when getNext() is called. If * getPrevious() has not been called, this * has the same value as mmHistory.size(). */ private int mmPosition = 0; /** * Maximum undo history size. */ private int mmMaxHistorySize = -1; private int size() { return mmHistory.size(); } /** * Clear history. */ private void clear() { mmPosition = 0; mmHistory.clear(); } /** * Adds a new edit operation to the history * at the current position. If executed * after a call to getPrevious() removes all * the future history (elements with * positions >= current history position). */ private void add(EditItem item) { while (mmHistory.size() > mmPosition) { mmHistory.removeLast(); } mmHistory.add(item); mmPosition++; if (mmMaxHistorySize >= 0) { trimHistory(); } } /** * Trim history when it exceeds max history * size. */ private void trimHistory() { while (mmHistory.size() > mmMaxHistorySize) { mmHistory.removeFirst(); mmPosition--; } if (mmPosition < 0) { mmPosition = 0; } } /** * Set the maximum history size. If size is * negative, then history size is only * limited by the device memory. */ private void setMaxHistorySize(int maxHistorySize) { mmMaxHistorySize = maxHistorySize; if (mmMaxHistorySize >= 0) { trimHistory(); } } /** * Traverses the history backward by one * position, returns and item at that * position. */ private EditItem getPrevious() { if (mmPosition == 0) { return null; } mmPosition--; return mmHistory.get(mmPosition); } /** * Traverses the history forward by one * position, returns and item at that * position. */ private EditItem getNext() { if (mmPosition >= mmHistory.size()) { return null; } EditItem item = mmHistory.get(mmPosition); mmPosition++; return item; } } /** * Represents the changes performed by a * single edit operation. */ private final class EditItem { private final int mmStart; private final CharSequence mmBefore; private final CharSequence mmAfter; /** * Constructs EditItem of a modification * that was applied at position start and * replaced CharSequence before with * CharSequence after. */ public EditItem(int start, CharSequence before, CharSequence after) { mmStart = start; mmBefore = before; mmAfter = after; } } //endregion } }