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`.
This commit is contained in:
Trygve Aaberge
2021-06-07 17:48:36 +02:00
parent 865f29d49a
commit 1a5a66d0ee
5 changed files with 90 additions and 24 deletions

View File

@@ -25,6 +25,7 @@ import com.termux.app.TermuxActivity;
import com.termux.shared.data.UrlUtils; import com.termux.shared.data.UrlUtils;
import com.termux.shared.file.FileUtils; import com.termux.shared.file.FileUtils;
import com.termux.shared.interact.MessageDialogUtils; import com.termux.shared.interact.MessageDialogUtils;
import com.termux.shared.interact.ShareUtils;
import com.termux.shared.shell.ShellUtils; import com.termux.shared.shell.ShellUtils;
import com.termux.shared.terminal.TermuxTerminalViewClientBase; import com.termux.shared.terminal.TermuxTerminalViewClientBase;
import com.termux.shared.terminal.io.extrakeys.SpecialButton; 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.KeyboardUtils;
import com.termux.shared.view.ViewUtils; import com.termux.shared.view.ViewUtils;
import com.termux.terminal.KeyHandler; import com.termux.terminal.KeyHandler;
import com.termux.terminal.TerminalBuffer;
import com.termux.terminal.TerminalEmulator; import com.termux.terminal.TerminalEmulator;
import com.termux.terminal.TerminalSession; import com.termux.terminal.TerminalSession;
@@ -172,10 +174,26 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
@Override @Override
public void onSingleTapUp(MotionEvent e) { public void onSingleTapUp(MotionEvent e) {
if (!KeyboardUtils.areDisableSoftKeyboardFlagsSet(mActivity)) TerminalEmulator term = mActivity.getCurrentSession().getEmulator();
KeyboardUtils.showSoftKeyboard(mActivity, mActivity.getTerminalView());
else if (mActivity.getProperties().shouldOpenTerminalTranscriptURLOnClick()) {
Logger.logVerbose(LOG_TAG, "Not showing soft keyboard onSingleTapUp since its disabled"); int[] xAndY = mActivity.getTerminalView().getTextSelectionCursorController().getXAndYFromEvent(e);
String wordAtTap = term.getScreen().getWordAtLocation(xAndY[0], xAndY[1]);
LinkedHashSet<CharSequence> 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 @Override
@@ -670,13 +688,7 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
lv.setOnItemLongClickListener((parent, view, position, id) -> { lv.setOnItemLongClickListener((parent, view, position, id) -> {
dialog.dismiss(); dialog.dismiss();
String url = (String) urls[position]; String url = (String) urls[position];
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); ShareUtils.openURL(mActivity, url);
try {
mActivity.startActivity(i, null);
} catch (ActivityNotFoundException e) {
// If no applications match, Android displays a system message.
mActivity.startActivity(Intent.createChooser(i, null));
}
return true; return true;
}); });
}); });

View File

@@ -102,6 +102,45 @@ public final class TerminalBuffer {
return builder.toString(); 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() { public int getActiveTranscriptRows() {
return mActiveTranscriptRows; return mActiveTranscriptRows;
} }

View File

@@ -45,4 +45,21 @@ public class ScreenBufferTest extends TerminalTestCase {
withTerminalSized(5, 3).enterString("ABC\r\nFG"); withTerminalSized(5, 3).enterString("ABC\r\nFG");
assertEquals("ABC\nFG", mTerminal.getScreen().getSelectedText(0, 0, 1, 1, true, true)); 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));
}
} }

View File

@@ -94,7 +94,7 @@ public final class TerminalView extends View {
@Override @Override
public boolean onUp(MotionEvent event) { public boolean onUp(MotionEvent event) {
mScrollRemainder = 0.0f; 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 // Quick event processing when mouse tracking is active - do not wait for check of double tapping
// for zooming. // for zooming.
sendMouseEventCode(event, TerminalEmulator.MOUSE_LEFT_BUTTON, true); sendMouseEventCode(event, TerminalEmulator.MOUSE_LEFT_BUTTON, true);
@@ -114,13 +114,8 @@ public final class TerminalView extends View {
return true; return true;
} }
requestFocus(); requestFocus();
if (!mEmulator.isMouseTrackingActive()) { mClient.onSingleTapUp(event);
if (!event.isFromSource(InputDevice.SOURCE_MOUSE)) { return true;
mClient.onSingleTapUp(event);
return true;
}
}
return false;
} }
@Override @Override
@@ -550,7 +545,6 @@ public final class TerminalView extends View {
sendMouseEventCode(event, TerminalEmulator.MOUSE_LEFT_BUTTON_MOVED, true); sendMouseEventCode(event, TerminalEmulator.MOUSE_LEFT_BUTTON_MOVED, true);
break; break;
} }
return true;
} }
} }
@@ -1135,7 +1129,7 @@ public final class TerminalView extends View {
/** /**
* Define functions required for text selection and its handles. * Define functions required for text selection and its handles.
*/ */
TextSelectionCursorController getTextSelectionCursorController() { public TextSelectionCursorController getTextSelectionCursorController() {
if (mTextSelectionCursorController == null) { if (mTextSelectionCursorController == null) {
mTextSelectionCursorController = new TextSelectionCursorController(this); mTextSelectionCursorController = new TextSelectionCursorController(this);

View File

@@ -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()); int cx = (int) (event.getX() / terminalView.mRenderer.getFontWidth());
final boolean eventFromMouse = event.isFromSource(InputDevice.SOURCE_MOUSE); final boolean eventFromMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
// Offset for finger: // Offset for finger:
final int SELECT_TEXT_OFFSET_Y = eventFromMouse ? 0 : -40; final int SELECT_TEXT_OFFSET_Y = eventFromMouse ? 0 : -40;
int cy = (int) ((event.getY() + SELECT_TEXT_OFFSET_Y) / terminalView.mRenderer.getFontLineSpacing()) + terminalView.getTopRow(); int cy = (int) ((event.getY() + SELECT_TEXT_OFFSET_Y) / terminalView.mRenderer.getFontLineSpacing()) + terminalView.getTopRow();
return new int[] { cx, cy };
}
mSelX1 = mSelX2 = cx; public void setInitialTextSelectionPosition(MotionEvent event) {
mSelY1 = mSelY2 = cy; int[] xAndY = getXAndYFromEvent(event);
mSelX1 = mSelX2 = xAndY[0];
mSelY1 = mSelY2 = xAndY[1];
TerminalBuffer screen = terminalView.mEmulator.getScreen(); TerminalBuffer screen = terminalView.mEmulator.getScreen();
if (!" ".equals(screen.getSelectedText(mSelX1, mSelY1, mSelX1, mSelY1))) { if (!" ".equals(screen.getSelectedText(mSelX1, mSelY1, mSelX1, mSelY1))) {