diff options
8 files changed, 597 insertions, 377 deletions
diff --git a/app/src/main/java/de/mud/terminal/vt320.java b/app/src/main/java/de/mud/terminal/vt320.java index dc95bea..3c929b2 100644 --- a/app/src/main/java/de/mud/terminal/vt320.java +++ b/app/src/main/java/de/mud/terminal/vt320.java @@ -672,6 +672,7 @@ public void setScreenSize(int c, int r, boolean broadcast) { boolean capslock = false; boolean numlock = false; int mouserpt = 0; + int mouserptSaved = 0; byte mousebut = 0; boolean useibmcharset = false; @@ -2197,9 +2198,20 @@ public void setScreenSize(int c, int r, boolean broadcast) { DCEvars[DCEvar] = 0; term_state = TSTATE_DCEQ; break; - case 's': // XTERM_SAVE missing! - if (true || debug > 1) - debug("ESC [ ? " + DCEvars[0] + " s unimplemented!"); + case 's': + for (int i = 0; i <= DCEvar; i++) { + switch (DCEvars[i]) { + case 9: + case 1000: + case 1001: + case 1002: + case 1003: + mouserptSaved = mouserpt; + break; + default: + debug("ESC [ ? " + DCEvars[0] + " s, unimplemented!"); + } + } break; case 'r': // XTERM_RESTORE if (true || debug > 1) @@ -2227,7 +2239,7 @@ public void setScreenSize(int c, int r, boolean broadcast) { case 1001: case 1002: case 1003: - mouserpt = DCEvars[i]; + mouserpt = mouserptSaved; break; default: debug("ESC [ ? " + DCEvars[0] + " r, unimplemented!"); 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" |