diff --git a/app/src/main/java/com/termux/app/TermuxActivity.java b/app/src/main/java/com/termux/app/TermuxActivity.java
index 3f40e57b..a1e85392 100644
--- a/app/src/main/java/com/termux/app/TermuxActivity.java
+++ b/app/src/main/java/com/termux/app/TermuxActivity.java
@@ -82,7 +82,8 @@ import java.util.regex.Pattern;
*/
public final class TermuxActivity extends Activity implements ServiceConnection {
- private static final int CONTEXTMENU_SELECT_ID = 0;
+ private static final int CONTEXTMENU_SELECT_URL_ID = 0;
+ private static final int CONTEXTMENU_SHARE_TRANSCRIPT_ID = 1;
private static final int CONTEXTMENU_PASTE_ID = 3;
private static final int CONTEXTMENU_KILL_PROCESS_ID = 4;
private static final int CONTEXTMENU_RESET_TERMINAL_ID = 5;
@@ -122,7 +123,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
*/
boolean mIsVisible;
- private SoundPool mBellSoundPool = new SoundPool.Builder().setMaxStreams(1).setAudioAttributes(
+ private final SoundPool mBellSoundPool = new SoundPool.Builder().setMaxStreams(1).setAudioAttributes(
new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION).build()).build();
private int mBellSoundId;
@@ -218,6 +219,9 @@ public final class TermuxActivity extends Activity implements ServiceConnection
getDrawer().closeDrawers();
} else if (unicodeChar == 'f'/* full screen */) {
toggleImmersive();
+ } else if (unicodeChar == 'k'/* keyboard */) {
+ InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
} else if (unicodeChar == 'm'/* menu */) {
mTerminalView.showContextMenu();
} else if (unicodeChar == 'r'/* rename */) {
@@ -255,29 +259,23 @@ public final class TermuxActivity extends Activity implements ServiceConnection
return scale;
}
- @Override
- public void onLongPress(MotionEvent event) {
- mTerminalView.showContextMenu();
- }
-
@Override
public void onSingleTapUp(MotionEvent e) {
- switch (mSettings.mTapBehaviour) {
- case TermuxPreferences.TAP_TOGGLE_KEYBOARD:
- // Toggle keyboard visibility if tapping with a finger:
- InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
- imm.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, 0);
- break;
- case TermuxPreferences.TAP_SHOW_MENU:
- mTerminalView.showContextMenu();
- break;
- }
+ InputMethodManager mgr = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+ mgr.showSoftInput(mTerminalView, InputMethodManager.SHOW_IMPLICIT);
}
@Override
public boolean shouldBackButtonBeMappedToEscape() {
return mSettings.mBackIsEscape;
}
+
+ @Override
+ public void copyModeChanged(boolean copyMode) {
+ // Disable drawer while copying.
+ getDrawer().setDrawerLockMode(copyMode ? DrawerLayout.LOCK_MODE_LOCKED_CLOSED : DrawerLayout.LOCK_MODE_UNLOCKED);
+ }
+
});
View newSessionButton = findViewById(R.id.new_session_button);
@@ -389,7 +387,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
@Override
public void onClipboardText(TerminalSession session, String text) {
if (!mIsVisible) return;
- showToast("Clipboard set:\n\"" + text + "\"", true);
+ showToast("Clipboard:\n\"" + text + "\"", false);
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
clipboard.setPrimaryClip(new ClipData(null, new String[] { "text/plain" }, new ClipData.Item(text)));
}
@@ -475,6 +473,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
TermuxInstaller.setupIfNeeded(TermuxActivity.this, new Runnable() {
@Override
public void run() {
+ if (mTermService == null) return; // Activity might have been destroyed.
try {
if (TermuxPreferences.isShowWelcomeDialog(TermuxActivity.this)) {
new AlertDialog.Builder(TermuxActivity.this).setTitle(R.string.welcome_dialog_title).setMessage(R.string.welcome_dialog_body)
@@ -623,9 +622,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
TerminalSession currentSession = getCurrentTermSession();
if (currentSession == null) return;
- ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
- menu.add(Menu.NONE, CONTEXTMENU_PASTE_ID, Menu.NONE, R.string.paste_text).setEnabled(clipboard.hasPrimaryClip());
- menu.add(Menu.NONE, CONTEXTMENU_SELECT_ID, Menu.NONE, R.string.select);
+ menu.add(Menu.NONE, CONTEXTMENU_SELECT_URL_ID, Menu.NONE, R.string.select_url);
+ menu.add(Menu.NONE, CONTEXTMENU_SHARE_TRANSCRIPT_ID, Menu.NONE, R.string.select_all_and_share);
menu.add(Menu.NONE, CONTEXTMENU_RESET_TERMINAL_ID, Menu.NONE, R.string.reset_terminal);
menu.add(Menu.NONE, CONTEXTMENU_KILL_PROCESS_ID, Menu.NONE, R.string.kill_process).setEnabled(currentSession.isRunning());
menu.add(Menu.NONE, CONTEXTMENU_TOGGLE_FULLSCREEN_ID, Menu.NONE, R.string.toggle_fullscreen).setCheckable(true).setChecked(mSettings.isFullScreen());
@@ -701,89 +699,74 @@ public final class TermuxActivity extends Activity implements ServiceConnection
@Override
public boolean onContextItemSelected(MenuItem item) {
+ TerminalSession session = getCurrentTermSession();
+
switch (item.getItemId()) {
- case CONTEXTMENU_SELECT_ID:
- CharSequence[] items = new CharSequence[] { getString(R.string.select_text), getString(R.string.select_url),
- getString(R.string.select_all_and_share) };
- new AlertDialog.Builder(this).setItems(items, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- switch (which) {
- case 0:
- mTerminalView.toggleSelectingText();
- break;
- case 1:
- showUrlSelection();
- break;
- case 2:
- TerminalSession session = getCurrentTermSession();
- if (session != null) {
- Intent intent = new Intent(Intent.ACTION_SEND);
- intent.setType("text/plain");
- intent.putExtra(Intent.EXTRA_TEXT, session.getEmulator().getScreen().getTranscriptText().trim());
- intent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.share_transcript_title));
- startActivity(Intent.createChooser(intent, getString(R.string.share_transcript_chooser_title)));
- }
- break;
+ case CONTEXTMENU_SELECT_URL_ID:
+ showUrlSelection();
+ return true;
+ case CONTEXTMENU_SHARE_TRANSCRIPT_ID:
+ if (session != null) {
+ Intent intent = new Intent(Intent.ACTION_SEND);
+ intent.setType("text/plain");
+ intent.putExtra(Intent.EXTRA_TEXT, session.getEmulator().getScreen().getTranscriptText().trim());
+ intent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.share_transcript_title));
+ startActivity(Intent.createChooser(intent, getString(R.string.share_transcript_chooser_title)));
+ }
+ return true;
+ case CONTEXTMENU_PASTE_ID:
+ doPaste();
+ return true;
+ case CONTEXTMENU_KILL_PROCESS_ID:
+ final AlertDialog.Builder b = new AlertDialog.Builder(this);
+ b.setIcon(android.R.drawable.ic_dialog_alert);
+ b.setMessage(R.string.confirm_kill_process);
+ b.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.dismiss();
+ getCurrentTermSession().finishIfRunning();
}
- dialog.dismiss();
+ });
+ b.setNegativeButton(android.R.string.no, null);
+ b.show();
+ return true;
+ case CONTEXTMENU_RESET_TERMINAL_ID: {
+ if (session != null) {
+ session.reset();
+ showToast(getResources().getString(R.string.reset_toast_notification), true);
}
- }).show();
- return true;
- case CONTEXTMENU_PASTE_ID:
- doPaste();
- return true;
- case CONTEXTMENU_KILL_PROCESS_ID:
- final AlertDialog.Builder b = new AlertDialog.Builder(this);
- b.setIcon(android.R.drawable.ic_dialog_alert);
- b.setMessage(R.string.confirm_kill_process);
- b.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int id) {
- dialog.dismiss();
- getCurrentTermSession().finishIfRunning();
+ return true;
+ }
+ case CONTEXTMENU_STYLING_ID: {
+ Intent stylingIntent = new Intent();
+ stylingIntent.setClassName("com.termux.styling", "com.termux.styling.TermuxStyleActivity");
+ try {
+ startActivity(stylingIntent);
+ } catch (ActivityNotFoundException e) {
+ new AlertDialog.Builder(this).setMessage(R.string.styling_not_installed)
+ .setPositiveButton(R.string.styling_install, new android.content.DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://play.google.com/store/apps/details?id=com.termux.styling")));
+ }
+ }).setNegativeButton(android.R.string.cancel, null).show();
}
- });
- b.setNegativeButton(android.R.string.no, null);
- b.show();
- return true;
- case CONTEXTMENU_RESET_TERMINAL_ID: {
- TerminalSession session = getCurrentTermSession();
- if (session != null) {
- session.reset();
- showToast(getResources().getString(R.string.reset_toast_notification), true);
}
return true;
- }
- case CONTEXTMENU_STYLING_ID: {
- Intent stylingIntent = new Intent();
- stylingIntent.setClassName("com.termux.styling", "com.termux.styling.TermuxStyleActivity");
- try {
- startActivity(stylingIntent);
- } catch (ActivityNotFoundException e) {
- new AlertDialog.Builder(this).setMessage(R.string.styling_not_installed)
- .setPositiveButton(R.string.styling_install, new android.content.DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://play.google.com/store/apps/details?id=com.termux.styling")));
- }
- }).setNegativeButton(android.R.string.cancel, null).show();
- }
- }
- return true;
- case CONTEXTMENU_TOGGLE_FULLSCREEN_ID:
- toggleImmersive();
- return true;
- case CONTEXTMENU_HELP_ID:
- startActivity(new Intent(this, TermuxHelpActivity.class));
- return true;
- default:
- return super.onContextItemSelected(item);
+ case CONTEXTMENU_TOGGLE_FULLSCREEN_ID:
+ toggleImmersive();
+ return true;
+ case CONTEXTMENU_HELP_ID:
+ startActivity(new Intent(this, TermuxHelpActivity.class));
+ return true;
+ default:
+ return super.onContextItemSelected(item);
}
}
@Override
- public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
+ public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
if (requestCode == REQUESTCODE_PERMISSION_STORAGE && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
TermuxInstaller.setupStorageSymlinks(this);
}
diff --git a/app/src/main/java/com/termux/app/TermuxPreferences.java b/app/src/main/java/com/termux/app/TermuxPreferences.java
index be85f7a1..39fb3c6e 100644
--- a/app/src/main/java/com/termux/app/TermuxPreferences.java
+++ b/app/src/main/java/com/termux/app/TermuxPreferences.java
@@ -1,7 +1,5 @@
package com.termux.app;
-import com.termux.terminal.TerminalSession;
-
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
@@ -10,6 +8,8 @@ import android.util.Log;
import android.util.TypedValue;
import android.widget.Toast;
+import com.termux.terminal.TerminalSession;
+
import java.io.File;
import java.io.FileInputStream;
import java.lang.annotation.Retention;
@@ -26,14 +26,6 @@ final class TermuxPreferences {
static final int BELL_BEEP = 2;
static final int BELL_IGNORE = 3;
- @IntDef({TAP_TOGGLE_KEYBOARD, TAP_SHOW_MENU, TAP_IGNORE})
- @Retention(RetentionPolicy.SOURCE)
- public @interface TapTerminalBehaviour {}
-
- static final int TAP_TOGGLE_KEYBOARD = 1;
- static final int TAP_SHOW_MENU = 2;
- static final int TAP_IGNORE = 3;
-
private final int MIN_FONTSIZE;
private static final int MAX_FONTSIZE = 256;
@@ -48,9 +40,6 @@ final class TermuxPreferences {
@AsciiBellBehaviour
int mBellBehaviour = BELL_VIBRATE;
- @TapTerminalBehaviour
- int mTapBehaviour = TAP_TOGGLE_KEYBOARD;
-
boolean mBackIsEscape = true;
TermuxPreferences(Context context) {
@@ -124,42 +113,26 @@ final class TermuxPreferences {
public void reloadFromProperties(Context context) {
try {
File propsFile = new File(TermuxService.HOME_PATH + "/.config/termux/termux.properties");
+ Properties props = new Properties();
if (propsFile.isFile() && propsFile.canRead()) {
- Properties props = new Properties();
try (FileInputStream in = new FileInputStream(propsFile)) {
props.load(in);
}
-
- switch (props.getProperty("bell-character", "vibrate")) {
- case "beep":
- mBellBehaviour = BELL_BEEP;
- break;
- case "ignore":
- mBellBehaviour = BELL_IGNORE;
- break;
- default: // "vibrate".
- mBellBehaviour = BELL_VIBRATE;
- break;
- }
-
- switch (props.getProperty("tap-screen", "toggle-keyboard")) {
- case "show-menu":
- mTapBehaviour = TAP_SHOW_MENU;
- break;
- case "ignore":
- mTapBehaviour = TAP_IGNORE;
- break;
- default: // "toggle-keyboard".
- mTapBehaviour = TAP_TOGGLE_KEYBOARD;
- break;
- }
-
- mBackIsEscape = !"back".equals(props.getProperty("back-key", "escape"));
- } else {
- mBellBehaviour = BELL_VIBRATE;
- mTapBehaviour = TAP_TOGGLE_KEYBOARD;
- mBackIsEscape = true;
}
+
+ switch (props.getProperty("bell-character", "vibrate")) {
+ case "beep":
+ mBellBehaviour = BELL_BEEP;
+ break;
+ case "ignore":
+ mBellBehaviour = BELL_IGNORE;
+ break;
+ default: // "vibrate".
+ mBellBehaviour = BELL_VIBRATE;
+ break;
+ }
+
+ mBackIsEscape = "escape".equals(props.getProperty("back-key", "escape"));
} catch (Exception e) {
Toast.makeText(context, "Error loading properties: " + e.getMessage(), Toast.LENGTH_LONG).show();
Log.e("termux", "Error loading props", e);
diff --git a/app/src/main/java/com/termux/terminal/TerminalBuffer.java b/app/src/main/java/com/termux/terminal/TerminalBuffer.java
index 2305f0e3..03460238 100644
--- a/app/src/main/java/com/termux/terminal/TerminalBuffer.java
+++ b/app/src/main/java/com/termux/terminal/TerminalBuffer.java
@@ -61,6 +61,10 @@ public final class TerminalBuffer {
TerminalRow lineObject = mLines[externalToInternalRow(row)];
int x1Index = lineObject.findStartOfColumn(x1);
int x2Index = (x2 < mColumns) ? lineObject.findStartOfColumn(x2) : lineObject.getSpaceUsed();
+ if (x2Index == x1Index) {
+ // Selected the start of a wide character.
+ x2Index = lineObject.findStartOfColumn(x2+1);
+ }
char[] line = lineObject.mText;
int lastPrintingCharIndex = -1;
int i;
@@ -71,7 +75,7 @@ public final class TerminalBuffer {
} else {
for (i = x1Index; i < x2Index; ++i) {
char c = line[i];
- if (c != ' ' && !Character.isLowSurrogate(c)) lastPrintingCharIndex = i;
+ if (c != ' ') lastPrintingCharIndex = i;
}
}
if (lastPrintingCharIndex != -1) builder.append(line, x1Index, lastPrintingCharIndex - x1Index + 1);
diff --git a/app/src/main/java/com/termux/terminal/TerminalRow.java b/app/src/main/java/com/termux/terminal/TerminalRow.java
index 0730938b..a1cdd2bd 100644
--- a/app/src/main/java/com/termux/terminal/TerminalRow.java
+++ b/app/src/main/java/com/termux/terminal/TerminalRow.java
@@ -184,6 +184,7 @@ public final class TerminalRow {
mSpaceUsed += javaCharDifference;
// Store char. A combining character is stored at the end of the existing contents so that it modifies them:
+ //noinspection ResultOfMethodCallIgnored - since we already now how many java chars is used.
Character.toChars(codePoint, text, oldStartOfColumnIndex + (newIsCombining ? oldCharactersUsedForColumn : 0));
if (oldCodePointDisplayWidth == 2 && newCodePointDisplayWidth == 1) {
diff --git a/app/src/main/java/com/termux/view/TerminalKeyListener.java b/app/src/main/java/com/termux/view/TerminalKeyListener.java
index 83109b86..15350749 100644
--- a/app/src/main/java/com/termux/view/TerminalKeyListener.java
+++ b/app/src/main/java/com/termux/view/TerminalKeyListener.java
@@ -14,11 +14,11 @@ public interface TerminalKeyListener {
/** Callback function on scale events according to {@link ScaleGestureDetector#getScaleFactor()}. */
float onScale(float scale);
- void onLongPress(MotionEvent e);
-
/** On a single tap on the terminal if terminal mouse reporting not enabled. */
void onSingleTapUp(MotionEvent e);
boolean shouldBackButtonBeMappedToEscape();
+ void copyModeChanged(boolean copyMode);
+
}
diff --git a/app/src/main/java/com/termux/view/TerminalView.java b/app/src/main/java/com/termux/view/TerminalView.java
index d9e04856..87ca6a04 100644
--- a/app/src/main/java/com/termux/view/TerminalView.java
+++ b/app/src/main/java/com/termux/view/TerminalView.java
@@ -1,17 +1,26 @@
package com.termux.view;
import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.content.ClipData;
+import android.content.ClipboardManager;
import android.content.Context;
import android.graphics.Canvas;
+import android.graphics.Rect;
import android.graphics.Typeface;
+import android.graphics.drawable.BitmapDrawable;
+import android.os.Build;
import android.text.InputType;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
+import android.view.ActionMode;
import android.view.HapticFeedbackConstants;
import android.view.InputDevice;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.inputmethod.BaseInputConnection;
@@ -19,8 +28,10 @@ import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.widget.Scroller;
+import com.termux.R;
import com.termux.terminal.EmulatorDebug;
import com.termux.terminal.KeyHandler;
+import com.termux.terminal.TerminalBuffer;
import com.termux.terminal.TerminalColors;
import com.termux.terminal.TerminalEmulator;
import com.termux.terminal.TerminalSession;
@@ -34,7 +45,7 @@ import java.util.Properties;
public final class TerminalView extends View {
/** Log view key and IME events. */
- private static final boolean LOG_KEY_EVENTS = false;
+ private static final boolean LOG_KEY_EVENTS = true;
/** The currently displayed terminal session, whose emulator is {@link #mEmulator}. */
TerminalSession mTermSession;
@@ -51,9 +62,11 @@ public final class TerminalView extends View {
/** Keeping track of the special keys acting as Ctrl and Fn for the soft keyboard and other hardware keys. */
boolean mVirtualControlKeyDown, mVirtualFnKeyDown;
- boolean mIsSelectingText = false;
- int mSelXAnchor = -1, mSelYAnchor = -1;
+ boolean mIsSelectingText = false, mIsDraggingLeftSelection, mInitialTextSelection;
int mSelX1 = -1, mSelX2 = -1, mSelY1 = -1, mSelY2 = -1;
+ float mSelectionDownX, mSelectionDownY;
+ private ActionMode mActionMode;
+ private BitmapDrawable mLeftSelectionHandle, mRightSelectionHandle;
float mScaleFactor = 1.f;
final GestureAndScaleRecognizer mGestureRecognizer;
@@ -78,7 +91,7 @@ public final class TerminalView extends View {
@Override
public boolean onUp(MotionEvent e) {
mScrollRemainder = 0.0f;
- if (mEmulator != null && mEmulator.isMouseTrackingActive()) {
+ if (mEmulator != null && mEmulator.isMouseTrackingActive() && !mIsSelectingText) {
// Quick event processing when mouse tracking is active - do not wait for check of double tapping
// for zooming.
sendMouseEventCode(e, TerminalEmulator.MOUSE_LEFT_BUTTON, true);
@@ -91,6 +104,7 @@ public final class TerminalView extends View {
@Override
public boolean onSingleTapUp(MotionEvent e) {
if (mEmulator == null) return true;
+ if (mIsSelectingText) { toggleSelectingText(null); return true; }
requestFocus();
if (!mEmulator.isMouseTrackingActive()) {
if (!e.isFromSource(InputDevice.SOURCE_MOUSE)) {
@@ -103,7 +117,8 @@ public final class TerminalView extends View {
@Override
public boolean onScroll(MotionEvent e2, float distanceX, float distanceY) {
- if (mEmulator == null) return true;
+ Log.e("termux", "onScroll=" + e2 + ", mIsselection=" + mIsSelectingText + ", mouse=" + e2.isFromSource(InputDevice.SOURCE_MOUSE));
+ if (mEmulator == null || mIsSelectingText) return true;
if (mEmulator.isMouseTrackingActive() && e2.isFromSource(InputDevice.SOURCE_MOUSE)) {
// If moving with mouse pointer while pressing button, report that instead of scroll.
// This means that we never report moving with button press-events for touch input,
@@ -128,7 +143,7 @@ public final class TerminalView extends View {
@Override
public boolean onFling(final MotionEvent e2, float velocityX, float velocityY) {
- if (mEmulator == null) return true;
+ if (mEmulator == null || mIsSelectingText) return true;
// Do not start scrolling until last fling has been taken care of:
if (!mScroller.isFinished()) return true;
@@ -175,9 +190,9 @@ public final class TerminalView extends View {
@Override
public void onLongPress(MotionEvent e) {
- if (mEmulator != null && !mGestureRecognizer.isInProgress()) {
+ if (!mGestureRecognizer.isInProgress()) {
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
- mOnKeyListener.onLongPress(e);
+ toggleSelectingText(e);
}
}
});
@@ -228,7 +243,7 @@ public final class TerminalView extends View {
//
// So a bit messy. If this gets too messy it's perhaps best resolved by reverting back to just
// "TYPE_NULL" and let the Pinyin Input english keyboard be in word mode.
- outAttrs.inputType = InputType.TYPE_NULL | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD;
+ outAttrs.inputType = InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD;
// Let part of the application show behind when in landscape:
outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_FULLSCREEN;
@@ -339,7 +354,6 @@ public final class TerminalView extends View {
int rowShift = mEmulator.getScrollCounter();
mSelY1 -= rowShift;
mSelY2 -= rowShift;
- mSelYAnchor -= rowShift;
}
mEmulator.clearScrollCounter();
@@ -422,66 +436,91 @@ public final class TerminalView extends View {
@SuppressLint("ClickableViewAccessibility")
@Override
+ @TargetApi(23)
public boolean onTouchEvent(MotionEvent ev) {
if (mEmulator == null) return true;
- final boolean eventFromMouse = ev.isFromSource(InputDevice.SOURCE_MOUSE);
final int action = ev.getAction();
- if (eventFromMouse) {
- if ((ev.getButtonState() & MotionEvent.BUTTON_SECONDARY) != 0) {
- if (action == MotionEvent.ACTION_DOWN) showContextMenu();
- return true;
- } else if (mEmulator.isMouseTrackingActive() && (ev.getAction() == MotionEvent.ACTION_DOWN || ev.getAction() == MotionEvent.ACTION_UP)) {
- sendMouseEventCode(ev, TerminalEmulator.MOUSE_LEFT_BUTTON, ev.getAction() == MotionEvent.ACTION_DOWN);
- return true;
- } else if (!mEmulator.isMouseTrackingActive() && action == MotionEvent.ACTION_DOWN) {
- // Start text selection with mouse. Note that the check against MotionEvent.ACTION_DOWN is
- // important, since we otherwise would pick up secondary mouse button up actions.
- mIsSelectingText = true;
- }
- } else if (!mIsSelectingText) {
- mGestureRecognizer.onTouchEvent(ev);
- return true;
- }
-
if (mIsSelectingText) {
+ int cy = (int) (ev.getY() / mRenderer.mFontLineSpacing) + mTopRow;
int cx = (int) (ev.getX() / mRenderer.mFontWidth);
- // Offset for finger:
- final int SELECT_TEXT_OFFSET_Y = eventFromMouse ? 0 : -40;
- int cy = (int) ((ev.getY() + SELECT_TEXT_OFFSET_Y) / mRenderer.mFontLineSpacing) + mTopRow;
+
switch (action) {
+ case MotionEvent.ACTION_UP:
+ mInitialTextSelection = false;
+ break;
case MotionEvent.ACTION_DOWN:
- mSelXAnchor = cx;
- mSelYAnchor = cy;
- mSelX1 = cx;
- mSelY1 = cy;
- mSelX2 = mSelX1;
- mSelY2 = mSelY1;
- invalidate();
+ int distanceFromSel1 = Math.abs(cx-mSelX1) + Math.abs(cy-mSelY1);
+ int distanceFromSel2 = Math.abs(cx-mSelX2) + Math.abs(cy-mSelY2);
+ mIsDraggingLeftSelection = distanceFromSel1 <= distanceFromSel2;
+ mSelectionDownX = ev.getX();
+ mSelectionDownY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
- case MotionEvent.ACTION_UP:
- boolean touchBeforeAnchor = (cy < mSelYAnchor || (cy == mSelYAnchor && cx < mSelXAnchor));
- int minx = touchBeforeAnchor ? cx : mSelXAnchor;
- int maxx = !touchBeforeAnchor ? cx : mSelXAnchor;
- int miny = touchBeforeAnchor ? cy : mSelYAnchor;
- int maxy = !touchBeforeAnchor ? cy : mSelYAnchor;
- mSelX1 = minx;
- mSelY1 = miny;
- mSelX2 = maxx;
- mSelY2 = maxy;
- if (action == MotionEvent.ACTION_UP) {
- String selectedText = mEmulator.getSelectedText(mSelX1, mSelY1, mSelX2, mSelY2).trim();
- mTermSession.clipboardText(selectedText);
- toggleSelectingText();
+ if (mInitialTextSelection) break;
+ float deltaX = ev.getX() - mSelectionDownX;
+ float deltaY = ev.getY() - mSelectionDownY;
+ int deltaCols = (int) Math.ceil(deltaX / mRenderer.mFontWidth);
+ int deltaRows = (int) Math.ceil(deltaY / mRenderer.mFontLineSpacing);
+ mSelectionDownX += deltaCols * mRenderer.mFontWidth;
+ mSelectionDownY += deltaRows * mRenderer.mFontLineSpacing;
+ if (mIsDraggingLeftSelection) {
+ mSelX1 += deltaCols;
+ mSelY1 += deltaRows;
+ } else {
+ mSelX2 += deltaCols;
+ mSelY2 += deltaRows;
}
+
+ mSelX1 = Math.min(mEmulator.mColumns, Math.max(0, mSelX1));
+ mSelX2 = Math.min(mEmulator.mColumns, Math.max(0, mSelX2));
+
+ if (mSelY1 == mSelY2 && mSelX1 > mSelX2 || mSelY1 > mSelY2) {
+ // Switch handles.
+ mIsDraggingLeftSelection = !mIsDraggingLeftSelection;
+ int tmpX1 = mSelX1, tmpY1 = mSelY1;
+ mSelX1 = mSelX2; mSelY1 = mSelY2;
+ mSelX2 = tmpX1; mSelY2 = tmpY1;
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) mActionMode.invalidateContentRect();
invalidate();
break;
default:
- toggleSelectingText();
- invalidate();
break;
}
+ mGestureRecognizer.onTouchEvent(ev);
+ return true;
+ } else if (ev.isFromSource(InputDevice.SOURCE_MOUSE)) {
+ if (ev.isButtonPressed(MotionEvent.BUTTON_SECONDARY)) {
+ if (action == MotionEvent.ACTION_DOWN) showContextMenu();
+ return true;
+ } else if (ev.isButtonPressed(MotionEvent.BUTTON_TERTIARY)) {
+ ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
+ ClipData clipData = clipboard.getPrimaryClip();
+ if (clipData != null) {
+ CharSequence paste = clipData.getItemAt(0).coerceToText(getContext());
+ if (!TextUtils.isEmpty(paste)) mEmulator.paste(paste.toString());
+ }
+ } else if (mEmulator.isMouseTrackingActive()) { // BUTTON_PRIMARY.
+ switch (ev.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ case MotionEvent.ACTION_UP:
+ sendMouseEventCode(ev, TerminalEmulator.MOUSE_LEFT_BUTTON, ev.getAction() == MotionEvent.ACTION_DOWN);
+ break;
+ case MotionEvent.ACTION_MOVE:
+ sendMouseEventCode(ev, TerminalEmulator.MOUSE_LEFT_BUTTON_MOVED, true);
+ break;
+ }
+ return true;
+ } else if (action == MotionEvent.ACTION_DOWN) {
+ // Start text selection with mouse. Note that the check against MotionEvent.ACTION_DOWN is
+ // important, since we otherwise would pick up secondary mouse button up actions.
+ toggleSelectingText(ev);
+ return true;
+ }
+ } else {
+ mGestureRecognizer.onTouchEvent(ev);
return true;
}
@@ -491,13 +530,18 @@ public final class TerminalView extends View {
@Override
public boolean onKeyPreIme(int keyCode, KeyEvent event) {
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "onKeyPreIme(keyCode=" + keyCode + ", event=" + event + ")");
- if (keyCode == KeyEvent.KEYCODE_BACK && mOnKeyListener.shouldBackButtonBeMappedToEscape()) {
- // Intercept back button to treat it as escape:
- switch (event.getAction()) {
- case KeyEvent.ACTION_DOWN:
- return onKeyDown(keyCode, event);
- case KeyEvent.ACTION_UP:
- return onKeyUp(keyCode, event);
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ if (mIsSelectingText) {
+ toggleSelectingText(null);
+ return true;
+ } else if (mOnKeyListener.shouldBackButtonBeMappedToEscape()) {
+ // Intercept back button to treat it as escape:
+ switch (event.getAction()) {
+ case KeyEvent.ACTION_DOWN:
+ return onKeyDown(keyCode, event);
+ case KeyEvent.ACTION_UP:
+ return onKeyUp(keyCode, event);
+ }
}
}
return super.onKeyPreIme(keyCode, event);
@@ -776,13 +820,147 @@ public final class TerminalView extends View {
canvas.drawColor(0XFF000000);
} else {
mRenderer.render(mEmulator, canvas, mTopRow, mSelY1, mSelY2, mSelX1, mSelX2);
+
+ if (mIsSelectingText) {
+ final int gripHandleWidth = mLeftSelectionHandle.getIntrinsicWidth();
+ final int gripHandleMargin = gripHandleWidth / 4; // See the png.
+
+ int right = Math.round((mSelX1) * mRenderer.mFontWidth) + gripHandleMargin;
+ int top = (mSelY1+1 - mTopRow)*mRenderer.mFontLineSpacing + mRenderer.mFontLineSpacingAndAscent;
+ mLeftSelectionHandle.setBounds(right - gripHandleWidth, top, right, top + mLeftSelectionHandle.getIntrinsicHeight());
+ mLeftSelectionHandle.draw(canvas);
+
+ int left = Math.round((mSelX2+1)*mRenderer.mFontWidth) - gripHandleMargin;
+ top = (mSelY2+1 - mTopRow) *mRenderer.mFontLineSpacing + mRenderer.mFontLineSpacingAndAscent;
+ mRightSelectionHandle.setBounds(left, top, left + gripHandleWidth, top + mRightSelectionHandle.getIntrinsicHeight());
+ mRightSelectionHandle.draw(canvas);
+ }
}
}
/** Toggle text selection mode in the view. */
- public void toggleSelectingText() {
+ @TargetApi(23)
+ public void toggleSelectingText(MotionEvent ev) {
mIsSelectingText = !mIsSelectingText;
- if (!mIsSelectingText) mSelX1 = mSelY1 = mSelX2 = mSelY2 = -1;
+ mOnKeyListener.copyModeChanged(mIsSelectingText);
+
+ if (mIsSelectingText) {
+ if (mLeftSelectionHandle == null) {
+ mLeftSelectionHandle = (BitmapDrawable) getContext().getDrawable(R.drawable.text_select_handle_left_material);
+ mRightSelectionHandle = (BitmapDrawable) getContext().getDrawable(R.drawable.text_select_handle_right_material);
+ }
+
+ int cx = (int) (ev.getX() / mRenderer.mFontWidth);
+ final boolean eventFromMouse = ev.isFromSource(InputDevice.SOURCE_MOUSE);
+ // Offset for finger:
+ final int SELECT_TEXT_OFFSET_Y = eventFromMouse ? 0 : -40;
+ int cy = (int) ((ev.getY() + SELECT_TEXT_OFFSET_Y) / mRenderer.mFontLineSpacing) + mTopRow;
+
+ mSelX1 = mSelX2 = cx;
+ mSelY1 = mSelY2 = cy;
+
+ TerminalBuffer screen = mEmulator.getScreen();
+ if (!" ".equals(screen.getSelectedText(mSelX1, mSelY1, mSelX1, mSelY1))) {
+ // Selecting something other than whitespace. Expand to word.
+ while (mSelX1 > 0 && !"".equals(screen.getSelectedText(mSelX1-1, mSelY1, mSelX1-1, mSelY1))) {
+ mSelX1--;
+ }
+ while (mSelX2 < mEmulator.mColumns-1 && !"".equals(screen.getSelectedText(mSelX2+1, mSelY1, mSelX2+1, mSelY1))) {
+ mSelX2++;
+ }
+ }
+
+ mInitialTextSelection = true;
+ mIsDraggingLeftSelection = true;
+ mSelectionDownX = ev.getX();
+ mSelectionDownY = ev.getY();
+
+ final ActionMode.Callback callback = new ActionMode.Callback() {
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
+ menu.add(Menu.NONE, 1, Menu.NONE, R.string.copy_text);
+ menu.add(Menu.NONE, 2, Menu.NONE, R.string.paste_text).setEnabled(clipboard.hasPrimaryClip());
+ menu.add(Menu.NONE, 3, Menu.NONE, R.string.text_selection_more);
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ return false;
+ }
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ switch (item.getItemId()) {
+ case 1:
+ String selectedText = mEmulator.getSelectedText(mSelX1, mSelY1, mSelX2, mSelY2).trim();
+ mTermSession.clipboardText(selectedText);
+ break;
+ case 2:
+ ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
+ ClipData clipData = clipboard.getPrimaryClip();
+ if (clipData != null) {
+ CharSequence paste = clipData.getItemAt(0).coerceToText(getContext());
+ if (!TextUtils.isEmpty(paste)) mEmulator.paste(paste.toString());
+ }
+ break;
+ case 3:
+ showContextMenu();
+ break;
+ }
+ toggleSelectingText(null);
+ return true;
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ }
+
+ };
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ mActionMode = startActionMode(new ActionMode.Callback2() {
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ return callback.onCreateActionMode(mode, menu);
+ }
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ return false;
+ }
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ return callback.onActionItemClicked(mode, item);
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ // Ignore.
+ }
+
+ @Override
+ public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
+ int x1 = Math.round(mSelX1 * mRenderer.mFontWidth);
+ int x2 = Math.round(mSelX2 * mRenderer.mFontWidth);
+ int y1 = Math.round((mSelY1 - mTopRow) * mRenderer.mFontLineSpacing);
+ int y2 = Math.round((mSelY2 + 1 - mTopRow) * mRenderer.mFontLineSpacing);
+ outRect.set(Math.min(x1, x2), y1, Math.max(x1, x2), y2);
+ }
+ }, ActionMode.TYPE_FLOATING);
+ } else {
+ mActionMode = startActionMode(callback);
+ }
+
+
+ invalidate();
+ } else {
+ mActionMode.finish();
+ mSelX1 = mSelY1 = mSelX2 = mSelY2 = -1;
+ invalidate();
+ }
}
public TerminalSession getCurrentSession() {
diff --git a/app/src/main/res/drawable-xxhdpi/text_select_handle_left_mtrl_alpha.png b/app/src/main/res/drawable-xxhdpi/text_select_handle_left_mtrl_alpha.png
new file mode 100644
index 00000000..39818db8
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/text_select_handle_left_mtrl_alpha.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/text_select_handle_right_mtrl_alpha.png b/app/src/main/res/drawable-xxhdpi/text_select_handle_right_mtrl_alpha.png
new file mode 100644
index 00000000..260e090b
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/text_select_handle_right_mtrl_alpha.png differ
diff --git a/app/src/main/res/drawable/text_select_handle_left_material.xml b/app/src/main/res/drawable/text_select_handle_left_material.xml
new file mode 100644
index 00000000..89733d5d
--- /dev/null
+++ b/app/src/main/res/drawable/text_select_handle_left_material.xml
@@ -0,0 +1,4 @@
+
+
diff --git a/app/src/main/res/drawable/text_select_handle_right_material.xml b/app/src/main/res/drawable/text_select_handle_right_material.xml
new file mode 100644
index 00000000..21a2cb87
--- /dev/null
+++ b/app/src/main/res/drawable/text_select_handle_right_material.xml
@@ -0,0 +1,4 @@
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index f3f55662..cb505616 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -29,18 +29,18 @@
Terminal reset.
- Select…
- Select text
Select URL
Click URL to copy or long press to open
- Select all text and share
+ Share transcript
No URL found in the terminal.
URL copied to clipboard
Send text to:
Paste
- Hangup
+ Copy
+ More…
+ Hangup
Close this process?
Set session name