mirror of
https://github.com/fankes/termux-app.git
synced 2025-09-07 11:09:49 +08:00
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.
This commit is contained in:
@@ -82,7 +82,8 @@ import java.util.regex.Pattern;
|
|||||||
*/
|
*/
|
||||||
public final class TermuxActivity extends Activity implements ServiceConnection {
|
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_PASTE_ID = 3;
|
||||||
private static final int CONTEXTMENU_KILL_PROCESS_ID = 4;
|
private static final int CONTEXTMENU_KILL_PROCESS_ID = 4;
|
||||||
private static final int CONTEXTMENU_RESET_TERMINAL_ID = 5;
|
private static final int CONTEXTMENU_RESET_TERMINAL_ID = 5;
|
||||||
@@ -122,7 +123,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
*/
|
*/
|
||||||
boolean mIsVisible;
|
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)
|
new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||||
.setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION).build()).build();
|
.setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION).build()).build();
|
||||||
private int mBellSoundId;
|
private int mBellSoundId;
|
||||||
@@ -218,6 +219,9 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
getDrawer().closeDrawers();
|
getDrawer().closeDrawers();
|
||||||
} else if (unicodeChar == 'f'/* full screen */) {
|
} else if (unicodeChar == 'f'/* full screen */) {
|
||||||
toggleImmersive();
|
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 */) {
|
} else if (unicodeChar == 'm'/* menu */) {
|
||||||
mTerminalView.showContextMenu();
|
mTerminalView.showContextMenu();
|
||||||
} else if (unicodeChar == 'r'/* rename */) {
|
} else if (unicodeChar == 'r'/* rename */) {
|
||||||
@@ -255,29 +259,23 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
return scale;
|
return scale;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLongPress(MotionEvent event) {
|
|
||||||
mTerminalView.showContextMenu();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSingleTapUp(MotionEvent e) {
|
public void onSingleTapUp(MotionEvent e) {
|
||||||
switch (mSettings.mTapBehaviour) {
|
InputMethodManager mgr = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||||
case TermuxPreferences.TAP_TOGGLE_KEYBOARD:
|
mgr.showSoftInput(mTerminalView, InputMethodManager.SHOW_IMPLICIT);
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean shouldBackButtonBeMappedToEscape() {
|
public boolean shouldBackButtonBeMappedToEscape() {
|
||||||
return mSettings.mBackIsEscape;
|
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);
|
View newSessionButton = findViewById(R.id.new_session_button);
|
||||||
@@ -389,7 +387,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
@Override
|
@Override
|
||||||
public void onClipboardText(TerminalSession session, String text) {
|
public void onClipboardText(TerminalSession session, String text) {
|
||||||
if (!mIsVisible) return;
|
if (!mIsVisible) return;
|
||||||
showToast("Clipboard set:\n\"" + text + "\"", true);
|
showToast("Clipboard:\n\"" + text + "\"", false);
|
||||||
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
||||||
clipboard.setPrimaryClip(new ClipData(null, new String[] { "text/plain" }, new ClipData.Item(text)));
|
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() {
|
TermuxInstaller.setupIfNeeded(TermuxActivity.this, new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
|
if (mTermService == null) return; // Activity might have been destroyed.
|
||||||
try {
|
try {
|
||||||
if (TermuxPreferences.isShowWelcomeDialog(TermuxActivity.this)) {
|
if (TermuxPreferences.isShowWelcomeDialog(TermuxActivity.this)) {
|
||||||
new AlertDialog.Builder(TermuxActivity.this).setTitle(R.string.welcome_dialog_title).setMessage(R.string.welcome_dialog_body)
|
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();
|
TerminalSession currentSession = getCurrentTermSession();
|
||||||
if (currentSession == null) return;
|
if (currentSession == null) return;
|
||||||
|
|
||||||
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
menu.add(Menu.NONE, CONTEXTMENU_SELECT_URL_ID, Menu.NONE, R.string.select_url);
|
||||||
menu.add(Menu.NONE, CONTEXTMENU_PASTE_ID, Menu.NONE, R.string.paste_text).setEnabled(clipboard.hasPrimaryClip());
|
menu.add(Menu.NONE, CONTEXTMENU_SHARE_TRANSCRIPT_ID, Menu.NONE, R.string.select_all_and_share);
|
||||||
menu.add(Menu.NONE, CONTEXTMENU_SELECT_ID, Menu.NONE, R.string.select);
|
|
||||||
menu.add(Menu.NONE, CONTEXTMENU_RESET_TERMINAL_ID, Menu.NONE, R.string.reset_terminal);
|
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_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());
|
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
|
@Override
|
||||||
public boolean onContextItemSelected(MenuItem item) {
|
public boolean onContextItemSelected(MenuItem item) {
|
||||||
|
TerminalSession session = getCurrentTermSession();
|
||||||
|
|
||||||
switch (item.getItemId()) {
|
switch (item.getItemId()) {
|
||||||
case CONTEXTMENU_SELECT_ID:
|
case CONTEXTMENU_SELECT_URL_ID:
|
||||||
CharSequence[] items = new CharSequence[] { getString(R.string.select_text), getString(R.string.select_url),
|
showUrlSelection();
|
||||||
getString(R.string.select_all_and_share) };
|
return true;
|
||||||
new AlertDialog.Builder(this).setItems(items, new DialogInterface.OnClickListener() {
|
case CONTEXTMENU_SHARE_TRANSCRIPT_ID:
|
||||||
@Override
|
if (session != null) {
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
Intent intent = new Intent(Intent.ACTION_SEND);
|
||||||
switch (which) {
|
intent.setType("text/plain");
|
||||||
case 0:
|
intent.putExtra(Intent.EXTRA_TEXT, session.getEmulator().getScreen().getTranscriptText().trim());
|
||||||
mTerminalView.toggleSelectingText();
|
intent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.share_transcript_title));
|
||||||
break;
|
startActivity(Intent.createChooser(intent, getString(R.string.share_transcript_chooser_title)));
|
||||||
case 1:
|
}
|
||||||
showUrlSelection();
|
return true;
|
||||||
break;
|
case CONTEXTMENU_PASTE_ID:
|
||||||
case 2:
|
doPaste();
|
||||||
TerminalSession session = getCurrentTermSession();
|
return true;
|
||||||
if (session != null) {
|
case CONTEXTMENU_KILL_PROCESS_ID:
|
||||||
Intent intent = new Intent(Intent.ACTION_SEND);
|
final AlertDialog.Builder b = new AlertDialog.Builder(this);
|
||||||
intent.setType("text/plain");
|
b.setIcon(android.R.drawable.ic_dialog_alert);
|
||||||
intent.putExtra(Intent.EXTRA_TEXT, session.getEmulator().getScreen().getTranscriptText().trim());
|
b.setMessage(R.string.confirm_kill_process);
|
||||||
intent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.share_transcript_title));
|
b.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
|
||||||
startActivity(Intent.createChooser(intent, getString(R.string.share_transcript_chooser_title)));
|
@Override
|
||||||
}
|
public void onClick(DialogInterface dialog, int id) {
|
||||||
break;
|
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;
|
||||||
return true;
|
}
|
||||||
case CONTEXTMENU_PASTE_ID:
|
case CONTEXTMENU_STYLING_ID: {
|
||||||
doPaste();
|
Intent stylingIntent = new Intent();
|
||||||
return true;
|
stylingIntent.setClassName("com.termux.styling", "com.termux.styling.TermuxStyleActivity");
|
||||||
case CONTEXTMENU_KILL_PROCESS_ID:
|
try {
|
||||||
final AlertDialog.Builder b = new AlertDialog.Builder(this);
|
startActivity(stylingIntent);
|
||||||
b.setIcon(android.R.drawable.ic_dialog_alert);
|
} catch (ActivityNotFoundException e) {
|
||||||
b.setMessage(R.string.confirm_kill_process);
|
new AlertDialog.Builder(this).setMessage(R.string.styling_not_installed)
|
||||||
b.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
|
.setPositiveButton(R.string.styling_install, new android.content.DialogInterface.OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(DialogInterface dialog, int id) {
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
dialog.dismiss();
|
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://play.google.com/store/apps/details?id=com.termux.styling")));
|
||||||
getCurrentTermSession().finishIfRunning();
|
}
|
||||||
|
}).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;
|
return true;
|
||||||
}
|
case CONTEXTMENU_TOGGLE_FULLSCREEN_ID:
|
||||||
case CONTEXTMENU_STYLING_ID: {
|
toggleImmersive();
|
||||||
Intent stylingIntent = new Intent();
|
return true;
|
||||||
stylingIntent.setClassName("com.termux.styling", "com.termux.styling.TermuxStyleActivity");
|
case CONTEXTMENU_HELP_ID:
|
||||||
try {
|
startActivity(new Intent(this, TermuxHelpActivity.class));
|
||||||
startActivity(stylingIntent);
|
return true;
|
||||||
} catch (ActivityNotFoundException e) {
|
default:
|
||||||
new AlertDialog.Builder(this).setMessage(R.string.styling_not_installed)
|
return super.onContextItemSelected(item);
|
||||||
.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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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) {
|
if (requestCode == REQUESTCODE_PERMISSION_STORAGE && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
TermuxInstaller.setupStorageSymlinks(this);
|
TermuxInstaller.setupStorageSymlinks(this);
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,5 @@
|
|||||||
package com.termux.app;
|
package com.termux.app;
|
||||||
|
|
||||||
import com.termux.terminal.TerminalSession;
|
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
@@ -10,6 +8,8 @@ import android.util.Log;
|
|||||||
import android.util.TypedValue;
|
import android.util.TypedValue;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import com.termux.terminal.TerminalSession;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.lang.annotation.Retention;
|
import java.lang.annotation.Retention;
|
||||||
@@ -26,14 +26,6 @@ final class TermuxPreferences {
|
|||||||
static final int BELL_BEEP = 2;
|
static final int BELL_BEEP = 2;
|
||||||
static final int BELL_IGNORE = 3;
|
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 final int MIN_FONTSIZE;
|
||||||
private static final int MAX_FONTSIZE = 256;
|
private static final int MAX_FONTSIZE = 256;
|
||||||
|
|
||||||
@@ -48,9 +40,6 @@ final class TermuxPreferences {
|
|||||||
@AsciiBellBehaviour
|
@AsciiBellBehaviour
|
||||||
int mBellBehaviour = BELL_VIBRATE;
|
int mBellBehaviour = BELL_VIBRATE;
|
||||||
|
|
||||||
@TapTerminalBehaviour
|
|
||||||
int mTapBehaviour = TAP_TOGGLE_KEYBOARD;
|
|
||||||
|
|
||||||
boolean mBackIsEscape = true;
|
boolean mBackIsEscape = true;
|
||||||
|
|
||||||
TermuxPreferences(Context context) {
|
TermuxPreferences(Context context) {
|
||||||
@@ -124,42 +113,26 @@ final class TermuxPreferences {
|
|||||||
public void reloadFromProperties(Context context) {
|
public void reloadFromProperties(Context context) {
|
||||||
try {
|
try {
|
||||||
File propsFile = new File(TermuxService.HOME_PATH + "/.config/termux/termux.properties");
|
File propsFile = new File(TermuxService.HOME_PATH + "/.config/termux/termux.properties");
|
||||||
|
Properties props = new Properties();
|
||||||
if (propsFile.isFile() && propsFile.canRead()) {
|
if (propsFile.isFile() && propsFile.canRead()) {
|
||||||
Properties props = new Properties();
|
|
||||||
try (FileInputStream in = new FileInputStream(propsFile)) {
|
try (FileInputStream in = new FileInputStream(propsFile)) {
|
||||||
props.load(in);
|
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) {
|
} catch (Exception e) {
|
||||||
Toast.makeText(context, "Error loading properties: " + e.getMessage(), Toast.LENGTH_LONG).show();
|
Toast.makeText(context, "Error loading properties: " + e.getMessage(), Toast.LENGTH_LONG).show();
|
||||||
Log.e("termux", "Error loading props", e);
|
Log.e("termux", "Error loading props", e);
|
||||||
|
@@ -61,6 +61,10 @@ public final class TerminalBuffer {
|
|||||||
TerminalRow lineObject = mLines[externalToInternalRow(row)];
|
TerminalRow lineObject = mLines[externalToInternalRow(row)];
|
||||||
int x1Index = lineObject.findStartOfColumn(x1);
|
int x1Index = lineObject.findStartOfColumn(x1);
|
||||||
int x2Index = (x2 < mColumns) ? lineObject.findStartOfColumn(x2) : lineObject.getSpaceUsed();
|
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;
|
char[] line = lineObject.mText;
|
||||||
int lastPrintingCharIndex = -1;
|
int lastPrintingCharIndex = -1;
|
||||||
int i;
|
int i;
|
||||||
@@ -71,7 +75,7 @@ public final class TerminalBuffer {
|
|||||||
} else {
|
} else {
|
||||||
for (i = x1Index; i < x2Index; ++i) {
|
for (i = x1Index; i < x2Index; ++i) {
|
||||||
char c = line[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);
|
if (lastPrintingCharIndex != -1) builder.append(line, x1Index, lastPrintingCharIndex - x1Index + 1);
|
||||||
|
@@ -184,6 +184,7 @@ public final class TerminalRow {
|
|||||||
mSpaceUsed += javaCharDifference;
|
mSpaceUsed += javaCharDifference;
|
||||||
|
|
||||||
// Store char. A combining character is stored at the end of the existing contents so that it modifies them:
|
// 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));
|
Character.toChars(codePoint, text, oldStartOfColumnIndex + (newIsCombining ? oldCharactersUsedForColumn : 0));
|
||||||
|
|
||||||
if (oldCodePointDisplayWidth == 2 && newCodePointDisplayWidth == 1) {
|
if (oldCodePointDisplayWidth == 2 && newCodePointDisplayWidth == 1) {
|
||||||
|
@@ -14,11 +14,11 @@ public interface TerminalKeyListener {
|
|||||||
/** Callback function on scale events according to {@link ScaleGestureDetector#getScaleFactor()}. */
|
/** Callback function on scale events according to {@link ScaleGestureDetector#getScaleFactor()}. */
|
||||||
float onScale(float scale);
|
float onScale(float scale);
|
||||||
|
|
||||||
void onLongPress(MotionEvent e);
|
|
||||||
|
|
||||||
/** On a single tap on the terminal if terminal mouse reporting not enabled. */
|
/** On a single tap on the terminal if terminal mouse reporting not enabled. */
|
||||||
void onSingleTapUp(MotionEvent e);
|
void onSingleTapUp(MotionEvent e);
|
||||||
|
|
||||||
boolean shouldBackButtonBeMappedToEscape();
|
boolean shouldBackButtonBeMappedToEscape();
|
||||||
|
|
||||||
|
void copyModeChanged(boolean copyMode);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,17 +1,26 @@
|
|||||||
package com.termux.view;
|
package com.termux.view;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.content.ClipData;
|
||||||
|
import android.content.ClipboardManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Rect;
|
||||||
import android.graphics.Typeface;
|
import android.graphics.Typeface;
|
||||||
|
import android.graphics.drawable.BitmapDrawable;
|
||||||
|
import android.os.Build;
|
||||||
import android.text.InputType;
|
import android.text.InputType;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import android.view.ActionMode;
|
||||||
import android.view.HapticFeedbackConstants;
|
import android.view.HapticFeedbackConstants;
|
||||||
import android.view.InputDevice;
|
import android.view.InputDevice;
|
||||||
import android.view.KeyCharacterMap;
|
import android.view.KeyCharacterMap;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuItem;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.inputmethod.BaseInputConnection;
|
import android.view.inputmethod.BaseInputConnection;
|
||||||
@@ -19,8 +28,10 @@ import android.view.inputmethod.EditorInfo;
|
|||||||
import android.view.inputmethod.InputConnection;
|
import android.view.inputmethod.InputConnection;
|
||||||
import android.widget.Scroller;
|
import android.widget.Scroller;
|
||||||
|
|
||||||
|
import com.termux.R;
|
||||||
import com.termux.terminal.EmulatorDebug;
|
import com.termux.terminal.EmulatorDebug;
|
||||||
import com.termux.terminal.KeyHandler;
|
import com.termux.terminal.KeyHandler;
|
||||||
|
import com.termux.terminal.TerminalBuffer;
|
||||||
import com.termux.terminal.TerminalColors;
|
import com.termux.terminal.TerminalColors;
|
||||||
import com.termux.terminal.TerminalEmulator;
|
import com.termux.terminal.TerminalEmulator;
|
||||||
import com.termux.terminal.TerminalSession;
|
import com.termux.terminal.TerminalSession;
|
||||||
@@ -34,7 +45,7 @@ import java.util.Properties;
|
|||||||
public final class TerminalView extends View {
|
public final class TerminalView extends View {
|
||||||
|
|
||||||
/** Log view key and IME events. */
|
/** 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}. */
|
/** The currently displayed terminal session, whose emulator is {@link #mEmulator}. */
|
||||||
TerminalSession mTermSession;
|
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. */
|
/** Keeping track of the special keys acting as Ctrl and Fn for the soft keyboard and other hardware keys. */
|
||||||
boolean mVirtualControlKeyDown, mVirtualFnKeyDown;
|
boolean mVirtualControlKeyDown, mVirtualFnKeyDown;
|
||||||
|
|
||||||
boolean mIsSelectingText = false;
|
boolean mIsSelectingText = false, mIsDraggingLeftSelection, mInitialTextSelection;
|
||||||
int mSelXAnchor = -1, mSelYAnchor = -1;
|
|
||||||
int mSelX1 = -1, mSelX2 = -1, mSelY1 = -1, mSelY2 = -1;
|
int mSelX1 = -1, mSelX2 = -1, mSelY1 = -1, mSelY2 = -1;
|
||||||
|
float mSelectionDownX, mSelectionDownY;
|
||||||
|
private ActionMode mActionMode;
|
||||||
|
private BitmapDrawable mLeftSelectionHandle, mRightSelectionHandle;
|
||||||
|
|
||||||
float mScaleFactor = 1.f;
|
float mScaleFactor = 1.f;
|
||||||
final GestureAndScaleRecognizer mGestureRecognizer;
|
final GestureAndScaleRecognizer mGestureRecognizer;
|
||||||
@@ -78,7 +91,7 @@ public final class TerminalView extends View {
|
|||||||
@Override
|
@Override
|
||||||
public boolean onUp(MotionEvent e) {
|
public boolean onUp(MotionEvent e) {
|
||||||
mScrollRemainder = 0.0f;
|
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
|
// Quick event processing when mouse tracking is active - do not wait for check of double tapping
|
||||||
// for zooming.
|
// for zooming.
|
||||||
sendMouseEventCode(e, TerminalEmulator.MOUSE_LEFT_BUTTON, true);
|
sendMouseEventCode(e, TerminalEmulator.MOUSE_LEFT_BUTTON, true);
|
||||||
@@ -91,6 +104,7 @@ public final class TerminalView extends View {
|
|||||||
@Override
|
@Override
|
||||||
public boolean onSingleTapUp(MotionEvent e) {
|
public boolean onSingleTapUp(MotionEvent e) {
|
||||||
if (mEmulator == null) return true;
|
if (mEmulator == null) return true;
|
||||||
|
if (mIsSelectingText) { toggleSelectingText(null); return true; }
|
||||||
requestFocus();
|
requestFocus();
|
||||||
if (!mEmulator.isMouseTrackingActive()) {
|
if (!mEmulator.isMouseTrackingActive()) {
|
||||||
if (!e.isFromSource(InputDevice.SOURCE_MOUSE)) {
|
if (!e.isFromSource(InputDevice.SOURCE_MOUSE)) {
|
||||||
@@ -103,7 +117,8 @@ public final class TerminalView extends View {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onScroll(MotionEvent e2, float distanceX, float distanceY) {
|
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 (mEmulator.isMouseTrackingActive() && e2.isFromSource(InputDevice.SOURCE_MOUSE)) {
|
||||||
// If moving with mouse pointer while pressing button, report that instead of scroll.
|
// 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,
|
// 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
|
@Override
|
||||||
public boolean onFling(final MotionEvent e2, float velocityX, float velocityY) {
|
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:
|
// Do not start scrolling until last fling has been taken care of:
|
||||||
if (!mScroller.isFinished()) return true;
|
if (!mScroller.isFinished()) return true;
|
||||||
|
|
||||||
@@ -175,9 +190,9 @@ public final class TerminalView extends View {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLongPress(MotionEvent e) {
|
public void onLongPress(MotionEvent e) {
|
||||||
if (mEmulator != null && !mGestureRecognizer.isInProgress()) {
|
if (!mGestureRecognizer.isInProgress()) {
|
||||||
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
|
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
|
// 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.
|
// "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:
|
// Let part of the application show behind when in landscape:
|
||||||
outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_FULLSCREEN;
|
outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_FULLSCREEN;
|
||||||
@@ -339,7 +354,6 @@ public final class TerminalView extends View {
|
|||||||
int rowShift = mEmulator.getScrollCounter();
|
int rowShift = mEmulator.getScrollCounter();
|
||||||
mSelY1 -= rowShift;
|
mSelY1 -= rowShift;
|
||||||
mSelY2 -= rowShift;
|
mSelY2 -= rowShift;
|
||||||
mSelYAnchor -= rowShift;
|
|
||||||
}
|
}
|
||||||
mEmulator.clearScrollCounter();
|
mEmulator.clearScrollCounter();
|
||||||
|
|
||||||
@@ -422,66 +436,91 @@ public final class TerminalView extends View {
|
|||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
@Override
|
@Override
|
||||||
|
@TargetApi(23)
|
||||||
public boolean onTouchEvent(MotionEvent ev) {
|
public boolean onTouchEvent(MotionEvent ev) {
|
||||||
if (mEmulator == null) return true;
|
if (mEmulator == null) return true;
|
||||||
final boolean eventFromMouse = ev.isFromSource(InputDevice.SOURCE_MOUSE);
|
|
||||||
final int action = ev.getAction();
|
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) {
|
if (mIsSelectingText) {
|
||||||
|
int cy = (int) (ev.getY() / mRenderer.mFontLineSpacing) + mTopRow;
|
||||||
int cx = (int) (ev.getX() / mRenderer.mFontWidth);
|
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) {
|
switch (action) {
|
||||||
|
case MotionEvent.ACTION_UP:
|
||||||
|
mInitialTextSelection = false;
|
||||||
|
break;
|
||||||
case MotionEvent.ACTION_DOWN:
|
case MotionEvent.ACTION_DOWN:
|
||||||
mSelXAnchor = cx;
|
int distanceFromSel1 = Math.abs(cx-mSelX1) + Math.abs(cy-mSelY1);
|
||||||
mSelYAnchor = cy;
|
int distanceFromSel2 = Math.abs(cx-mSelX2) + Math.abs(cy-mSelY2);
|
||||||
mSelX1 = cx;
|
mIsDraggingLeftSelection = distanceFromSel1 <= distanceFromSel2;
|
||||||
mSelY1 = cy;
|
mSelectionDownX = ev.getX();
|
||||||
mSelX2 = mSelX1;
|
mSelectionDownY = ev.getY();
|
||||||
mSelY2 = mSelY1;
|
|
||||||
invalidate();
|
|
||||||
break;
|
break;
|
||||||
case MotionEvent.ACTION_MOVE:
|
case MotionEvent.ACTION_MOVE:
|
||||||
case MotionEvent.ACTION_UP:
|
if (mInitialTextSelection) break;
|
||||||
boolean touchBeforeAnchor = (cy < mSelYAnchor || (cy == mSelYAnchor && cx < mSelXAnchor));
|
float deltaX = ev.getX() - mSelectionDownX;
|
||||||
int minx = touchBeforeAnchor ? cx : mSelXAnchor;
|
float deltaY = ev.getY() - mSelectionDownY;
|
||||||
int maxx = !touchBeforeAnchor ? cx : mSelXAnchor;
|
int deltaCols = (int) Math.ceil(deltaX / mRenderer.mFontWidth);
|
||||||
int miny = touchBeforeAnchor ? cy : mSelYAnchor;
|
int deltaRows = (int) Math.ceil(deltaY / mRenderer.mFontLineSpacing);
|
||||||
int maxy = !touchBeforeAnchor ? cy : mSelYAnchor;
|
mSelectionDownX += deltaCols * mRenderer.mFontWidth;
|
||||||
mSelX1 = minx;
|
mSelectionDownY += deltaRows * mRenderer.mFontLineSpacing;
|
||||||
mSelY1 = miny;
|
if (mIsDraggingLeftSelection) {
|
||||||
mSelX2 = maxx;
|
mSelX1 += deltaCols;
|
||||||
mSelY2 = maxy;
|
mSelY1 += deltaRows;
|
||||||
if (action == MotionEvent.ACTION_UP) {
|
} else {
|
||||||
String selectedText = mEmulator.getSelectedText(mSelX1, mSelY1, mSelX2, mSelY2).trim();
|
mSelX2 += deltaCols;
|
||||||
mTermSession.clipboardText(selectedText);
|
mSelY2 += deltaRows;
|
||||||
toggleSelectingText();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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();
|
invalidate();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
toggleSelectingText();
|
|
||||||
invalidate();
|
|
||||||
break;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -491,13 +530,18 @@ public final class TerminalView extends View {
|
|||||||
@Override
|
@Override
|
||||||
public boolean onKeyPreIme(int keyCode, KeyEvent event) {
|
public boolean onKeyPreIme(int keyCode, KeyEvent event) {
|
||||||
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "onKeyPreIme(keyCode=" + keyCode + ", event=" + event + ")");
|
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "onKeyPreIme(keyCode=" + keyCode + ", event=" + event + ")");
|
||||||
if (keyCode == KeyEvent.KEYCODE_BACK && mOnKeyListener.shouldBackButtonBeMappedToEscape()) {
|
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
||||||
// Intercept back button to treat it as escape:
|
if (mIsSelectingText) {
|
||||||
switch (event.getAction()) {
|
toggleSelectingText(null);
|
||||||
case KeyEvent.ACTION_DOWN:
|
return true;
|
||||||
return onKeyDown(keyCode, event);
|
} else if (mOnKeyListener.shouldBackButtonBeMappedToEscape()) {
|
||||||
case KeyEvent.ACTION_UP:
|
// Intercept back button to treat it as escape:
|
||||||
return onKeyUp(keyCode, event);
|
switch (event.getAction()) {
|
||||||
|
case KeyEvent.ACTION_DOWN:
|
||||||
|
return onKeyDown(keyCode, event);
|
||||||
|
case KeyEvent.ACTION_UP:
|
||||||
|
return onKeyUp(keyCode, event);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return super.onKeyPreIme(keyCode, event);
|
return super.onKeyPreIme(keyCode, event);
|
||||||
@@ -776,13 +820,147 @@ public final class TerminalView extends View {
|
|||||||
canvas.drawColor(0XFF000000);
|
canvas.drawColor(0XFF000000);
|
||||||
} else {
|
} else {
|
||||||
mRenderer.render(mEmulator, canvas, mTopRow, mSelY1, mSelY2, mSelX1, mSelX2);
|
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. */
|
/** Toggle text selection mode in the view. */
|
||||||
public void toggleSelectingText() {
|
@TargetApi(23)
|
||||||
|
public void toggleSelectingText(MotionEvent ev) {
|
||||||
mIsSelectingText = !mIsSelectingText;
|
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() {
|
public TerminalSession getCurrentSession() {
|
||||||
|
Binary file not shown.
After Width: | Height: | Size: 2.0 KiB |
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:src="@drawable/text_select_handle_left_mtrl_alpha"
|
||||||
|
android:tint="#2196F3" />
|
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:src="@drawable/text_select_handle_right_mtrl_alpha"
|
||||||
|
android:tint="#2196F3" />
|
@@ -29,18 +29,18 @@
|
|||||||
|
|
||||||
<string name="reset_toast_notification">Terminal reset.</string>
|
<string name="reset_toast_notification">Terminal reset.</string>
|
||||||
|
|
||||||
<string name="select">Select…</string>
|
|
||||||
<string name="select_text">Select text</string>
|
|
||||||
<string name="select_url">Select URL</string>
|
<string name="select_url">Select URL</string>
|
||||||
<string name="select_url_dialog_title">Click URL to copy or long press to open</string>
|
<string name="select_url_dialog_title">Click URL to copy or long press to open</string>
|
||||||
<string name="select_all_and_share">Select all text and share</string>
|
<string name="select_all_and_share">Share transcript</string>
|
||||||
<string name="select_url_no_found">No URL found in the terminal.</string>
|
<string name="select_url_no_found">No URL found in the terminal.</string>
|
||||||
<string name="select_url_copied_to_clipboard">URL copied to clipboard</string>
|
<string name="select_url_copied_to_clipboard">URL copied to clipboard</string>
|
||||||
<string name="share_transcript_chooser_title">Send text to:</string>
|
<string name="share_transcript_chooser_title">Send text to:</string>
|
||||||
|
|
||||||
<string name="paste_text">Paste</string>
|
<string name="paste_text">Paste</string>
|
||||||
<string name="kill_process">Hangup</string>
|
<string name="copy_text">Copy</string>
|
||||||
|
<string name="text_selection_more">More…</string>
|
||||||
|
|
||||||
|
<string name="kill_process">Hangup</string>
|
||||||
<string name="confirm_kill_process">Close this process?</string>
|
<string name="confirm_kill_process">Close this process?</string>
|
||||||
|
|
||||||
<string name="session_rename_title">Set session name</string>
|
<string name="session_rename_title">Set session name</string>
|
||||||
|
Reference in New Issue
Block a user