From 1a5a66d0ee6726889c2f6c1e92eca67ff519d9a3 Mon Sep 17 00:00:00 2001 From: Trygve Aaberge Date: Mon, 7 Jun 2021 17:48:36 +0200 Subject: [PATCH 1/2] Support clicking directly on a URL to open it This allows you to click/press directly on a URL in the terminal view to open it. It takes priority over opening the keyboard, so if you click on a URL it is opened, and if you click anywhere else the keyboard opens like before. Currently, if the application in the terminal is tracking the mouse and you click on a URL, both actions happen. The mouse event is sent to the application, and the URL is also opened. To enable support for this, you have to set `terminal-onclick-url-open=true` in `termux.properties`. --- .../terminal/TermuxTerminalViewClient.java | 34 ++++++++++------ .../com/termux/terminal/TerminalBuffer.java | 39 +++++++++++++++++++ .../com/termux/terminal/ScreenBufferTest.java | 17 ++++++++ .../java/com/termux/view/TerminalView.java | 14 ++----- .../TextSelectionCursorController.java | 10 +++-- 5 files changed, 90 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/com/termux/app/terminal/TermuxTerminalViewClient.java b/app/src/main/java/com/termux/app/terminal/TermuxTerminalViewClient.java index 62c09ed1..eb62901f 100644 --- a/app/src/main/java/com/termux/app/terminal/TermuxTerminalViewClient.java +++ b/app/src/main/java/com/termux/app/terminal/TermuxTerminalViewClient.java @@ -25,6 +25,7 @@ import com.termux.app.TermuxActivity; import com.termux.shared.data.UrlUtils; import com.termux.shared.file.FileUtils; import com.termux.shared.interact.MessageDialogUtils; +import com.termux.shared.interact.ShareUtils; import com.termux.shared.shell.ShellUtils; import com.termux.shared.terminal.TermuxTerminalViewClientBase; import com.termux.shared.terminal.io.extrakeys.SpecialButton; @@ -42,6 +43,7 @@ import com.termux.shared.termux.TermuxUtils; import com.termux.shared.view.KeyboardUtils; import com.termux.shared.view.ViewUtils; import com.termux.terminal.KeyHandler; +import com.termux.terminal.TerminalBuffer; import com.termux.terminal.TerminalEmulator; import com.termux.terminal.TerminalSession; @@ -172,10 +174,26 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase { @Override public void onSingleTapUp(MotionEvent e) { - if (!KeyboardUtils.areDisableSoftKeyboardFlagsSet(mActivity)) - KeyboardUtils.showSoftKeyboard(mActivity, mActivity.getTerminalView()); - else - Logger.logVerbose(LOG_TAG, "Not showing soft keyboard onSingleTapUp since its disabled"); + TerminalEmulator term = mActivity.getCurrentSession().getEmulator(); + + if (mActivity.getProperties().shouldOpenTerminalTranscriptURLOnClick()) { + int[] xAndY = mActivity.getTerminalView().getTextSelectionCursorController().getXAndYFromEvent(e); + String wordAtTap = term.getScreen().getWordAtLocation(xAndY[0], xAndY[1]); + LinkedHashSet urlSet = UrlUtils.extractUrls(wordAtTap); + + if (!urlSet.isEmpty()) { + String url = (String) urlSet.iterator().next(); + ShareUtils.openURL(mActivity, url); + return; + } + } + + if (!term.isMouseTrackingActive() && !e.isFromSource(InputDevice.SOURCE_MOUSE)) { + if (!KeyboardUtils.areDisableSoftKeyboardFlagsSet(mActivity)) + KeyboardUtils.showSoftKeyboard(mActivity, mActivity.getTerminalView()); + else + Logger.logVerbose(LOG_TAG, "Not showing soft keyboard onSingleTapUp since its disabled"); + } } @Override @@ -670,13 +688,7 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase { lv.setOnItemLongClickListener((parent, view, position, id) -> { dialog.dismiss(); String url = (String) urls[position]; - Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); - try { - mActivity.startActivity(i, null); - } catch (ActivityNotFoundException e) { - // If no applications match, Android displays a system message. - mActivity.startActivity(Intent.createChooser(i, null)); - } + ShareUtils.openURL(mActivity, url); return true; }); }); diff --git a/terminal-emulator/src/main/java/com/termux/terminal/TerminalBuffer.java b/terminal-emulator/src/main/java/com/termux/terminal/TerminalBuffer.java index 1e3af9de..8b898434 100644 --- a/terminal-emulator/src/main/java/com/termux/terminal/TerminalBuffer.java +++ b/terminal-emulator/src/main/java/com/termux/terminal/TerminalBuffer.java @@ -102,6 +102,45 @@ public final class TerminalBuffer { return builder.toString(); } + public String getWordAtLocation(int x, int y) { + // Set y1 and y2 to the lines where the wrapped line starts and ends. + // I.e. if a line that is wrapped to 3 lines starts at line 4, and this + // is called with y=5, then y1 would be set to 4 and y2 would be set to 6. + int y1 = y; + int y2 = y; + while (y1 > 0 && !getSelectedText(0, y1 - 1, mColumns, y, true, true).contains("\n")) { + y1--; + } + while (y2 < mScreenRows && !getSelectedText(0, y, mColumns, y2 + 1, true, true).contains("\n")) { + y2++; + } + + // Get the text for the whole wrapped line + String text = getSelectedText(0, y1, mColumns, y2, true, true); + // The index of x in text + int textOffset = (y - y1) * mColumns + x; + + if (textOffset >= text.length()) { + // The click was to the right of the last word on the line, so + // there's no word to return + return ""; + } + + // Set x1 and x2 to the indices of the last space before x and the + // first space after x in text respectively + int x1 = text.lastIndexOf(' ', textOffset); + int x2 = text.indexOf(' ', textOffset); + if (x2 == -1) { + x2 = text.length(); + } + + if (x1 == x2) { + // The click was on a space, so there's no word to return + return ""; + } + return text.substring(x1 + 1, x2); + } + public int getActiveTranscriptRows() { return mActiveTranscriptRows; } diff --git a/terminal-emulator/src/test/java/com/termux/terminal/ScreenBufferTest.java b/terminal-emulator/src/test/java/com/termux/terminal/ScreenBufferTest.java index 23942bf5..9a8f115e 100644 --- a/terminal-emulator/src/test/java/com/termux/terminal/ScreenBufferTest.java +++ b/terminal-emulator/src/test/java/com/termux/terminal/ScreenBufferTest.java @@ -45,4 +45,21 @@ public class ScreenBufferTest extends TerminalTestCase { withTerminalSized(5, 3).enterString("ABC\r\nFG"); assertEquals("ABC\nFG", mTerminal.getScreen().getSelectedText(0, 0, 1, 1, true, true)); } + + public void testGetWordAtLocation() { + withTerminalSized(5, 3).enterString("ABCDEFGHIJ\r\nKLMNO"); + assertEquals("ABCDEFGHIJKLMNO", mTerminal.getScreen().getWordAtLocation(0, 0)); + assertEquals("ABCDEFGHIJKLMNO", mTerminal.getScreen().getWordAtLocation(4, 1)); + assertEquals("ABCDEFGHIJKLMNO", mTerminal.getScreen().getWordAtLocation(4, 2)); + + withTerminalSized(5, 3).enterString("ABC DEF GHI "); + assertEquals("ABC", mTerminal.getScreen().getWordAtLocation(0, 0)); + assertEquals("", mTerminal.getScreen().getWordAtLocation(3, 0)); + assertEquals("DEF", mTerminal.getScreen().getWordAtLocation(4, 0)); + assertEquals("DEF", mTerminal.getScreen().getWordAtLocation(0, 1)); + assertEquals("DEF", mTerminal.getScreen().getWordAtLocation(1, 1)); + assertEquals("GHI", mTerminal.getScreen().getWordAtLocation(0, 2)); + assertEquals("", mTerminal.getScreen().getWordAtLocation(1, 2)); + assertEquals("", mTerminal.getScreen().getWordAtLocation(2, 2)); + } } diff --git a/terminal-view/src/main/java/com/termux/view/TerminalView.java b/terminal-view/src/main/java/com/termux/view/TerminalView.java index a6ef7106..25773840 100644 --- a/terminal-view/src/main/java/com/termux/view/TerminalView.java +++ b/terminal-view/src/main/java/com/termux/view/TerminalView.java @@ -94,7 +94,7 @@ public final class TerminalView extends View { @Override public boolean onUp(MotionEvent event) { mScrollRemainder = 0.0f; - if (mEmulator != null && mEmulator.isMouseTrackingActive() && !isSelectingText() && !scrolledWithFinger) { + if (mEmulator != null && mEmulator.isMouseTrackingActive() && !event.isFromSource(InputDevice.SOURCE_MOUSE) && !isSelectingText() && !scrolledWithFinger) { // Quick event processing when mouse tracking is active - do not wait for check of double tapping // for zooming. sendMouseEventCode(event, TerminalEmulator.MOUSE_LEFT_BUTTON, true); @@ -114,13 +114,8 @@ public final class TerminalView extends View { return true; } requestFocus(); - if (!mEmulator.isMouseTrackingActive()) { - if (!event.isFromSource(InputDevice.SOURCE_MOUSE)) { - mClient.onSingleTapUp(event); - return true; - } - } - return false; + mClient.onSingleTapUp(event); + return true; } @Override @@ -550,7 +545,6 @@ public final class TerminalView extends View { sendMouseEventCode(event, TerminalEmulator.MOUSE_LEFT_BUTTON_MOVED, true); break; } - return true; } } @@ -1135,7 +1129,7 @@ public final class TerminalView extends View { /** * Define functions required for text selection and its handles. */ - TextSelectionCursorController getTextSelectionCursorController() { + public TextSelectionCursorController getTextSelectionCursorController() { if (mTextSelectionCursorController == null) { mTextSelectionCursorController = new TextSelectionCursorController(this); diff --git a/terminal-view/src/main/java/com/termux/view/textselection/TextSelectionCursorController.java b/terminal-view/src/main/java/com/termux/view/textselection/TextSelectionCursorController.java index 954bf84c..fe137a18 100644 --- a/terminal-view/src/main/java/com/termux/view/textselection/TextSelectionCursorController.java +++ b/terminal-view/src/main/java/com/termux/view/textselection/TextSelectionCursorController.java @@ -88,15 +88,19 @@ public class TextSelectionCursorController implements CursorController { } } - public void setInitialTextSelectionPosition(MotionEvent event) { + public int[] getXAndYFromEvent(MotionEvent event) { int cx = (int) (event.getX() / terminalView.mRenderer.getFontWidth()); final boolean eventFromMouse = event.isFromSource(InputDevice.SOURCE_MOUSE); // Offset for finger: final int SELECT_TEXT_OFFSET_Y = eventFromMouse ? 0 : -40; int cy = (int) ((event.getY() + SELECT_TEXT_OFFSET_Y) / terminalView.mRenderer.getFontLineSpacing()) + terminalView.getTopRow(); + return new int[] { cx, cy }; + } - mSelX1 = mSelX2 = cx; - mSelY1 = mSelY2 = cy; + public void setInitialTextSelectionPosition(MotionEvent event) { + int[] xAndY = getXAndYFromEvent(event); + mSelX1 = mSelX2 = xAndY[0]; + mSelY1 = mSelY2 = xAndY[1]; TerminalBuffer screen = terminalView.mEmulator.getScreen(); if (!" ".equals(screen.getSelectedText(mSelX1, mSelY1, mSelX1, mSelY1))) { From 54bb83de41385b839138ac8d8b6edd75e1b75cab Mon Sep 17 00:00:00 2001 From: Trygve Aaberge Date: Tue, 14 Sep 2021 21:06:16 +0200 Subject: [PATCH 2/2] Fix calculation of row number for selection and URL clicking When calculating the row that is clicked, for mouse tracking mFontLineSpacingAndAscent was taken into account, but for selection and URL clicking it wasn't. This adds a common function for calculating the column and row which does take it into account and use that for all three. I'm not quite sure why it's necessary to subtract mFontLineSpacingAndAscent, but with this calculation the click location matches the line that is acted on for me with both touch and mouse and on different font sizes. It also removes the offset for finger the selection/url used because I don't think it's common for apps on Android to have such an offset, and because the mouse tracking did not use such an offset. --- .../terminal/TermuxTerminalViewClient.java | 4 +-- .../java/com/termux/view/TerminalView.java | 27 ++++++++++++++++--- .../TextSelectionCursorController.java | 15 +++-------- 3 files changed, 29 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/termux/app/terminal/TermuxTerminalViewClient.java b/app/src/main/java/com/termux/app/terminal/TermuxTerminalViewClient.java index eb62901f..0a565a9a 100644 --- a/app/src/main/java/com/termux/app/terminal/TermuxTerminalViewClient.java +++ b/app/src/main/java/com/termux/app/terminal/TermuxTerminalViewClient.java @@ -177,8 +177,8 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase { TerminalEmulator term = mActivity.getCurrentSession().getEmulator(); if (mActivity.getProperties().shouldOpenTerminalTranscriptURLOnClick()) { - int[] xAndY = mActivity.getTerminalView().getTextSelectionCursorController().getXAndYFromEvent(e); - String wordAtTap = term.getScreen().getWordAtLocation(xAndY[0], xAndY[1]); + int[] columnAndRow = mActivity.getTerminalView().getColumnAndRow(e, true); + String wordAtTap = term.getScreen().getWordAtLocation(columnAndRow[0], columnAndRow[1]); LinkedHashSet urlSet = UrlUtils.extractUrls(wordAtTap); if (!urlSet.isEmpty()) { diff --git a/terminal-view/src/main/java/com/termux/view/TerminalView.java b/terminal-view/src/main/java/com/termux/view/TerminalView.java index 25773840..2b56d66e 100644 --- a/terminal-view/src/main/java/com/termux/view/TerminalView.java +++ b/terminal-view/src/main/java/com/termux/view/TerminalView.java @@ -466,10 +466,31 @@ public final class TerminalView extends View { return true; } + /** + * Get the zero indexed column and row of the terminal view for the + * position of the event. + * + * @param event The event with the position to get the column and row for. + * @param relativeToScroll If true the column number will take the scroll + * position into account. E.g. if scrolled 3 lines up and the event + * position is in the top left, column will be -3 if relativeToScroll is + * true and 0 if relativeToScroll is false. + * @return Array with the column and row. + */ + public int[] getColumnAndRow(MotionEvent event, boolean relativeToScroll) { + int column = (int) (event.getX() / mRenderer.mFontWidth); + int row = (int) ((event.getY() - mRenderer.mFontLineSpacingAndAscent) / mRenderer.mFontLineSpacing); + if (relativeToScroll) { + row += mTopRow; + } + return new int[] { column, row }; + } + /** Send a single mouse event code to the terminal. */ void sendMouseEventCode(MotionEvent e, int button, boolean pressed) { - int x = (int) (e.getX() / mRenderer.mFontWidth) + 1; - int y = (int) ((e.getY() - mRenderer.mFontLineSpacingAndAscent) / mRenderer.mFontLineSpacing) + 1; + int[] columnAndRow = getColumnAndRow(e, false); + int x = columnAndRow[0] + 1; + int y = columnAndRow[1] + 1; if (pressed && (button == TerminalEmulator.MOUSE_WHEELDOWN_BUTTON || button == TerminalEmulator.MOUSE_WHEELUP_BUTTON)) { if (mMouseStartDownTime == e.getDownTime()) { x = mMouseScrollStartX; @@ -1129,7 +1150,7 @@ public final class TerminalView extends View { /** * Define functions required for text selection and its handles. */ - public TextSelectionCursorController getTextSelectionCursorController() { + TextSelectionCursorController getTextSelectionCursorController() { if (mTextSelectionCursorController == null) { mTextSelectionCursorController = new TextSelectionCursorController(this); diff --git a/terminal-view/src/main/java/com/termux/view/textselection/TextSelectionCursorController.java b/terminal-view/src/main/java/com/termux/view/textselection/TextSelectionCursorController.java index fe137a18..253e8dea 100644 --- a/terminal-view/src/main/java/com/termux/view/textselection/TextSelectionCursorController.java +++ b/terminal-view/src/main/java/com/termux/view/textselection/TextSelectionCursorController.java @@ -88,19 +88,10 @@ public class TextSelectionCursorController implements CursorController { } } - public int[] getXAndYFromEvent(MotionEvent event) { - int cx = (int) (event.getX() / terminalView.mRenderer.getFontWidth()); - final boolean eventFromMouse = event.isFromSource(InputDevice.SOURCE_MOUSE); - // Offset for finger: - final int SELECT_TEXT_OFFSET_Y = eventFromMouse ? 0 : -40; - int cy = (int) ((event.getY() + SELECT_TEXT_OFFSET_Y) / terminalView.mRenderer.getFontLineSpacing()) + terminalView.getTopRow(); - return new int[] { cx, cy }; - } - public void setInitialTextSelectionPosition(MotionEvent event) { - int[] xAndY = getXAndYFromEvent(event); - mSelX1 = mSelX2 = xAndY[0]; - mSelY1 = mSelY2 = xAndY[1]; + int[] columnAndRow = terminalView.getColumnAndRow(event, true); + mSelX1 = mSelX2 = columnAndRow[0]; + mSelY1 = mSelY2 = columnAndRow[1]; TerminalBuffer screen = terminalView.mEmulator.getScreen(); if (!" ".equals(screen.getSelectedText(mSelX1, mSelY1, mSelX1, mSelY1))) {