From f93f415b77fd777bccbac6c73f2aecfe01464fb8 Mon Sep 17 00:00:00 2001 From: Ryan Hansberry Date: Fri, 2 Oct 2015 17:50:01 -0700 Subject: Text selection now uses TextView. Pre-Honeycomb uses the same method as before. --- .../main/java/org/connectbot/ConsoleActivity.java | 363 +++++---------------- app/src/main/java/org/connectbot/TerminalView.java | 329 ++++++++++++++++--- .../org/connectbot/service/TerminalBridge.java | 44 ++- .../connectbot/service/TerminalKeyListener.java | 8 + 4 files changed, 413 insertions(+), 331 deletions(-) (limited to 'app/src/main/java/org') diff --git a/app/src/main/java/org/connectbot/ConsoleActivity.java b/app/src/main/java/org/connectbot/ConsoleActivity.java index d628a07..e979b34 100644 --- a/app/src/main/java/org/connectbot/ConsoleActivity.java +++ b/app/src/main/java/org/connectbot/ConsoleActivity.java @@ -50,10 +50,9 @@ 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; @@ -61,9 +60,6 @@ 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 +70,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,8 +95,6 @@ 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; @@ -140,7 +133,6 @@ 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; @@ -498,8 +490,9 @@ 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.addOnPageChangeListener( new ViewPager.SimpleOnPageChangeListener() { @Override @@ -669,255 +662,81 @@ 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; - - @Override - public void onLongPress(MotionEvent e) { - super.onLongPress(e); - openContextMenu(pager); - } - + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { + pager.setOnTouchListener(new OnTouchListener() { + public boolean onTouch(View v, MotionEvent event) { + TerminalBridge bridge = adapter.getCurrentTerminalView().bridge; - @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; - } + boolean isCopyingInProgress = + (copySource != null && copySource.isSelectingForCopy()); + if (!isCopyingInProgress && keyboardGroup.getVisibility() == View.GONE) { + showEmulatedKeys(true); } - } - - return false; - } - - - }); - - pager.setLongClickable(true); - pager.setOnTouchListener(new OnTouchListener() { - - public boolean onTouch(View v, MotionEvent event) { - TerminalBridge bridge = adapter.getCurrentTerminalView().bridge; + // when copying, highlight the area + if (isCopyingInProgress) { + SelectionArea area = copySource.getSelectionArea(); + int row = (int) Math.floor(event.getY() / bridge.charHeight); + int col = (int) Math.floor(event.getX() / bridge.charWidth); - // 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; - } - } + 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; - // 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); + // if the user moves, start the selection for other corner + area.finishSelectingOrigin(); - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - // recording starting area - if (area.isSelectingOrigin()) { + // update selected area 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; - } - } + 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; + } - 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); - } + // copy selected area to clipboard + String copiedText = area.copyFrom(copySource.buffer); - // pass any touch events back to detector - return detect.onTouchEvent(event); - } + clipboard.setText(copiedText); + Toast.makeText(ConsoleActivity.this, getString(R.string.console_copy_done, copiedText.length()), Toast.LENGTH_LONG).show(); + // fall through to clear state - /** - * @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(); + case MotionEvent.ACTION_CANCEL: + // make sure we clear any highlighted area + area.reset(); + copySource.setSelectingForCopy(false); + copySource.redraw(); 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; } - - 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; + }); + } } /** @@ -1011,19 +830,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) { + startCopyMode(); + 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 +965,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); @@ -1173,32 +997,6 @@ public class ConsoleActivity extends AppCompatActivity implements BridgeDisconne setVolumeControlStream(AudioManager.STREAM_MUSIC); } - @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,6 +1106,9 @@ public class ConsoleActivity extends AppCompatActivity implements BridgeDisconne super.onSaveInstanceState(savedInstanceState); } + /** + * Only intended for pre-Honeycomb devices. + */ private void startCopyMode() { // mark as copying and reset any previous bounds TerminalView terminalView = (TerminalView) adapter.getCurrentTerminalView(); @@ -1494,7 +1295,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 +1373,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..6d051f6 100644 --- a/app/src/main/java/org/connectbot/TerminalView.java +++ b/app/src/main/java/org/connectbot/TerminalView.java @@ -27,6 +27,7 @@ import org.connectbot.service.TerminalBridge; import org.connectbot.service.TerminalKeyListener; import android.annotation.TargetApi; +import android.annotation.SuppressLint; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; @@ -38,11 +39,19 @@ 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.support.v4.view.ViewPager; +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; @@ -51,6 +60,7 @@ 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; @@ -62,10 +72,18 @@ import de.mud.terminal.vt320; * * @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 ViewPager viewPager; + private GestureDetector gestureDetector; + + private ClipboardManager clipboard; + private ActionMode selectionActionMode = null; + private String currentSelection = ""; + private final Paint paint; private final Paint cursorPaint; private final Paint cursorStrokePaint; @@ -96,17 +114,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, ViewPager 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 +162,7 @@ public class TerminalView extends View implements FontSizeChangedListener { scaleMatrix = new Matrix(); bridge.addFontSizeChangedListener(this); + bridge.terminalView = this; // connect our view up to the bridge setOnKeyListener(bridge.getKeyHandler()); @@ -150,6 +171,261 @@ 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(0x00000000); + setTypeface(Typeface.MONOSPACE); + onFontSizeChanged(bridge.getFontSize()); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + setTextIsSelectable(true); + + this.setCustomSelectionActionModeCallback(new ActionMode.Callback() { + private static final int PASTE = 0; + + @Override + @SuppressLint("NewApi") + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + TerminalView.this.selectionActionMode = mode; + + menu.add(0, PASTE, 2, "Paste") + .setIcon(R.drawable.ic_action_paste) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_WITH_TEXT | MenuItem.SHOW_AS_ACTION_ALWAYS); + + return true; + } + + @Override + @SuppressLint("NewApi") + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + if (item.getItemId() == PASTE) { + String clip = clipboard.getText().toString(); + TerminalView.this.bridge.injectString(clip); + mode.finish(); + return true; + } + + return false; + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return false; + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + } + }); + + 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; + + copyBufferToText(); + } + + return true; + } + + @Override + public boolean onSingleTapConfirmed(MotionEvent e) { + viewPager.performClick(); + return super.onSingleTapConfirmed(e); + } + }); + } + } + + 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 && + selectionActionMode != null) { + selectionActionMode.finish(); + selectionActionMode = null; + } + } + + @Override + protected void onSelectionChanged(int selStart, int selEnd) { + currentSelection = getText().toString().substring(selStart, selEnd); + super.onSelectionChanged(selStart, selEnd); + } + + @Override + public boolean performLongClick() { + copyBufferToText(); + return super.performLongClick(); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { + return false; + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH && + MotionEventCompat.getSource(event) == InputDevice.SOURCE_MOUSE) { + if (onMouseEvent(event, bridge)) { + return true; + } + } + + super.onTouchEvent(event); + if (gestureDetector != null) { + gestureDetector.onTouchEvent(event); + } + + return true; + } + + /** + * @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_TERTIARY: + // Middle click pastes. + String clip = clipboard.getText().toString(); + bridge.injectString(clip); + 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; + } + + 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); + if (yDistance != 0) { + int base = bridge.buffer.getWindowBase(); + bridge.buffer.setWindowBase(base - Math.round(yDistance)); + return true; + } + } + } + return super.onGenericMotionEvent(event); + } + + private void copyBufferToText() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { + // It is pointless to 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); } public void destroy() { @@ -168,6 +444,9 @@ public class TerminalView extends View implements FontSizeChangedListener { public void onFontSizeChanged(float size) { scaleCursors(); + setTextSize(size); + setLineSpacing(0.0f, 1.1f); // KLUDGE: doesnt work on certain font sizes + copyBufferToText(); } private void scaleCursors() { @@ -246,19 +525,22 @@ 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( - area.getLeft() * bridge.charWidth, - area.getTop() * bridge.charHeight, - (area.getRight() + 1) * bridge.charWidth, - (area.getBottom() + 1) * bridge.charHeight + area.getLeft() * bridge.charWidth, + area.getTop() * bridge.charHeight, + (area.getRight() + 1) * bridge.charWidth, + (area.getBottom() + 1) * bridge.charHeight ); canvas.drawPaint(cursorPaint); canvas.restore(); } } + + super.onDraw(canvas); } public void notifyUser(String message) { @@ -324,37 +606,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..b532a20 100644 --- a/app/src/main/java/org/connectbot/service/TerminalBridge.java +++ b/app/src/main/java/org/connectbot/service/TerminalBridge.java @@ -71,6 +71,7 @@ public class TerminalBridge implements VDUDisplay { public int defaultBg = HostDatabase.DEFAULT_BG_COLOR; protected final TerminalManager manager; + public TerminalView terminalView; public HostBean host; @@ -340,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 (terminalView != null) { + terminalView.copyCurrentSelectionToClipboard(); + } + } + /** * Inject a specific string into this terminal. Used for post-login strings * and pasting clipboard. @@ -482,18 +510,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 +554,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 -- cgit v1.2.3