aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKenny Root <kenny@the-b.org>2015-10-08 15:12:10 -0700
committerKenny Root <kenny@the-b.org>2015-10-08 15:12:10 -0700
commit7daecb5d1e546efca204eb57593c324de0333376 (patch)
tree0bf257385dcb05317abac9607fc8c6a035fbf15e
parentfde40fdcb62c764df90e889ae95e28b86ac5f746 (diff)
parent35f62aae8f8259e0f0e5db7ad7b967621175bf29 (diff)
downloadconnectbot-7daecb5d1e546efca204eb57593c324de0333376.tar.gz
connectbot-7daecb5d1e546efca204eb57593c324de0333376.tar.bz2
connectbot-7daecb5d1e546efca204eb57593c324de0333376.zip
Merge pull request #277 from rhansby/textview-terminal-2
TextView in terminal for better text selection
-rw-r--r--app/src/main/java/org/connectbot/ConsoleActivity.java355
-rw-r--r--app/src/main/java/org/connectbot/TerminalView.java483
-rw-r--r--app/src/main/java/org/connectbot/service/TerminalBridge.java43
-rw-r--r--app/src/main/java/org/connectbot/service/TerminalKeyListener.java8
-rw-r--r--app/src/main/java/org/connectbot/util/TerminalViewPager.java61
-rw-r--r--app/src/main/res/layout-large/act_console.xml2
-rw-r--r--app/src/main/res/layout/act_console.xml2
7 files changed, 581 insertions, 373 deletions
diff --git a/app/src/main/java/org/connectbot/ConsoleActivity.java b/app/src/main/java/org/connectbot/ConsoleActivity.java
index d628a07..440661a 100644
--- a/app/src/main/java/org/connectbot/ConsoleActivity.java
+++ b/app/src/main/java/org/connectbot/ConsoleActivity.java
@@ -22,15 +22,14 @@ import java.util.ArrayList;
import java.util.List;
import org.connectbot.bean.HostBean;
-import org.connectbot.bean.SelectionArea;
import org.connectbot.service.BridgeDisconnectedListener;
import org.connectbot.service.PromptHelper;
import org.connectbot.service.TerminalBridge;
import org.connectbot.service.TerminalKeyListener;
import org.connectbot.service.TerminalManager;
import org.connectbot.util.PreferenceConstants;
+import org.connectbot.util.TerminalViewPager;
-import android.annotation.TargetApi;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.ComponentName;
@@ -50,20 +49,15 @@ import android.os.IBinder;
import android.os.Message;
import android.preference.PreferenceManager;
import android.support.annotation.Nullable;
-import android.support.v4.app.ActivityCompat;
import android.support.design.widget.TabLayout;
+import android.support.v4.app.ActivityCompat;
import android.support.v4.view.MenuItemCompat;
-import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.PagerAdapter;
-import android.support.v4.view.ViewPager;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.text.ClipboardManager;
import android.util.Log;
-import android.view.ContextMenu;
-import android.view.GestureDetector;
-import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
@@ -74,7 +68,6 @@ import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnKeyListener;
import android.view.View.OnTouchListener;
-import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
@@ -100,14 +93,12 @@ public class ConsoleActivity extends AppCompatActivity implements BridgeDisconne
protected static final int REQUEST_EDIT = 1;
- private static final int CLICK_TIME = 400;
- private static final float MAX_CLICK_DISTANCE = 25f;
private static final int KEYBOARD_DISPLAY_TIME = 3000;
private static final int KEYBOARD_REPEAT_INITIAL = 500;
private static final int KEYBOARD_REPEAT = 100;
private static final String STATE_SELECTED_URI = "selectedUri";
- protected ViewPager pager = null;
+ protected TerminalViewPager pager = null;
protected TabLayout tabs = null;
protected Toolbar toolbar = null;
@Nullable
@@ -140,15 +131,11 @@ public class ConsoleActivity extends AppCompatActivity implements BridgeDisconne
private Animation fade_out_delayed;
private Animation keyboard_fade_in, keyboard_fade_out;
- private float lastX, lastY;
private InputMethodManager inputManager;
private MenuItem disconnect, copy, paste, portForward, resize, urlscan;
- protected TerminalBridge copySource = null;
- private int lastTouchRow, lastTouchCol;
-
private boolean forcedOrientation;
private Handler handler = new Handler();
@@ -498,10 +485,11 @@ public class ConsoleActivity extends AppCompatActivity implements BridgeDisconne
inflater = LayoutInflater.from(this);
toolbar = (Toolbar) findViewById(R.id.toolbar);
- pager = (ViewPager) findViewById(R.id.console_flip);
- registerForContextMenu(pager);
+
+ pager = (TerminalViewPager) findViewById(R.id.console_flip);
+
pager.addOnPageChangeListener(
- new ViewPager.SimpleOnPageChangeListener() {
+ new TerminalViewPager.SimpleOnPageChangeListener() {
@Override
public void onPageSelected(int position) {
setTitle(adapter.getPageTitle(position));
@@ -669,258 +657,17 @@ public class ConsoleActivity extends AppCompatActivity implements BridgeDisconne
if (tabs != null)
setupTabLayoutWithViewPager();
- // detect fling gestures to switch between terminals
- final GestureDetector detect = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() {
- private float totalY = 0;
-
+ pager.setOnClickListener(new OnClickListener() {
@Override
- public void onLongPress(MotionEvent e) {
- super.onLongPress(e);
- openContextMenu(pager);
- }
-
-
- @Override
- public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
-
- // if copying, then ignore
- if (copySource != null && copySource.isSelectingForCopy())
- return false;
-
- if (e1 == null || e2 == null)
- return false;
-
- // if releasing then reset total scroll
- if (e2.getAction() == MotionEvent.ACTION_UP) {
- totalY = 0;
- }
-
- // activate consider if within x tolerance
- int touchSlop = ViewConfiguration.get(ConsoleActivity.this).getScaledTouchSlop();
- if (Math.abs(e1.getX() - e2.getX()) < touchSlop * 4) {
-
- View view = adapter.getCurrentTerminalView();
- if (view == null) return false;
- TerminalView terminal = (TerminalView) view;
-
- // estimate how many rows we have scrolled through
- // accumulate distance that doesn't trigger immediate scroll
- totalY += distanceY;
- final int moved = (int) (totalY / terminal.bridge.charHeight);
-
- // consume as scrollback only if towards right half of screen
- if (e2.getX() > view.getWidth() / 2) {
- if (moved != 0) {
- int base = terminal.bridge.buffer.getWindowBase();
- terminal.bridge.buffer.setWindowBase(base + moved);
- totalY = 0;
- return true;
- }
- } else {
- // otherwise consume as pgup/pgdown for every 5 lines
- if (moved > 5) {
- ((vt320) terminal.bridge.buffer).keyPressed(vt320.KEY_PAGE_DOWN, ' ', 0);
- terminal.bridge.tryKeyVibrate();
- totalY = 0;
- return true;
- } else if (moved < -5) {
- ((vt320) terminal.bridge.buffer).keyPressed(vt320.KEY_PAGE_UP, ' ', 0);
- terminal.bridge.tryKeyVibrate();
- totalY = 0;
- return true;
- }
-
- }
-
- }
-
- return false;
- }
-
-
- });
-
- pager.setLongClickable(true);
- pager.setOnTouchListener(new OnTouchListener() {
-
- public boolean onTouch(View v, MotionEvent event) {
- TerminalBridge bridge = adapter.getCurrentTerminalView().bridge;
-
- // Handle mouse-specific actions.
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH &&
- MotionEventCompat.getSource(event) == InputDevice.SOURCE_MOUSE) {
- if (onMouseEvent(event, bridge)) {
- return true;
- }
- }
-
- // when copying, highlight the area
- if (copySource != null && copySource.isSelectingForCopy()) {
- SelectionArea area = copySource.getSelectionArea();
- int row = (int) Math.floor(event.getY() / bridge.charHeight);
- int col = (int) Math.floor(event.getX() / bridge.charWidth);
-
- switch (event.getAction()) {
- case MotionEvent.ACTION_DOWN:
- // recording starting area
- if (area.isSelectingOrigin()) {
- area.setRow(row);
- area.setColumn(col);
- lastTouchRow = row;
- lastTouchCol = col;
- copySource.redraw();
- }
- return true;
- case MotionEvent.ACTION_MOVE:
- /* ignore when user hasn't moved since last time so
- * we can fine-tune with directional pad
- */
- if (row == lastTouchRow && col == lastTouchCol)
- return true;
-
- // if the user moves, start the selection for other corner
- area.finishSelectingOrigin();
-
- // update selected area
- area.setRow(row);
- area.setColumn(col);
- lastTouchRow = row;
- lastTouchCol = col;
- copySource.redraw();
- return true;
- case MotionEvent.ACTION_UP:
- /* If they didn't move their finger, maybe they meant to
- * select the rest of the text with the directional pad.
- */
- if (area.getLeft() == area.getRight() &&
- area.getTop() == area.getBottom()) {
- return true;
- }
-
- // copy selected area to clipboard
- String copiedText = area.copyFrom(copySource.buffer);
-
- clipboard.setText(copiedText);
- Toast.makeText(ConsoleActivity.this, getString(R.string.console_copy_done, copiedText.length()), Toast.LENGTH_LONG).show();
- // fall through to clear state
-
- case MotionEvent.ACTION_CANCEL:
- // make sure we clear any highlighted area
- area.reset();
- copySource.setSelectingForCopy(false);
- copySource.redraw();
- return true;
- }
- }
-
- if (event.getAction() == MotionEvent.ACTION_DOWN) {
- lastX = event.getX();
- lastY = event.getY();
- } else if (event.getAction() == MotionEvent.ACTION_UP
- && keyboardGroup.getVisibility() == View.GONE
- && event.getEventTime() - event.getDownTime() < CLICK_TIME
- && Math.abs(event.getX() - lastX) < MAX_CLICK_DISTANCE
- && Math.abs(event.getY() - lastY) < MAX_CLICK_DISTANCE) {
- showEmulatedKeys(true);
- }
-
- // pass any touch events back to detector
- return detect.onTouchEvent(event);
- }
-
- /**
- * @param event
- * @param bridge
- * @return True if the event is handled.
- */
- @TargetApi(14)
- private boolean onMouseEvent(MotionEvent event, TerminalBridge bridge) {
- int row = (int) Math.floor(event.getY() / bridge.charHeight);
- int col = (int) Math.floor(event.getX() / bridge.charWidth);
- int meta = event.getMetaState();
- boolean shiftOn = (meta & KeyEvent.META_SHIFT_ON) != 0;
- boolean mouseReport = ((vt320) bridge.buffer).isMouseReportEnabled();
-
- // MouseReport can be "defeated" using the shift key.
- if ((!mouseReport || shiftOn)) {
- if (event.getAction() == MotionEvent.ACTION_DOWN) {
- switch (event.getButtonState()) {
- case MotionEvent.BUTTON_PRIMARY:
- // Automatically start copy mode if using a mouse.
- startCopyMode();
- break;
- case MotionEvent.BUTTON_SECONDARY:
- openContextMenu(pager);
- return true;
- case MotionEvent.BUTTON_TERTIARY:
- // Middle click pastes.
- pasteIntoTerminal();
- return true;
- }
- }
- } else if (event.getAction() == MotionEvent.ACTION_DOWN) {
- ((vt320) bridge.buffer).mousePressed(
- col, row, mouseEventToJavaModifiers(event));
- return true;
- } else if (event.getAction() == MotionEvent.ACTION_UP) {
- ((vt320) bridge.buffer).mouseReleased(col, row);
- return true;
- } else if (event.getAction() == MotionEvent.ACTION_MOVE) {
- int buttonState = event.getButtonState();
- int button = (buttonState & MotionEvent.BUTTON_PRIMARY) != 0 ? 0 :
- (buttonState & MotionEvent.BUTTON_SECONDARY) != 0 ? 1 :
- (buttonState & MotionEvent.BUTTON_TERTIARY) != 0 ? 2 : 3;
- ((vt320) bridge.buffer).mouseMoved(
- button,
- col,
- row,
- (meta & KeyEvent.META_CTRL_ON) != 0,
- (meta & KeyEvent.META_SHIFT_ON) != 0,
- (meta & KeyEvent.META_META_ON) != 0);
- return true;
+ public void onClick(View v) {
+ if (keyboardGroup.getVisibility() == View.GONE) {
+ showEmulatedKeys(false);
}
-
- return false;
}
-
});
}
/**
- * Takes an android mouse event and produces a Java InputEvent modifiers int which can be
- * passed to vt320.
- * @param mouseEvent The {@link MotionEvent} which should be a mouse click or release.
- * @return A Java InputEvent modifier int. See
- * http://docs.oracle.com/javase/7/docs/api/java/awt/event/InputEvent.html
- */
- @TargetApi(14)
- private static int mouseEventToJavaModifiers(MotionEvent mouseEvent) {
- if (MotionEventCompat.getSource(mouseEvent) != InputDevice.SOURCE_MOUSE) return 0;
-
- int mods = 0;
-
- // See http://docs.oracle.com/javase/7/docs/api/constant-values.html
- int buttonState = mouseEvent.getButtonState();
- if ((buttonState & MotionEvent.BUTTON_PRIMARY) != 0)
- mods |= 16;
- if ((buttonState & MotionEvent.BUTTON_SECONDARY) != 0)
- mods |= 8;
- if ((buttonState & MotionEvent.BUTTON_TERTIARY) != 0)
- mods |= 4;
-
- // Note: Meta and Ctrl are intentionally swapped here to keep logic in vt320 simple.
- int meta = mouseEvent.getMetaState();
- if ((meta & KeyEvent.META_META_ON) != 0)
- mods |= 2;
- if ((meta & KeyEvent.META_SHIFT_ON) != 0)
- mods |= 1;
- if ((meta & KeyEvent.META_CTRL_ON) != 0)
- mods |= 4;
-
- return mods;
- }
-
- /**
* Ties the {@link TabLayout} to the {@link ViewPager}.
*
* <p>This method will:
@@ -1011,19 +758,21 @@ public class ConsoleActivity extends AppCompatActivity implements BridgeDisconne
}
});
- copy = menu.add(R.string.console_menu_copy);
- if (hardKeyboard)
- copy.setAlphabeticShortcut('c');
- MenuItemCompat.setShowAsAction(copy, MenuItemCompat.SHOW_AS_ACTION_IF_ROOM);
- copy.setIcon(R.drawable.ic_action_copy);
- copy.setEnabled(activeTerminal);
- copy.setOnMenuItemClickListener(new OnMenuItemClickListener() {
- public boolean onMenuItemClick(MenuItem item) {
- startCopyMode();
- Toast.makeText(ConsoleActivity.this, getString(R.string.console_copy_start), Toast.LENGTH_LONG).show();
- return true;
- }
- });
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
+ copy = menu.add(R.string.console_menu_copy);
+ if (hardKeyboard)
+ copy.setAlphabeticShortcut('c');
+ MenuItemCompat.setShowAsAction(copy, MenuItemCompat.SHOW_AS_ACTION_IF_ROOM);
+ copy.setIcon(R.drawable.ic_action_copy);
+ copy.setEnabled(activeTerminal);
+ copy.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ adapter.getCurrentTerminalView().startPreHoneycombCopyMode();
+ Toast.makeText(ConsoleActivity.this, getString(R.string.console_copy_start), Toast.LENGTH_LONG).show();
+ return true;
+ }
+ });
+ }
paste = menu.add(R.string.console_menu_paste);
if (hardKeyboard)
@@ -1144,7 +893,10 @@ public class ConsoleActivity extends AppCompatActivity implements BridgeDisconne
disconnect.setTitle(R.string.list_host_disconnect);
else
disconnect.setTitle(R.string.console_menu_close);
- copy.setEnabled(activeTerminal);
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
+ copy.setEnabled(activeTerminal);
+ }
paste.setEnabled(clipboard.hasText() && sessionOpen);
portForward.setEnabled(sessionOpen && canForwardPorts);
urlscan.setEnabled(activeTerminal);
@@ -1174,32 +926,6 @@ public class ConsoleActivity extends AppCompatActivity implements BridgeDisconne
}
@Override
- public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
- final TerminalView view = adapter.getCurrentTerminalView();
- boolean activeTerminal = view != null;
- boolean sessionOpen = false;
-
- if (activeTerminal) {
- TerminalBridge bridge = view.bridge;
- sessionOpen = bridge.isSessionOpen();
- }
-
- MenuItem paste = menu.add(R.string.console_menu_paste);
- if (hardKeyboard)
- paste.setAlphabeticShortcut('v');
- paste.setIcon(android.R.drawable.ic_menu_edit);
- paste.setEnabled(clipboard.hasText() && sessionOpen);
- paste.setOnMenuItemClickListener(new OnMenuItemClickListener() {
- public boolean onMenuItemClick(MenuItem item) {
- pasteIntoTerminal();
- return true;
- }
- });
-
-
- }
-
- @Override
public void onStart() {
super.onStart();
@@ -1308,21 +1034,6 @@ public class ConsoleActivity extends AppCompatActivity implements BridgeDisconne
super.onSaveInstanceState(savedInstanceState);
}
- private void startCopyMode() {
- // mark as copying and reset any previous bounds
- TerminalView terminalView = (TerminalView) adapter.getCurrentTerminalView();
- copySource = terminalView.bridge;
-
- SelectionArea area = copySource.getSelectionArea();
- area.reset();
- area.setBounds(copySource.buffer.getColumns(), copySource.buffer.getRows());
-
- copySource.setSelectingForCopy(true);
-
- // Make sure we show the initial selection
- copySource.redraw();
- }
-
/**
* Save the currently shown {@link TerminalView} as the default. This is
* saved back down into {@link TerminalManager} where we can read it again
@@ -1494,7 +1205,7 @@ public class ConsoleActivity extends AppCompatActivity implements BridgeDisconne
overlay.setText(bridge.host.getNickname());
// and add our terminal view control, using index to place behind overlay
- final TerminalView terminal = new TerminalView(container.getContext(), bridge);
+ final TerminalView terminal = new TerminalView(container.getContext(), bridge, pager);
terminal.setId(R.id.terminal_view);
view.addView(terminal, 0);
@@ -1572,7 +1283,9 @@ public class ConsoleActivity extends AppCompatActivity implements BridgeDisconne
public TerminalView getCurrentTerminalView() {
View currentView = pager.findViewWithTag(getBridgeAtPosition(pager.getCurrentItem()));
- if (currentView == null) return null;
+ if (currentView == null) {
+ return null;
+ }
return (TerminalView) currentView.findViewById(R.id.terminal_view);
}
}
diff --git a/app/src/main/java/org/connectbot/TerminalView.java b/app/src/main/java/org/connectbot/TerminalView.java
index 7c4f51f..bc095fc 100644
--- a/app/src/main/java/org/connectbot/TerminalView.java
+++ b/app/src/main/java/org/connectbot/TerminalView.java
@@ -25,47 +25,72 @@ import org.connectbot.bean.SelectionArea;
import org.connectbot.service.FontSizeChangedListener;
import org.connectbot.service.TerminalBridge;
import org.connectbot.service.TerminalKeyListener;
+import org.connectbot.util.TerminalViewPager;
import android.annotation.TargetApi;
+import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.database.Cursor;
import android.graphics.Canvas;
+import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelXorXfermode;
import android.graphics.RectF;
+import android.graphics.Typeface;
import android.net.Uri;
import android.os.AsyncTask;
+import android.os.Build;
import android.support.v4.view.MotionEventCompat;
+import android.text.ClipboardManager;
+import android.view.ActionMode;
+import android.view.GestureDetector;
import android.view.InputDevice;
import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
import android.view.MotionEvent;
-import android.view.View;
import android.view.ViewGroup.LayoutParams;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
+import android.widget.TextView;
import android.widget.Toast;
import de.mud.terminal.VDUBuffer;
import de.mud.terminal.vt320;
/**
- * User interface {@link View} for showing a TerminalBridge in an
+ * User interface {@link TextView} for showing a TerminalBridge in an
* {@link android.app.Activity}. Handles drawing bitmap updates and passing keystrokes down
* to terminal.
*
+ * On Honeycomb devices and above (>= APIv11), a TextView with transparent text (which is identical
+ * to the bitmap) is drawn above the bitmap. This TextView exists to allow the user to
+ * select and copy text.
+ *
* @author jsharkey
*/
-public class TerminalView extends View implements FontSizeChangedListener {
+public class TerminalView extends TextView implements FontSizeChangedListener {
private final Context context;
public final TerminalBridge bridge;
+
+ private final TerminalViewPager viewPager;
+ private GestureDetector gestureDetector;
+
+ private ClipboardManager clipboard;
+ private ActionMode selectionActionMode = null;
+ private String currentSelection = "";
+
+ // These are only used for pre-Honeycomb copying.
+ private int lastTouchedRow, lastTouchedCol;
+
private final Paint paint;
private final Paint cursorPaint;
private final Paint cursorStrokePaint;
@@ -96,17 +121,19 @@ public class TerminalView extends View implements FontSizeChangedListener {
private static final String SCREENREADER_INTENT_ACTION = "android.accessibilityservice.AccessibilityService";
private static final String SCREENREADER_INTENT_CATEGORY = "android.accessibilityservice.category.FEEDBACK_SPOKEN";
- public TerminalView(Context context, TerminalBridge bridge) {
+ public TerminalView(Context context, TerminalBridge bridge, TerminalViewPager pager) {
super(context);
this.context = context;
this.bridge = bridge;
- paint = new Paint();
+ this.viewPager = pager;
setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
setFocusable(true);
setFocusableInTouchMode(true);
+ paint = new Paint();
+
cursorPaint = new Paint();
cursorPaint.setColor(bridge.color[bridge.defaultFg]);
cursorPaint.setXfermode(new PixelXorXfermode(bridge.color[bridge.defaultBg]));
@@ -142,6 +169,7 @@ public class TerminalView extends View implements FontSizeChangedListener {
scaleMatrix = new Matrix();
bridge.addFontSizeChangedListener(this);
+ bridge.parentChanged(this);
// connect our view up to the bridge
setOnKeyListener(bridge.getKeyHandler());
@@ -150,6 +178,400 @@ public class TerminalView extends View implements FontSizeChangedListener {
// Enable accessibility features if a screen reader is active.
new AccessibilityStateTester().execute((Void) null);
+
+ clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
+
+ setTextColor(Color.TRANSPARENT);
+ setTypeface(Typeface.MONOSPACE);
+ onFontSizeChanged(bridge.getFontSize());
+
+ // Allow selection of and copying text for Honeycomb and above devices.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ setTextIsSelectable(true);
+ setCustomSelectionActionModeCallback(new TextSelectionActionModeCallback());
+ }
+
+ gestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
+ private TerminalBridge bridge = TerminalView.this.bridge;
+ private float totalY = 0;
+
+ @Override
+ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+ // if releasing then reset total scroll
+ if (e2.getAction() == MotionEvent.ACTION_UP) {
+ totalY = 0;
+ }
+
+ totalY += distanceY;
+ final int moved = (int) (totalY / bridge.charHeight);
+
+ if (moved != 0) {
+ int base = bridge.buffer.getWindowBase();
+ bridge.buffer.setWindowBase(base + moved);
+ totalY = 0;
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean onSingleTapConfirmed(MotionEvent e) {
+ viewPager.performClick();
+ return super.onSingleTapConfirmed(e);
+ }
+ });
+ }
+
+ @TargetApi(11)
+ private void closeSelectionActionMode() {
+ if (selectionActionMode != null) {
+ selectionActionMode.finish();
+ selectionActionMode = null;
+ }
+ }
+
+ public void copyCurrentSelectionToClipboard() {
+ ClipboardManager clipboard =
+ (ClipboardManager) TerminalView.this.context.getSystemService(Context.CLIPBOARD_SERVICE);
+ if (currentSelection.length() != 0) {
+ clipboard.setText(currentSelection);
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ closeSelectionActionMode();
+ }
+ }
+
+ @Override
+ protected void onSelectionChanged(int selStart, int selEnd) {
+ if (selStart <= selEnd) {
+ currentSelection = getText().toString().substring(selStart, selEnd);
+ }
+ super.onSelectionChanged(selStart, selEnd);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ // Selection may be beginning. Sync the TextView with the buffer.
+ refreshTextFromBuffer();
+ }
+
+ // Mouse input is treated differently:
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH &&
+ MotionEventCompat.getSource(event) == InputDevice.SOURCE_MOUSE) {
+ if (onMouseEvent(event, bridge)) {
+ return true;
+ }
+ viewPager.setPagingEnabled(true);
+ } else if (gestureDetector != null) {
+ // The gesture detector should not be called if touch event was from mouse.
+ gestureDetector.onTouchEvent(event);
+ }
+
+ // Old version of copying, only for pre-Honeycomb.
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
+ // when copying, highlight the area
+ if (bridge.isSelectingForCopy()) {
+ SelectionArea area = bridge.getSelectionArea();
+ int row = (int) Math.floor(event.getY() / bridge.charHeight);
+ int col = (int) Math.floor(event.getX() / bridge.charWidth);
+
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ // recording starting area
+ viewPager.setPagingEnabled(false);
+ if (area.isSelectingOrigin()) {
+ area.setRow(row);
+ area.setColumn(col);
+ lastTouchedRow = row;
+ lastTouchedCol = col;
+ bridge.redraw();
+ }
+ return true;
+ case MotionEvent.ACTION_MOVE:
+ /* ignore when user hasn't moved since last time so
+ * we can fine-tune with directional pad
+ */
+ if (row == lastTouchedRow && col == lastTouchedCol)
+ return true;
+
+ // if the user moves, start the selection for other corner
+ area.finishSelectingOrigin();
+
+ // update selected area
+ area.setRow(row);
+ area.setColumn(col);
+ lastTouchedRow = row;
+ lastTouchedCol = col;
+ bridge.redraw();
+ return true;
+ case MotionEvent.ACTION_UP:
+ /* If they didn't move their finger, maybe they meant to
+ * select the rest of the text with the directional pad.
+ */
+ if (area.getLeft() == area.getRight() &&
+ area.getTop() == area.getBottom()) {
+ return true;
+ }
+
+ // copy selected area to clipboard
+ String copiedText = area.copyFrom(bridge.buffer);
+
+ clipboard.setText(copiedText);
+ Toast.makeText(
+ context,
+ context.getString(R.string.console_copy_done, copiedText.length()),
+ Toast.LENGTH_LONG).show();
+
+ // fall through to clear state
+
+ case MotionEvent.ACTION_CANCEL:
+ // make sure we clear any highlighted area
+ area.reset();
+ bridge.setSelectingForCopy(false);
+ bridge.redraw();
+ viewPager.setPagingEnabled(true);
+ return true;
+ }
+ }
+
+ return true;
+ }
+
+ super.onTouchEvent(event);
+
+ return true;
+ }
+
+ @TargetApi(11)
+ private class TextSelectionActionModeCallback implements ActionMode.Callback {
+ private static final int COPY = 0;
+ private static final int PASTE = 1;
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ return false;
+ }
+
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ TerminalView.this.selectionActionMode = mode;
+
+ menu.clear();
+
+ menu.add(0, COPY, 0, R.string.console_menu_copy)
+ .setIcon(R.drawable.ic_action_copy)
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_WITH_TEXT | MenuItem.SHOW_AS_ACTION_IF_ROOM);
+ menu.add(0, PASTE, 1, R.string.console_menu_paste)
+ .setIcon(R.drawable.ic_action_paste)
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_WITH_TEXT | MenuItem.SHOW_AS_ACTION_IF_ROOM);
+
+ return true;
+ }
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ switch (item.getItemId()) {
+ case COPY:
+ copyCurrentSelectionToClipboard();
+ return true;
+ case PASTE:
+ String clip = clipboard.getText().toString();
+ TerminalView.this.bridge.injectString(clip);
+ mode.finish();
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ }
+ }
+
+ /**
+ * @param event
+ * @param bridge
+ * @return True if the event is handled.
+ */
+ @TargetApi(14)
+ private boolean onMouseEvent(MotionEvent event, TerminalBridge bridge) {
+ int row = (int) Math.floor(event.getY() / bridge.charHeight);
+ int col = (int) Math.floor(event.getX() / bridge.charWidth);
+ int meta = event.getMetaState();
+ boolean shiftOn = (meta & KeyEvent.META_SHIFT_ON) != 0;
+ vt320 vtBuffer = (vt320) bridge.buffer;
+ boolean mouseReport = vtBuffer.isMouseReportEnabled();
+
+ // MouseReport can be "defeated" using the shift key.
+ if (!mouseReport || shiftOn) {
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ if (event.getButtonState() == MotionEvent.BUTTON_TERTIARY) {
+ // Middle click pastes.
+ String clip = clipboard.getText().toString();
+ bridge.injectString(clip);
+ return true;
+ }
+
+ // Begin "selection mode"
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ closeSelectionActionMode();
+ }
+ } else if (event.getAction() == MotionEvent.ACTION_MOVE) {
+ // In the middle of selection.
+
+ if (selectionActionMode == null) {
+ selectionActionMode = startActionMode(new TextSelectionActionModeCallback());
+ }
+
+ int selectionStart = getSelectionStart();
+ int selectionEnd = getSelectionEnd();
+
+ if (selectionStart > selectionEnd) {
+ int tempStart = selectionStart;
+ selectionStart = selectionEnd;
+ selectionEnd = tempStart;
+ }
+
+ currentSelection = getText().toString().substring(selectionStart, selectionEnd);
+ }
+ } else if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ viewPager.setPagingEnabled(false);
+ vtBuffer.mousePressed(
+ col, row, mouseEventToJavaModifiers(event));
+ return true;
+ } else if (event.getAction() == MotionEvent.ACTION_UP) {
+ viewPager.setPagingEnabled(true);
+ vtBuffer.mouseReleased(col, row);
+ return true;
+ } else if (event.getAction() == MotionEvent.ACTION_MOVE) {
+ int buttonState = event.getButtonState();
+ int button = (buttonState & MotionEvent.BUTTON_PRIMARY) != 0 ? 0 :
+ (buttonState & MotionEvent.BUTTON_SECONDARY) != 0 ? 1 :
+ (buttonState & MotionEvent.BUTTON_TERTIARY) != 0 ? 2 : 3;
+ vtBuffer.mouseMoved(
+ button,
+ col,
+ row,
+ (meta & KeyEvent.META_CTRL_ON) != 0,
+ (meta & KeyEvent.META_SHIFT_ON) != 0,
+ (meta & KeyEvent.META_META_ON) != 0);
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Takes an android mouse event and produces a Java InputEvent modifiers int which can be
+ * passed to vt320.
+ * @param mouseEvent The {@link MotionEvent} which should be a mouse click or release.
+ * @return A Java InputEvent modifier int. See
+ * http://docs.oracle.com/javase/7/docs/api/java/awt/event/InputEvent.html
+ */
+ @TargetApi(14)
+ private static int mouseEventToJavaModifiers(MotionEvent mouseEvent) {
+ if (MotionEventCompat.getSource(mouseEvent) != InputDevice.SOURCE_MOUSE) return 0;
+
+ int mods = 0;
+
+ // See http://docs.oracle.com/javase/7/docs/api/constant-values.html
+ int buttonState = mouseEvent.getButtonState();
+ if ((buttonState & MotionEvent.BUTTON_PRIMARY) != 0)
+ mods |= 16;
+ if ((buttonState & MotionEvent.BUTTON_SECONDARY) != 0)
+ mods |= 8;
+ if ((buttonState & MotionEvent.BUTTON_TERTIARY) != 0)
+ mods |= 4;
+
+ // Note: Meta and Ctrl are intentionally swapped here to keep logic in vt320 simple.
+ int meta = mouseEvent.getMetaState();
+ if ((meta & KeyEvent.META_META_ON) != 0)
+ mods |= 2;
+ if ((meta & KeyEvent.META_SHIFT_ON) != 0)
+ mods |= 1;
+ if ((meta & KeyEvent.META_CTRL_ON) != 0)
+ mods |= 4;
+
+ return mods;
+ }
+
+ @Override
+ @TargetApi(12)
+ public boolean onGenericMotionEvent(MotionEvent event) {
+ if ((MotionEventCompat.getSource(event) & InputDevice.SOURCE_CLASS_POINTER) != 0) {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_SCROLL:
+ // Process scroll wheel movement:
+ float yDistance = MotionEventCompat.getAxisValue(event, MotionEvent.AXIS_VSCROLL);
+ vt320 vtBuffer = (vt320) bridge.buffer;
+ boolean mouseReport = vtBuffer.isMouseReportEnabled();
+ if (mouseReport) {
+ int row = (int) Math.floor(event.getY() / bridge.charHeight);
+ int col = (int) Math.floor(event.getX() / bridge.charWidth);
+
+ vtBuffer.mouseWheel(
+ yDistance > 0,
+ col,
+ row,
+ (event.getMetaState() & KeyEvent.META_CTRL_ON) != 0,
+ (event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0,
+ (event.getMetaState() & KeyEvent.META_META_ON) != 0);
+ return true;
+ } else if (yDistance != 0) {
+ int base = bridge.buffer.getWindowBase();
+ bridge.buffer.setWindowBase(base - Math.round(yDistance));
+ return true;
+ }
+ }
+ }
+ return super.onGenericMotionEvent(event);
+ }
+
+ // TODO: cleanup and possibly optimize
+ private void refreshTextFromBuffer() {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
+ // Do not run this function because the textView is not selectable pre-Honeycomb.
+ return;
+ }
+
+ VDUBuffer vb = bridge.getVDUBuffer();
+
+ String line = "";
+ String buffer = "";
+
+ int windowBase = vb.getWindowBase();
+ int rowBegin = vb.getTopMargin();
+ int rowEnd = vb.getBottomMargin();
+ int numCols = vb.getColumns() - 1;
+
+ for (int r = rowBegin; r <= rowEnd; r++) {
+ for (int c = 0; c < numCols; c++) {
+ line += vb.charArray[windowBase + r][c];
+ }
+ buffer += line.replaceAll("\\s+$", "") + "\n";
+ line = "";
+ }
+
+ setText(buffer);
+ }
+
+ /**
+ * Only intended for pre-Honeycomb devices.
+ */
+ public void startPreHoneycombCopyMode() {
+ // mark as copying and reset any previous bounds
+ SelectionArea area = bridge.getSelectionArea();
+ area.reset();
+ area.setBounds(bridge.buffer.getColumns(), bridge.buffer.getRows());
+
+ bridge.setSelectingForCopy(true);
+
+ // Make sure we show the initial selection
+ bridge.redraw();
}
public void destroy() {
@@ -166,8 +588,21 @@ public class TerminalView extends View implements FontSizeChangedListener {
scaleCursors();
}
- public void onFontSizeChanged(float size) {
+ public void onFontSizeChanged(final float size) {
scaleCursors();
+
+ ((Activity) context).runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ setTextSize(size);
+
+ // For the TextView to line up with the bitmap text, lineHeight must be equal to
+ // the bridge's charHeight. See TextView.getLineHeight(), which has been reversed to
+ // derive lineSpacingMultiplier.
+ float lineSpacingMultiplier = (float) bridge.charHeight / getPaint().getFontMetricsInt(null);
+ setLineSpacing(0.0f, lineSpacingMultiplier);
+ }
+ });
}
private void scaleCursors() {
@@ -246,7 +681,8 @@ public class TerminalView extends View implements FontSizeChangedListener {
}
// draw any highlighted area
- if (bridge.isSelectingForCopy()) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB &&
+ bridge.isSelectingForCopy()) {
SelectionArea area = bridge.getSelectionArea();
canvas.save(Canvas.CLIP_SAVE_FLAG);
canvas.clipRect(
@@ -259,6 +695,8 @@ public class TerminalView extends View implements FontSizeChangedListener {
canvas.restore();
}
}
+
+ super.onDraw(canvas);
}
public void notifyUser(String message) {
@@ -324,37 +762,6 @@ public class TerminalView extends View implements FontSizeChangedListener {
};
}
- @Override
- @TargetApi(12)
- public boolean onGenericMotionEvent(MotionEvent event) {
- if ((MotionEventCompat.getSource(event) & InputDevice.SOURCE_CLASS_POINTER) != 0) {
- switch (event.getAction()) {
- case MotionEvent.ACTION_SCROLL:
- // Process scroll wheel movement:
- float yDistance = MotionEventCompat.getAxisValue(event, MotionEvent.AXIS_VSCROLL);
- boolean mouseReport = ((vt320) bridge.buffer).isMouseReportEnabled();
- if (mouseReport) {
- int row = (int) Math.floor(event.getY() / bridge.charHeight);
- int col = (int) Math.floor(event.getX() / bridge.charWidth);
-
- ((vt320) bridge.buffer).mouseWheel(
- yDistance > 0,
- col,
- row,
- (event.getMetaState() & KeyEvent.META_CTRL_ON) != 0,
- (event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0,
- (event.getMetaState() & KeyEvent.META_META_ON) != 0);
- return true;
- } else if (yDistance != 0) {
- int base = bridge.buffer.getWindowBase();
- bridge.buffer.setWindowBase(base - Math.round(yDistance));
- return true;
- }
- }
- }
- return super.onGenericMotionEvent(event);
- }
-
public void propagateConsoleText(char[] rawText, int length) {
if (mAccessibilityActive) {
synchronized (mAccessibilityLock) {
diff --git a/app/src/main/java/org/connectbot/service/TerminalBridge.java b/app/src/main/java/org/connectbot/service/TerminalBridge.java
index b9e29e8..a888cc3 100644
--- a/app/src/main/java/org/connectbot/service/TerminalBridge.java
+++ b/app/src/main/java/org/connectbot/service/TerminalBridge.java
@@ -341,6 +341,33 @@ public class TerminalBridge implements VDUDisplay {
}
/**
+ * Only intended for pre-Honeycomb devices.
+ */
+ public void setSelectingForCopy(boolean selectingForCopy) {
+ this.selectingForCopy = selectingForCopy;
+ }
+
+ /**
+ * Only intended for pre-Honeycomb devices.
+ */
+ public boolean isSelectingForCopy() {
+ return selectingForCopy;
+ }
+
+ /**
+ * Only intended for pre-Honeycomb devices.
+ */
+ public SelectionArea getSelectionArea() {
+ return selectionArea;
+ }
+
+ public void copyCurrentSelection() {
+ if (parent != null) {
+ parent.copyCurrentSelectionToClipboard();
+ }
+ }
+
+ /**
* Inject a specific string into this terminal. Used for post-login strings
* and pasting clipboard.
*/
@@ -482,18 +509,6 @@ public class TerminalBridge implements VDUDisplay {
}
}
- public void setSelectingForCopy(boolean selectingForCopy) {
- this.selectingForCopy = selectingForCopy;
- }
-
- public boolean isSelectingForCopy() {
- return selectingForCopy;
- }
-
- public SelectionArea getSelectionArea() {
- return selectionArea;
- }
-
public synchronized void tryKeyVibrate() {
manager.tryKeyVibrate();
}
@@ -538,6 +553,10 @@ public class TerminalBridge implements VDUDisplay {
forcedSize = false;
}
+ public float getFontSize() {
+ return fontSizeDp;
+ }
+
/**
* Add an {@link FontSizeChangedListener} to the list of listeners for this
* bridge.
diff --git a/app/src/main/java/org/connectbot/service/TerminalKeyListener.java b/app/src/main/java/org/connectbot/service/TerminalKeyListener.java
index 1b2ffe4..753fa86 100644
--- a/app/src/main/java/org/connectbot/service/TerminalKeyListener.java
+++ b/app/src/main/java/org/connectbot/service/TerminalKeyListener.java
@@ -299,6 +299,14 @@ public class TerminalKeyListener implements OnKeyListener, OnSharedPreferenceCha
return true;
}
+ // CTRL-SHIFT-C to copy.
+ if (keyCode == KeyEvent.KEYCODE_C
+ && (derivedMetaState & HC_META_CTRL_ON) != 0
+ && (derivedMetaState & KeyEvent.META_SHIFT_ON) != 0) {
+ bridge.copyCurrentSelection();
+ return true;
+ }
+
// CTRL-SHIFT-V to paste.
if (keyCode == KeyEvent.KEYCODE_V
&& (derivedMetaState & HC_META_CTRL_ON) != 0
diff --git a/app/src/main/java/org/connectbot/util/TerminalViewPager.java b/app/src/main/java/org/connectbot/util/TerminalViewPager.java
new file mode 100644
index 0000000..bb06b69
--- /dev/null
+++ b/app/src/main/java/org/connectbot/util/TerminalViewPager.java
@@ -0,0 +1,61 @@
+/*
+ * ConnectBot: simple, powerful, open-source SSH client for Android
+ * Copyright 2015 Kenny Root, Jeffrey Sharkey
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.connectbot.util;
+
+import android.content.Context;
+import android.support.v4.view.ViewPager;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+/**
+ * Custom ViewPager {@link ViewPager} which is used to swipe between TerminalViews
+ * {@link org.connectbot.TerminalView}. Also allows temporary disabling of paging
+ * functionality to prevent event intercepts.
+ *
+ * @author rhansby
+ */
+public class TerminalViewPager extends ViewPager {
+ private boolean enabled;
+
+ public TerminalViewPager(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ this.enabled = true;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (this.enabled) {
+ return super.onTouchEvent(event);
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent event) {
+ if (this.enabled) {
+ return super.onInterceptTouchEvent(event);
+ }
+
+ return false;
+ }
+
+ public void setPagingEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+}
diff --git a/app/src/main/res/layout-large/act_console.xml b/app/src/main/res/layout-large/act_console.xml
index 6e7ab14..297d0b1 100644
--- a/app/src/main/res/layout-large/act_console.xml
+++ b/app/src/main/res/layout-large/act_console.xml
@@ -53,7 +53,7 @@
android:text="@string/terminal_no_hosts_connected"
android:textAppearance="?android:attr/textAppearanceMedium"/>
- <android.support.v4.view.ViewPager
+ <org.connectbot.util.TerminalViewPager
android:id="@+id/console_flip"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
diff --git a/app/src/main/res/layout/act_console.xml b/app/src/main/res/layout/act_console.xml
index 34f1d42..fea3a00 100644
--- a/app/src/main/res/layout/act_console.xml
+++ b/app/src/main/res/layout/act_console.xml
@@ -33,7 +33,7 @@
android:text="@string/terminal_no_hosts_connected"
android:textAppearance="?android:attr/textAppearanceMedium"/>
- <android.support.v4.view.ViewPager
+ <org.connectbot.util.TerminalViewPager
android:id="@+id/console_flip"
android:layout_width="fill_parent"
android:layout_height="fill_parent"