diff options
author | Christian Schneppe <christian@pix-art.de> | 2016-12-25 21:11:20 +0100 |
---|---|---|
committer | Christian Schneppe <christian@pix-art.de> | 2016-12-25 21:11:20 +0100 |
commit | 1924cfbd53b371c6c0e5f7b5fa719ec62ed9aac6 (patch) | |
tree | ed95ad768802027c48476b141be9080bb295197f /src/main/java/de/pixart/messenger | |
parent | 37aedc49f36041abc7f35132e13241c26338ba99 (diff) |
support for quoting messages
Diffstat (limited to 'src/main/java/de/pixart/messenger')
6 files changed, 303 insertions, 8 deletions
diff --git a/src/main/java/de/pixart/messenger/ui/ConversationFragment.java b/src/main/java/de/pixart/messenger/ui/ConversationFragment.java index 01fc47afe..5363b3db6 100644 --- a/src/main/java/de/pixart/messenger/ui/ConversationFragment.java +++ b/src/main/java/de/pixart/messenger/ui/ConversationFragment.java @@ -11,6 +11,7 @@ import android.content.Intent; import android.content.IntentSender.SendIntentException; import android.os.Bundle; import android.os.Handler; +import android.text.Editable; import android.text.InputType; import android.util.Log; import android.util.Pair; @@ -519,6 +520,34 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa } } }); + messageListAdapter.setOnQuoteListener(new MessageAdapter.OnQuoteListener() { + + @Override + public void onQuote(String text) { + if (mEditMessage.isEnabled()) { + text = text.replaceAll("(\n *){2,}", "\n").replaceAll("(^|\n)", "$1> ").replaceAll("\n$", ""); + Editable editable = mEditMessage.getEditableText(); + int position = mEditMessage.getSelectionEnd(); + if (position == -1) position = editable.length(); + if (position > 0 && editable.charAt(position - 1) != '\n') { + editable.insert(position++, "\n"); + } + editable.insert(position, text); + position += text.length(); + editable.insert(position++, "\n"); + if (position < editable.length() && editable.charAt(position) != '\n') { + editable.insert(position, "\n"); + } + mEditMessage.setSelection(position); + mEditMessage.requestFocus(); + InputMethodManager inputMethodManager = (InputMethodManager) getActivity() + .getSystemService(Context.INPUT_METHOD_SERVICE); + if (inputMethodManager != null) { + inputMethodManager.showSoftInput(mEditMessage, InputMethodManager.SHOW_IMPLICIT); + } + } + } + }); messagesView.setAdapter(messageListAdapter); registerForContextMenu(messagesView); diff --git a/src/main/java/de/pixart/messenger/ui/XmppActivity.java b/src/main/java/de/pixart/messenger/ui/XmppActivity.java index 17d2ed3c4..9a9ad43a5 100644 --- a/src/main/java/de/pixart/messenger/ui/XmppActivity.java +++ b/src/main/java/de/pixart/messenger/ui/XmppActivity.java @@ -22,6 +22,7 @@ import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.Resources; +import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Point; import android.graphics.drawable.BitmapDrawable; @@ -414,6 +415,16 @@ public abstract class XmppActivity extends Activity { } } + public int getThemeResource(int r_attr_name, int r_drawable_def) { + int[] attrs = { r_attr_name }; + TypedArray ta = this.getTheme().obtainStyledAttributes(attrs); + + int res = ta.getResourceId(0, r_drawable_def); + ta.recycle(); + + return res; + } + protected boolean isOptimizingBattery() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE); diff --git a/src/main/java/de/pixart/messenger/ui/adapter/MessageAdapter.java b/src/main/java/de/pixart/messenger/ui/adapter/MessageAdapter.java index 71742281d..0b98614b7 100644 --- a/src/main/java/de/pixart/messenger/ui/adapter/MessageAdapter.java +++ b/src/main/java/de/pixart/messenger/ui/adapter/MessageAdapter.java @@ -25,7 +25,10 @@ import android.text.util.Linkify; import android.util.DisplayMetrics; import android.util.Log; import android.util.Patterns; +import android.view.ActionMode; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnLongClickListener; @@ -62,6 +65,8 @@ import de.pixart.messenger.entities.Transferable; import de.pixart.messenger.persistance.FileBackend; import de.pixart.messenger.ui.ConversationActivity; import de.pixart.messenger.ui.ShowFullscreenMessageActivity; +import de.pixart.messenger.ui.text.DividerSpan; +import de.pixart.messenger.ui.text.QuoteSpan; import de.pixart.messenger.ui.widget.ClickableMovementMethod; import de.pixart.messenger.ui.widget.CopyTextView; import de.pixart.messenger.ui.widget.ListSelectionManager; @@ -93,6 +98,7 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie private OnContactPictureLongClicked mOnContactPictureLongClickedListener; private boolean mIndicateReceived = false; + private OnQuoteListener onQuoteListener; private final ListSelectionManager listSelectionManager = new ListSelectionManager(); private HashMap<Integer, AudioWife> audioPlayer; private boolean mUseWhiteBackground = false; @@ -113,6 +119,10 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie this.mOnContactPictureLongClickedListener = listener; } + public void setOnQuoteListener(OnQuoteListener listener) { + this.onQuoteListener = listener; + } + @Override public int getViewTypeCount() { return 3; @@ -302,7 +312,7 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie viewHolder.messageBody.setIncludeFontPadding(false); Spannable span = new SpannableString(body); span.setSpan(new RelativeSizeSpan(4.0f), 0, body.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - span.setSpan(new ForegroundColorSpan(activity.getWarningTextColor()), 0, body.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + span.setSpan(new ForegroundColorSpan(activity.getWarningTextColor()), 0, body.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); viewHolder.messageBody.setText(span); } @@ -328,6 +338,74 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie } + private int applyQuoteSpan(SpannableStringBuilder body, int start, int end, boolean darkBackground) { + if (start > 1 && !"\n\n".equals(body.subSequence(start - 2, start).toString())) { + body.insert(start++, "\n"); + body.setSpan(new DividerSpan(false), start - 2, start, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + end++; + } + if (end < body.length() - 1 && !"\n\n".equals(body.subSequence(end, end + 2).toString())) { + body.insert(end, "\n"); + body.setSpan(new DividerSpan(false), end, end + 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + int color = darkBackground ? this.getMessageTextColor(darkBackground, false) + : getContext().getResources().getColor(R.color.bubble); + DisplayMetrics metrics = getContext().getResources().getDisplayMetrics(); + body.setSpan(new QuoteSpan(color, metrics), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + return 0; + } + + /** + * Applies QuoteSpan to group of lines which starts with > or ยป characters. + * Appends likebreaks and applies DividerSpan to them to show a padding between quote and text. + */ + private boolean handleTextQuotes(SpannableStringBuilder body, boolean darkBackground) { + boolean startsWithQuote = false; + char previous = '\n'; + int lineStart = -1; + int lineTextStart = -1; + int quoteStart = -1; + for (int i = 0; i <= body.length(); i++) { + char current = body.length() > i ? body.charAt(i) : '\n'; + if (lineStart == -1) { + if (previous == '\n') { + if (current == '>' || current == '\u00bb') { + // Line start with quote + lineStart = i; + if (quoteStart == -1) quoteStart = i; + if (i == 0) startsWithQuote = true; + } else if (quoteStart >= 0) { + // Line start without quote, apply spans there + applyQuoteSpan(body, quoteStart, i - 1, darkBackground); + quoteStart = -1; + } + } + } else { + // Remove extra spaces between > and first character in the line + // > character will be removed too + if (current != ' ' && lineTextStart == -1) { + lineTextStart = i; + } + if (current == '\n') { + body.delete(lineStart, lineTextStart); + i -= lineTextStart - lineStart; + if (i == lineStart) { + // Avoid empty lines because span over empty line can be hidden + body.insert(i++, " "); + } + lineStart = -1; + lineTextStart = -1; + } + } + previous = current; + } + if (quoteStart >= 0) { + // Apply spans to finishing open quote + applyQuoteSpan(body, quoteStart, body.length(), darkBackground); + } + return startsWithQuote; + } + private void displayTextMessage(final ViewHolder viewHolder, final Message message, boolean darkBackground) { if (viewHolder.download_button != null) { viewHolder.download_button.setVisibility(View.GONE); @@ -350,8 +428,9 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie for (Message.MergeSeparator mergeSeparator : mergeSeparators) { int start = body.getSpanStart(mergeSeparator); int end = body.getSpanEnd(mergeSeparator); - body.setSpan(new RelativeSizeSpan(0.3f), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + body.setSpan(new DividerSpan(true), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } + boolean startsWithQuote = handleTextQuotes(body, darkBackground); if (message.getType() != Message.TYPE_PRIVATE) { if (hasMeCommand) { body.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), 0, nick.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); @@ -372,7 +451,13 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie } body.insert(0, privateMarker); int privateMarkerIndex = privateMarker.length(); - body.insert(privateMarkerIndex, " "); + if (startsWithQuote) { + body.insert(privateMarkerIndex, "\n\n"); + body.setSpan(new DividerSpan(false), privateMarkerIndex, privateMarkerIndex + 2, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } else { + body.insert(privateMarkerIndex, " "); + } body.setSpan(new ForegroundColorSpan(getMessageTextColor(darkBackground, false)), 0, privateMarkerIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); body.setSpan(new StyleSpan(Typeface.BOLD), 0, privateMarkerIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); if (hasMeCommand) { @@ -641,7 +726,7 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie break; } if (viewHolder.messageBody != null) { - listSelectionManager.onCreate(viewHolder.messageBody); + listSelectionManager.onCreate(viewHolder.messageBody, new MessageBodyActionModeCallback(viewHolder.messageBody)); viewHolder.messageBody.setCopyHandler(this); } view.setTag(viewHolder); @@ -813,9 +898,84 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie listSelectionManager.onAfterNotifyDataSetChanged(); } + private String transformText(CharSequence text, int start, int end, boolean forCopy) { + SpannableStringBuilder builder = new SpannableStringBuilder(text); + Object copySpan = new Object(); + builder.setSpan(copySpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + DividerSpan[] dividerSpans = builder.getSpans(0, builder.length(), DividerSpan.class); + for (DividerSpan dividerSpan : dividerSpans) { + builder.replace(builder.getSpanStart(dividerSpan), builder.getSpanEnd(dividerSpan), + dividerSpan.isLarge() ? "\n\n" : "\n"); + } + start = builder.getSpanStart(copySpan); + end = builder.getSpanEnd(copySpan); + if (start == -1 || end == -1) return ""; + builder = new SpannableStringBuilder(builder, start, end); + if (forCopy) { + QuoteSpan[] quoteSpans = builder.getSpans(0, builder.length(), QuoteSpan.class); + for (QuoteSpan quoteSpan : quoteSpans) { + builder.insert(builder.getSpanStart(quoteSpan), "> "); + } + } + return builder.toString(); + } + @Override public String transformTextForCopy(CharSequence text, int start, int end) { - return text.toString().substring(start, end); + if (text instanceof Spanned) { + return transformText(text, start, end, true); + } else { + return text.toString().substring(start, end); + } + } + + public interface OnQuoteListener { + public void onQuote(String text); + } + + private class MessageBodyActionModeCallback implements ActionMode.Callback { + + private final TextView textView; + + public MessageBodyActionModeCallback(TextView textView) { + this.textView = textView; + } + + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + if (onQuoteListener != null) { + int quoteResId = activity.getThemeResource(R.attr.icon_quote, R.drawable.ic_action_reply); + // 3rd item is placed after "copy" item + menu.add(0, android.R.id.button1, 3, R.string.quote).setIcon(quoteResId) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT); + } + return false; + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return false; + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + if (item.getItemId() == android.R.id.button1) { + int start = textView.getSelectionStart(); + int end = textView.getSelectionEnd(); + if (end > start) { + String text = transformText(textView.getText(), start, end, false); + if (onQuoteListener != null) { + onQuoteListener.onQuote(text); + } + mode.finish(); + } + return true; + } + return false; + } + + @Override + public void onDestroyActionMode(ActionMode mode) {} } public void openDownloadable(Message message) { diff --git a/src/main/java/de/pixart/messenger/ui/text/DividerSpan.java b/src/main/java/de/pixart/messenger/ui/text/DividerSpan.java new file mode 100644 index 000000000..672a267be --- /dev/null +++ b/src/main/java/de/pixart/messenger/ui/text/DividerSpan.java @@ -0,0 +1,29 @@ +package de.pixart.messenger.ui.text; + +import android.text.TextPaint; +import android.text.style.MetricAffectingSpan; + +public class DividerSpan extends MetricAffectingSpan { + + private static final float PROPORTION = 0.3f; + + private final boolean large; + + public DividerSpan(boolean large) { + this.large = large; + } + + public boolean isLarge() { + return large; + } + + @Override + public void updateDrawState(TextPaint tp) { + tp.setTextSize(tp.getTextSize() * PROPORTION); + } + + @Override + public void updateMeasureState(TextPaint p) { + p.setTextSize(p.getTextSize() * PROPORTION); + } +}
\ No newline at end of file diff --git a/src/main/java/de/pixart/messenger/ui/text/QuoteSpan.java b/src/main/java/de/pixart/messenger/ui/text/QuoteSpan.java new file mode 100644 index 000000000..a24575509 --- /dev/null +++ b/src/main/java/de/pixart/messenger/ui/text/QuoteSpan.java @@ -0,0 +1,52 @@ +package de.pixart.messenger.ui.text; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.text.Layout; +import android.text.TextPaint; +import android.text.style.CharacterStyle; +import android.text.style.LeadingMarginSpan; +import android.util.DisplayMetrics; +import android.util.TypedValue; + +public class QuoteSpan extends CharacterStyle implements LeadingMarginSpan { + + private final int color; + + private final int width; + private final int paddingLeft; + private final int paddingRight; + + private static final float WIDTH_SP = 2f; + private static final float PADDING_LEFT_SP = 1.5f; + private static final float PADDING_RIGHT_SP = 8f; + + public QuoteSpan(int color, DisplayMetrics metrics) { + this.color = color; + this.width = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, WIDTH_SP, metrics); + this.paddingLeft = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, PADDING_LEFT_SP, metrics); + this.paddingRight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, PADDING_RIGHT_SP, metrics); + } + + @Override + public void updateDrawState(TextPaint tp) { + tp.setColor(this.color); + } + + @Override + public int getLeadingMargin(boolean first) { + return paddingLeft + width + paddingRight; + } + + @Override + public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom, + CharSequence text, int start, int end, boolean first, Layout layout) { + Paint.Style style = p.getStyle(); + int color = p.getColor(); + p.setStyle(Paint.Style.FILL); + p.setColor(this.color); + c.drawRect(x + dir * paddingLeft, top, x + dir * (paddingLeft + width), bottom, p); + p.setStyle(style); + p.setColor(color); + } +}
\ No newline at end of file diff --git a/src/main/java/de/pixart/messenger/ui/widget/ListSelectionManager.java b/src/main/java/de/pixart/messenger/ui/widget/ListSelectionManager.java index 487cf3a0a..7ccf94dbe 100644 --- a/src/main/java/de/pixart/messenger/ui/widget/ListSelectionManager.java +++ b/src/main/java/de/pixart/messenger/ui/widget/ListSelectionManager.java @@ -69,8 +69,8 @@ public class ListSelectionManager { private int futureSelectionStart; private int futureSelectionEnd; - public void onCreate(TextView textView) { - final CustomCallback callback = new CustomCallback(textView); + public void onCreate(TextView textView, ActionMode.Callback additionalCallback) { + final CustomCallback callback = new CustomCallback(textView, additionalCallback); textView.setCustomSelectionActionModeCallback(callback); } @@ -112,10 +112,12 @@ public class ListSelectionManager { private class CustomCallback implements ActionMode.Callback { private final TextView textView; + private final ActionMode.Callback additionalCallback; public Object identifier; - public CustomCallback(TextView textView) { + public CustomCallback(TextView textView, ActionMode.Callback additionalCallback) { this.textView = textView; + this.additionalCallback = additionalCallback; } @Override @@ -123,21 +125,33 @@ public class ListSelectionManager { selectionActionMode = mode; selectionIdentifier = identifier; selectionTextView = textView; + if (additionalCallback != null) { + additionalCallback.onCreateActionMode(mode, menu); + } return true; } @Override public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + if (additionalCallback != null) { + additionalCallback.onPrepareActionMode(mode, menu); + } return true; } @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + if (additionalCallback != null && additionalCallback.onActionItemClicked(mode, item)) { + return true; + } return false; } @Override public void onDestroyActionMode(ActionMode mode) { + if (additionalCallback != null) { + additionalCallback.onDestroyActionMode(mode); + } if (selectionActionMode == mode) { selectionActionMode = null; selectionIdentifier = null; |