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