From 862b461a07d64e3a645b1b1bba274a9159fab35f Mon Sep 17 00:00:00 2001 From: Fredrik Fornwall Date: Wed, 13 Jan 2016 03:00:21 +0100 Subject: [PATCH] Improve text selection functionality - Make text selection easier and quicker by selecting text directly on long press, and using standard grip bars for changing the selection. - Disable the drawer while selecting text. - Fix problem with selecting snippets of text with wide unicode characters at start and end. - Remove the "tap-screen" configuration option for a more common show keyboard behaviour when tapping the terminal. - Do no longer map the back key to escape by default - but it's still possible to do by configuration. - Add new hardware keyboard shortcut Ctrl+Shift+K for toggling soft keyboard visibility. --- .../java/com/termux/app/TermuxActivity.java | 171 +++++----- .../com/termux/app/TermuxPreferences.java | 61 +--- .../com/termux/terminal/TerminalBuffer.java | 6 +- .../java/com/termux/terminal/TerminalRow.java | 1 + .../com/termux/view/TerminalKeyListener.java | 4 +- .../java/com/termux/view/TerminalView.java | 304 ++++++++++++++---- .../text_select_handle_left_mtrl_alpha.png | Bin 0 -> 2032 bytes .../text_select_handle_right_mtrl_alpha.png | Bin 0 -> 15200 bytes .../text_select_handle_left_material.xml | 4 + .../text_select_handle_right_material.xml | 4 + app/src/main/res/values/strings.xml | 8 +- 11 files changed, 355 insertions(+), 208 deletions(-) create mode 100644 app/src/main/res/drawable-xxhdpi/text_select_handle_left_mtrl_alpha.png create mode 100644 app/src/main/res/drawable-xxhdpi/text_select_handle_right_mtrl_alpha.png create mode 100644 app/src/main/res/drawable/text_select_handle_left_material.xml create mode 100644 app/src/main/res/drawable/text_select_handle_right_material.xml 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 0000000000000000000000000000000000000000..39818db88613816bb4d17a85de435af254937567 GIT binary patch literal 2032 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 PyaQ{ksJ0B68@K)!N!r*i literal 0 HcmV?d00001 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