From 41682d1147078a3986d3f247167cb8a913693cb8 Mon Sep 17 00:00:00 2001 From: Torge Rosendahl Date: Wed, 15 Feb 2023 17:30:31 -0500 Subject: [PATCH 01/10] added press-and-hold listener to ClickableLinks --- .../ui/text/ClickableLinksDelegate.java | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java b/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java index 482a7eac..6b7573a5 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java @@ -6,11 +6,14 @@ import android.graphics.Paint; import android.graphics.Path; import android.graphics.Rect; import android.graphics.RectF; +import android.os.Handler; import android.text.Layout; import android.text.Spanned; import android.view.MotionEvent; import android.view.SoundEffectConstants; +import android.view.ViewConfiguration; import android.widget.TextView; +import android.widget.Toast; import me.grishka.appkit.utils.V; @@ -20,6 +23,7 @@ public class ClickableLinksDelegate { private Path hlPath; private LinkSpan selectedSpan; private TextView view; + private final Handler longClickHandler = new Handler(); public ClickableLinksDelegate(TextView view) { this.view=view; @@ -63,6 +67,7 @@ public class ClickableLinksDelegate { } hlPath=new Path(); selectedSpan=span; + longClickHandler.postDelayed(copyTextToClipboard, ViewConfiguration.getLongPressTimeout()); hlPaint.setColor((span.getColor() & 0x00FFFFFF) | 0x33000000); //l.getSelectionPath(start, end, hlPath); for(int j=lstart;j<=lend;j++){ @@ -90,21 +95,31 @@ public class ClickableLinksDelegate { } } if(event.getAction()==MotionEvent.ACTION_UP && selectedSpan!=null){ + longClickHandler.removeCallbacks(copyTextToClipboard); view.playSoundEffect(SoundEffectConstants.CLICK); selectedSpan.onClick(view.getContext()); - hlPath=null; - selectedSpan=null; - view.invalidate(); + resetAndInvalidate(); return false; } if(event.getAction()==MotionEvent.ACTION_CANCEL){ - hlPath=null; - selectedSpan=null; - view.invalidate(); + resetAndInvalidate(); return false; } return false; } + + Runnable copyTextToClipboard = () -> { + //TODO actually copy to clipboard + //TODO think about removing toast, system > A12 (?) has a built-in popup + Toast.makeText(view.getContext(), "copied to clipboard", Toast.LENGTH_SHORT).show(); + resetAndInvalidate(); + }; + + private void resetAndInvalidate() { + hlPath=null; + selectedSpan=null; + view.invalidate(); + } public void onDraw(Canvas canvas){ if(hlPath!=null){ From c0115f068c68fa9f0795a5dec0da2a74d2d62a08 Mon Sep 17 00:00:00 2001 From: Torge Rosendahl Date: Wed, 15 Feb 2023 17:40:43 -0500 Subject: [PATCH 02/10] implemented copy service --- .../android/ui/text/ClickableLinksDelegate.java | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java b/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java index 6b7573a5..1684458e 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java @@ -1,11 +1,15 @@ package org.joinmastodon.android.ui.text; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; import android.graphics.Canvas; import android.graphics.CornerPathEffect; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Rect; import android.graphics.RectF; +import android.os.Build; import android.os.Handler; import android.text.Layout; import android.text.Spanned; @@ -109,9 +113,15 @@ public class ClickableLinksDelegate { } Runnable copyTextToClipboard = () -> { - //TODO actually copy to clipboard - //TODO think about removing toast, system > A12 (?) has a built-in popup - Toast.makeText(view.getContext(), "copied to clipboard", Toast.LENGTH_SHORT).show(); + //copy link text to clipboard + ClipboardManager clipboard = (ClipboardManager) view.getContext().getSystemService(Context.CLIPBOARD_SERVICE); + clipboard.setPrimaryClip(ClipData.newPlainText("", selectedSpan.getLink())); + //show toast, android from S_V2 on has built-in popup, as documented in + //https://developer.android.com/develop/ui/views/touch-and-input/copy-paste#duplicate-notifications + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { + Toast.makeText(view.getContext(), "Copied", Toast.LENGTH_SHORT).show(); + } + //reset view resetAndInvalidate(); }; From e3486ebf7c673646ac7456c787ee17599a441eb5 Mon Sep 17 00:00:00 2001 From: Torge Rosendahl Date: Wed, 15 Feb 2023 18:00:45 -0500 Subject: [PATCH 03/10] added clickable link type switch for copy, to not copy hashtags and user IDs --- .../joinmastodon/android/ui/text/ClickableLinksDelegate.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java b/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java index 1684458e..4ae75ca3 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java @@ -113,6 +113,8 @@ public class ClickableLinksDelegate { } Runnable copyTextToClipboard = () -> { + //if target is not a link, don't copy + if (selectedSpan.getType() != LinkSpan.Type.URL) return; //copy link text to clipboard ClipboardManager clipboard = (ClipboardManager) view.getContext().getSystemService(Context.CLIPBOARD_SERVICE); clipboard.setPrimaryClip(ClipData.newPlainText("", selectedSpan.getLink())); From 42c6446125595c4b00970e37ac8208fa18caac06 Mon Sep 17 00:00:00 2001 From: Torge Rosendahl Date: Wed, 15 Feb 2023 18:03:40 -0500 Subject: [PATCH 04/10] refactoring: moved runnable and made it private, added copy toast localization. --- .../ui/text/ClickableLinksDelegate.java | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java b/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java index 4ae75ca3..036d6f9a 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java @@ -19,6 +19,8 @@ import android.view.ViewConfiguration; import android.widget.TextView; import android.widget.Toast; +import org.joinmastodon.android.R; + import me.grishka.appkit.utils.V; public class ClickableLinksDelegate { @@ -28,6 +30,21 @@ public class ClickableLinksDelegate { private LinkSpan selectedSpan; private TextView view; private final Handler longClickHandler = new Handler(); + + private final Runnable copyTextToClipboard = () -> { + //if target is not a link, don't copy + if (selectedSpan.getType() != LinkSpan.Type.URL) return; + //copy link text to clipboard + ClipboardManager clipboard = (ClipboardManager) view.getContext().getSystemService(Context.CLIPBOARD_SERVICE); + clipboard.setPrimaryClip(ClipData.newPlainText("", selectedSpan.getLink())); + //show toast, android from S_V2 on has built-in popup, as documented in + //https://developer.android.com/develop/ui/views/touch-and-input/copy-paste#duplicate-notifications + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { + Toast.makeText(view.getContext(), R.string.text_copied, Toast.LENGTH_SHORT).show(); + } + //reset view + resetAndInvalidate(); + }; public ClickableLinksDelegate(TextView view) { this.view=view; @@ -112,21 +129,6 @@ public class ClickableLinksDelegate { return false; } - Runnable copyTextToClipboard = () -> { - //if target is not a link, don't copy - if (selectedSpan.getType() != LinkSpan.Type.URL) return; - //copy link text to clipboard - ClipboardManager clipboard = (ClipboardManager) view.getContext().getSystemService(Context.CLIPBOARD_SERVICE); - clipboard.setPrimaryClip(ClipData.newPlainText("", selectedSpan.getLink())); - //show toast, android from S_V2 on has built-in popup, as documented in - //https://developer.android.com/develop/ui/views/touch-and-input/copy-paste#duplicate-notifications - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { - Toast.makeText(view.getContext(), "Copied", Toast.LENGTH_SHORT).show(); - } - //reset view - resetAndInvalidate(); - }; - private void resetAndInvalidate() { hlPath=null; selectedSpan=null; From f5df8225d1fb1039f312787d6ad5ddc5da24e0fc Mon Sep 17 00:00:00 2001 From: Torge Rosendahl Date: Wed, 15 Feb 2023 18:20:02 -0500 Subject: [PATCH 05/10] whitespace corrections --- .../joinmastodon/android/ui/text/ClickableLinksDelegate.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java b/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java index 036d6f9a..1906b2f3 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java @@ -45,7 +45,7 @@ public class ClickableLinksDelegate { //reset view resetAndInvalidate(); }; - + public ClickableLinksDelegate(TextView view) { this.view=view; hlPaint=new Paint(); @@ -134,7 +134,7 @@ public class ClickableLinksDelegate { selectedSpan=null; view.invalidate(); } - + public void onDraw(Canvas canvas){ if(hlPath!=null){ canvas.save(); From 794c4e52274b897036449ff39dd5aa8328510c07 Mon Sep 17 00:00:00 2001 From: Torge Rosendahl Date: Wed, 15 Feb 2023 19:54:08 -0500 Subject: [PATCH 06/10] removed longClickHandler and moved to view itself --- .../android/ui/text/ClickableLinksDelegate.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java b/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java index 1906b2f3..6035d2b5 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java @@ -10,7 +10,6 @@ import android.graphics.Path; import android.graphics.Rect; import android.graphics.RectF; import android.os.Build; -import android.os.Handler; import android.text.Layout; import android.text.Spanned; import android.view.MotionEvent; @@ -29,7 +28,6 @@ public class ClickableLinksDelegate { private Path hlPath; private LinkSpan selectedSpan; private TextView view; - private final Handler longClickHandler = new Handler(); private final Runnable copyTextToClipboard = () -> { //if target is not a link, don't copy @@ -88,7 +86,7 @@ public class ClickableLinksDelegate { } hlPath=new Path(); selectedSpan=span; - longClickHandler.postDelayed(copyTextToClipboard, ViewConfiguration.getLongPressTimeout()); + view.postDelayed(copyTextToClipboard, ViewConfiguration.getLongPressTimeout()); hlPaint.setColor((span.getColor() & 0x00FFFFFF) | 0x33000000); //l.getSelectionPath(start, end, hlPath); for(int j=lstart;j<=lend;j++){ @@ -116,7 +114,7 @@ public class ClickableLinksDelegate { } } if(event.getAction()==MotionEvent.ACTION_UP && selectedSpan!=null){ - longClickHandler.removeCallbacks(copyTextToClipboard); + view.removeCallbacks(copyTextToClipboard); view.playSoundEffect(SoundEffectConstants.CLICK); selectedSpan.onClick(view.getContext()); resetAndInvalidate(); From fd99f3caa14bb1d471d67e17a15d9957b711526d Mon Sep 17 00:00:00 2001 From: Torge Rosendahl Date: Wed, 15 Feb 2023 20:14:16 -0500 Subject: [PATCH 07/10] changed url longclick implementation to GestureListener --- .../ui/text/ClickableLinksDelegate.java | 94 +++++++++++-------- 1 file changed, 55 insertions(+), 39 deletions(-) diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java b/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java index 6035d2b5..300938f8 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java @@ -12,12 +12,15 @@ import android.graphics.RectF; import android.os.Build; import android.text.Layout; import android.text.Spanned; +import android.view.GestureDetector; import android.view.MotionEvent; import android.view.SoundEffectConstants; import android.view.ViewConfiguration; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.NonNull; + import org.joinmastodon.android.R; import me.grishka.appkit.utils.V; @@ -29,20 +32,7 @@ public class ClickableLinksDelegate { private LinkSpan selectedSpan; private TextView view; - private final Runnable copyTextToClipboard = () -> { - //if target is not a link, don't copy - if (selectedSpan.getType() != LinkSpan.Type.URL) return; - //copy link text to clipboard - ClipboardManager clipboard = (ClipboardManager) view.getContext().getSystemService(Context.CLIPBOARD_SERVICE); - clipboard.setPrimaryClip(ClipData.newPlainText("", selectedSpan.getLink())); - //show toast, android from S_V2 on has built-in popup, as documented in - //https://developer.android.com/develop/ui/views/touch-and-input/copy-paste#duplicate-notifications - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { - Toast.makeText(view.getContext(), R.string.text_copied, Toast.LENGTH_SHORT).show(); - } - //reset view - resetAndInvalidate(); - }; + GestureDetector gestureDetector; public ClickableLinksDelegate(TextView view) { this.view=view; @@ -50,10 +40,34 @@ public class ClickableLinksDelegate { hlPaint.setAntiAlias(true); hlPaint.setPathEffect(new CornerPathEffect(V.dp(3))); // view.setHighlightColor(view.getResources().getColor(android.R.color.holo_blue_light)); + gestureDetector = new GestureDetector(view.getContext(), new LinkGestureListener(), view.getHandler()); } public boolean onTouch(MotionEvent event) { - if(event.getAction()==MotionEvent.ACTION_DOWN){ + if(event.getAction()==MotionEvent.ACTION_CANCEL){ + resetAndInvalidate(); + } + return gestureDetector.onTouchEvent(event); + } + + private void resetAndInvalidate() { + hlPath=null; + selectedSpan=null; + view.invalidate(); + } + + public void onDraw(Canvas canvas){ + if(hlPath!=null){ + canvas.save(); + canvas.translate(0, view.getPaddingTop()); + canvas.drawPath(hlPath, hlPaint); + canvas.restore(); + } + } + + private class LinkGestureListener extends GestureDetector.SimpleOnGestureListener { + @Override + public boolean onDown(@NonNull MotionEvent event) { int line=-1; Rect rect=new Rect(); Layout l=view.getLayout(); @@ -112,34 +126,36 @@ public class ClickableLinksDelegate { } } } + return super.onDown(event); } - if(event.getAction()==MotionEvent.ACTION_UP && selectedSpan!=null){ - view.removeCallbacks(copyTextToClipboard); - view.playSoundEffect(SoundEffectConstants.CLICK); - selectedSpan.onClick(view.getContext()); - resetAndInvalidate(); + + @Override + public boolean onSingleTapUp(@NonNull MotionEvent event) { + if(selectedSpan!=null){ + view.removeCallbacks(copyTextToClipboard); + view.playSoundEffect(SoundEffectConstants.CLICK); + selectedSpan.onClick(view.getContext()); + resetAndInvalidate(); + return true; + } return false; } - if(event.getAction()==MotionEvent.ACTION_CANCEL){ + + @Override + public void onLongPress(@NonNull MotionEvent event) { + //if target is not a link, don't copy + if (selectedSpan == null) return; + if (selectedSpan.getType() != LinkSpan.Type.URL) return; + //copy link text to clipboard + ClipboardManager clipboard = (ClipboardManager) view.getContext().getSystemService(Context.CLIPBOARD_SERVICE); + clipboard.setPrimaryClip(ClipData.newPlainText("", selectedSpan.getLink())); + //show toast, android from S_V2 on has built-in popup, as documented in + //https://developer.android.com/develop/ui/views/touch-and-input/copy-paste#duplicate-notifications + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { + Toast.makeText(view.getContext(), R.string.text_copied, Toast.LENGTH_SHORT).show(); + } + //reset view resetAndInvalidate(); - return false; - } - return false; - } - - private void resetAndInvalidate() { - hlPath=null; - selectedSpan=null; - view.invalidate(); - } - - public void onDraw(Canvas canvas){ - if(hlPath!=null){ - canvas.save(); - canvas.translate(0, view.getPaddingTop()); - canvas.drawPath(hlPath, hlPaint); - canvas.restore(); } } - } From 0a8d73dc0bd8e5fd517f4e1654771c9928b4a466 Mon Sep 17 00:00:00 2001 From: Torge Rosendahl Date: Wed, 15 Feb 2023 20:19:10 -0500 Subject: [PATCH 08/10] cleanup, resolved some warnings --- .../android/ui/text/ClickableLinksDelegate.java | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java b/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java index 300938f8..be992190 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java @@ -15,7 +15,6 @@ import android.text.Spanned; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.SoundEffectConstants; -import android.view.ViewConfiguration; import android.widget.TextView; import android.widget.Toast; @@ -27,12 +26,12 @@ import me.grishka.appkit.utils.V; public class ClickableLinksDelegate { - private Paint hlPaint; + final private Paint hlPaint; private Path hlPath; private LinkSpan selectedSpan; - private TextView view; + final private TextView view; - GestureDetector gestureDetector; + final GestureDetector gestureDetector; public ClickableLinksDelegate(TextView view) { this.view=view; @@ -82,8 +81,7 @@ public class ClickableLinksDelegate { return false; } CharSequence text=view.getText(); - if(text instanceof Spanned){ - Spanned s=(Spanned)text; + if(text instanceof Spanned s){ LinkSpan[] spans=s.getSpans(0, s.length()-1, LinkSpan.class); if(spans.length>0){ for(LinkSpan span:spans){ @@ -100,7 +98,6 @@ public class ClickableLinksDelegate { } hlPath=new Path(); selectedSpan=span; - view.postDelayed(copyTextToClipboard, ViewConfiguration.getLongPressTimeout()); hlPaint.setColor((span.getColor() & 0x00FFFFFF) | 0x33000000); //l.getSelectionPath(start, end, hlPath); for(int j=lstart;j<=lend;j++){ @@ -132,7 +129,6 @@ public class ClickableLinksDelegate { @Override public boolean onSingleTapUp(@NonNull MotionEvent event) { if(selectedSpan!=null){ - view.removeCallbacks(copyTextToClipboard); view.playSoundEffect(SoundEffectConstants.CLICK); selectedSpan.onClick(view.getContext()); resetAndInvalidate(); From 4144639b7564a9d3be9306f165c8be63e493b9ae Mon Sep 17 00:00:00 2001 From: Torge Rosendahl Date: Sun, 26 Feb 2023 13:59:39 -0500 Subject: [PATCH 09/10] docu --- .../android/ui/text/ClickableLinksDelegate.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java b/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java index be992190..9dac109c 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java @@ -44,11 +44,16 @@ public class ClickableLinksDelegate { public boolean onTouch(MotionEvent event) { if(event.getAction()==MotionEvent.ACTION_CANCEL){ + // the gestureDetector does not provide a callback for CANCEL, therefore: + // remove background color of view before passing event to gestureDetector resetAndInvalidate(); } return gestureDetector.onTouchEvent(event); } + /** + * remove highlighting from span and let the system redraw the view + */ private void resetAndInvalidate() { hlPath=null; selectedSpan=null; @@ -64,6 +69,12 @@ public class ClickableLinksDelegate { } } + /** + * GestureListener for spans that represent URLs. + * onDown: on start of touch event, set highlighting + * onSingleTapUp: when there was a (short) tap, call onClick and reset highlighting + * onLongPress: copy URL to clipboard, let user know, reset highlighting + */ private class LinkGestureListener extends GestureDetector.SimpleOnGestureListener { @Override public boolean onDown(@NonNull MotionEvent event) { From f79fc66578d350d5323ed0be08d5e67062d87e90 Mon Sep 17 00:00:00 2001 From: Grishka Date: Sun, 5 Mar 2023 22:33:18 +0300 Subject: [PATCH 10/10] Fix --- .../android/ui/text/ClickableLinksDelegate.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java b/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java index 9dac109c..e7b21994 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java @@ -26,12 +26,12 @@ import me.grishka.appkit.utils.V; public class ClickableLinksDelegate { - final private Paint hlPaint; + private final Paint hlPaint; private Path hlPath; private LinkSpan selectedSpan; - final private TextView view; + private final TextView view; - final GestureDetector gestureDetector; + private final GestureDetector gestureDetector; public ClickableLinksDelegate(TextView view) { this.view=view;