Merge branch 'feature-selection' of https://github.com/Mishiranu/Conversations into Mishiranu-feature-selection
This commit is contained in:
commit
f8c21caec9
|
@ -62,6 +62,7 @@ import eu.siacs.conversations.ui.XmppActivity.OnValueEdited;
|
||||||
import eu.siacs.conversations.ui.adapter.MessageAdapter;
|
import eu.siacs.conversations.ui.adapter.MessageAdapter;
|
||||||
import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureClicked;
|
import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureClicked;
|
||||||
import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureLongClicked;
|
import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureLongClicked;
|
||||||
|
import eu.siacs.conversations.ui.widget.ListSelectionManager;
|
||||||
import eu.siacs.conversations.utils.GeoHelper;
|
import eu.siacs.conversations.utils.GeoHelper;
|
||||||
import eu.siacs.conversations.utils.UIHelper;
|
import eu.siacs.conversations.utils.UIHelper;
|
||||||
import eu.siacs.conversations.xmpp.XmppConnection;
|
import eu.siacs.conversations.xmpp.XmppConnection;
|
||||||
|
@ -536,6 +537,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
|
||||||
activity.getMenuInflater().inflate(R.menu.message_context, menu);
|
activity.getMenuInflater().inflate(R.menu.message_context, menu);
|
||||||
menu.setHeaderTitle(R.string.message_options);
|
menu.setHeaderTitle(R.string.message_options);
|
||||||
MenuItem copyText = menu.findItem(R.id.copy_text);
|
MenuItem copyText = menu.findItem(R.id.copy_text);
|
||||||
|
MenuItem selectText = menu.findItem(R.id.select_text);
|
||||||
MenuItem retryDecryption = menu.findItem(R.id.retry_decryption);
|
MenuItem retryDecryption = menu.findItem(R.id.retry_decryption);
|
||||||
MenuItem correctMessage = menu.findItem(R.id.correct_message);
|
MenuItem correctMessage = menu.findItem(R.id.correct_message);
|
||||||
MenuItem shareWith = menu.findItem(R.id.share_with);
|
MenuItem shareWith = menu.findItem(R.id.share_with);
|
||||||
|
@ -548,6 +550,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
|
||||||
&& !GeoHelper.isGeoUri(m.getBody())
|
&& !GeoHelper.isGeoUri(m.getBody())
|
||||||
&& m.treatAsDownloadable() != Message.Decision.MUST) {
|
&& m.treatAsDownloadable() != Message.Decision.MUST) {
|
||||||
copyText.setVisible(true);
|
copyText.setVisible(true);
|
||||||
|
selectText.setVisible(ListSelectionManager.isSupported());
|
||||||
}
|
}
|
||||||
if (m.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) {
|
if (m.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) {
|
||||||
retryDecryption.setVisible(true);
|
retryDecryption.setVisible(true);
|
||||||
|
@ -598,6 +601,9 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
|
||||||
case R.id.copy_text:
|
case R.id.copy_text:
|
||||||
copyText(selectedMessage);
|
copyText(selectedMessage);
|
||||||
return true;
|
return true;
|
||||||
|
case R.id.select_text:
|
||||||
|
selectText(selectedMessage);
|
||||||
|
return true;
|
||||||
case R.id.correct_message:
|
case R.id.correct_message:
|
||||||
correctMessage(selectedMessage);
|
correctMessage(selectedMessage);
|
||||||
return true;
|
return true;
|
||||||
|
@ -657,6 +663,24 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void selectText(Message message) {
|
||||||
|
final int index;
|
||||||
|
synchronized (this.messageList) {
|
||||||
|
index = this.messageList.indexOf(message);
|
||||||
|
}
|
||||||
|
if (index >= 0) {
|
||||||
|
final int first = this.messagesView.getFirstVisiblePosition();
|
||||||
|
final int last = first + this.messagesView.getChildCount();
|
||||||
|
if (index >= first && index < last) {
|
||||||
|
final View view = this.messagesView.getChildAt(index - first);
|
||||||
|
final TextView messageBody = this.messageListAdapter.getMessageBody(view);
|
||||||
|
if (messageBody != null) {
|
||||||
|
ListSelectionManager.startSelection(messageBody);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void deleteFile(Message message) {
|
private void deleteFile(Message message) {
|
||||||
if (activity.xmppConnectionService.getFileBackend().deleteFile(message)) {
|
if (activity.xmppConnectionService.getFileBackend().deleteFile(message)) {
|
||||||
message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED));
|
message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED));
|
||||||
|
|
|
@ -54,6 +54,7 @@ import eu.siacs.conversations.entities.Transferable;
|
||||||
import eu.siacs.conversations.persistance.FileBackend;
|
import eu.siacs.conversations.persistance.FileBackend;
|
||||||
import eu.siacs.conversations.ui.ConversationActivity;
|
import eu.siacs.conversations.ui.ConversationActivity;
|
||||||
import eu.siacs.conversations.ui.widget.ClickableMovementMethod;
|
import eu.siacs.conversations.ui.widget.ClickableMovementMethod;
|
||||||
|
import eu.siacs.conversations.ui.widget.ListSelectionManager;
|
||||||
import eu.siacs.conversations.utils.CryptoHelper;
|
import eu.siacs.conversations.utils.CryptoHelper;
|
||||||
import eu.siacs.conversations.utils.GeoHelper;
|
import eu.siacs.conversations.utils.GeoHelper;
|
||||||
import eu.siacs.conversations.utils.UIHelper;
|
import eu.siacs.conversations.utils.UIHelper;
|
||||||
|
@ -87,6 +88,8 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
||||||
private boolean mIndicateReceived = false;
|
private boolean mIndicateReceived = false;
|
||||||
private boolean mUseGreenBackground = false;
|
private boolean mUseGreenBackground = false;
|
||||||
|
|
||||||
|
private final ListSelectionManager listSelectionManager = new ListSelectionManager();
|
||||||
|
|
||||||
public MessageAdapter(ConversationActivity activity, List<Message> messages) {
|
public MessageAdapter(ConversationActivity activity, List<Message> messages) {
|
||||||
super(activity, 0, messages);
|
super(activity, 0, messages);
|
||||||
this.activity = activity;
|
this.activity = activity;
|
||||||
|
@ -362,6 +365,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
||||||
viewHolder.messageBody.setText(formattedBody);
|
viewHolder.messageBody.setText(formattedBody);
|
||||||
viewHolder.messageBody.setTextIsSelectable(true);
|
viewHolder.messageBody.setTextIsSelectable(true);
|
||||||
viewHolder.messageBody.setMovementMethod(ClickableMovementMethod.getInstance());
|
viewHolder.messageBody.setMovementMethod(ClickableMovementMethod.getInstance());
|
||||||
|
listSelectionManager.onUpdate(viewHolder.messageBody, message);
|
||||||
} else {
|
} else {
|
||||||
viewHolder.messageBody.setText("");
|
viewHolder.messageBody.setText("");
|
||||||
viewHolder.messageBody.setTextIsSelectable(false);
|
viewHolder.messageBody.setTextIsSelectable(false);
|
||||||
|
@ -535,6 +539,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
||||||
viewHolder = null;
|
viewHolder = null;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (viewHolder.messageBody != null) listSelectionManager.onCreate(viewHolder.messageBody);
|
||||||
view.setTag(viewHolder);
|
view.setTag(viewHolder);
|
||||||
} else {
|
} else {
|
||||||
viewHolder = (ViewHolder) view.getTag();
|
viewHolder = (ViewHolder) view.getTag();
|
||||||
|
@ -685,6 +690,13 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void notifyDataSetChanged() {
|
||||||
|
listSelectionManager.onBeforeNotifyDataSetChanged();
|
||||||
|
super.notifyDataSetChanged();
|
||||||
|
listSelectionManager.onAfterNotifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
public void openDownloadable(Message message) {
|
public void openDownloadable(Message message) {
|
||||||
DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message);
|
DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message);
|
||||||
if (!file.exists()) {
|
if (!file.exists()) {
|
||||||
|
@ -737,6 +749,15 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
||||||
this.mUseGreenBackground = activity.useGreenBackground();
|
this.mUseGreenBackground = activity.useGreenBackground();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TextView getMessageBody(View view) {
|
||||||
|
final Object tag = view.getTag();
|
||||||
|
if (tag instanceof ViewHolder) {
|
||||||
|
final ViewHolder viewHolder = (ViewHolder) tag;
|
||||||
|
return viewHolder.messageBody;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public interface OnContactPictureClicked {
|
public interface OnContactPictureClicked {
|
||||||
void onContactPictureClicked(Message message);
|
void onContactPictureClicked(Message message);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,201 @@
|
||||||
|
package eu.siacs.conversations.ui.widget;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.os.Message;
|
||||||
|
import android.text.Selection;
|
||||||
|
import android.text.Spannable;
|
||||||
|
import android.view.ActionMode;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
public class ListSelectionManager {
|
||||||
|
|
||||||
|
private static final int MESSAGE_SEND_RESET = 1;
|
||||||
|
private static final int MESSAGE_RESET = 2;
|
||||||
|
private static final int MESSAGE_START_SELECTION = 3;
|
||||||
|
|
||||||
|
private static final Handler HANDLER = new Handler(Looper.getMainLooper(), new Handler.Callback() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean handleMessage(Message msg) {
|
||||||
|
switch (msg.what) {
|
||||||
|
case MESSAGE_SEND_RESET: {
|
||||||
|
// Skip one more message queue loop
|
||||||
|
HANDLER.obtainMessage(MESSAGE_RESET, msg.obj).sendToTarget();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case MESSAGE_RESET: {
|
||||||
|
final ListSelectionManager listSelectionManager = (ListSelectionManager) msg.obj;
|
||||||
|
listSelectionManager.futureSelectionIdentifier = null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case MESSAGE_START_SELECTION: {
|
||||||
|
final StartSelectionHolder holder = (StartSelectionHolder) msg.obj;
|
||||||
|
holder.listSelectionManager.futureSelectionIdentifier = null;
|
||||||
|
startSelection(holder.textView, holder.start, holder.end);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
private static class StartSelectionHolder {
|
||||||
|
|
||||||
|
public final ListSelectionManager listSelectionManager;
|
||||||
|
public final TextView textView;
|
||||||
|
public final int start;
|
||||||
|
public final int end;
|
||||||
|
|
||||||
|
public StartSelectionHolder(ListSelectionManager listSelectionManager, TextView textView,
|
||||||
|
int start, int end) {
|
||||||
|
this.listSelectionManager = listSelectionManager;
|
||||||
|
this.textView = textView;
|
||||||
|
this.start = start;
|
||||||
|
this.end = end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ActionMode selectionActionMode;
|
||||||
|
private Object selectionIdentifier;
|
||||||
|
private TextView selectionTextView;
|
||||||
|
|
||||||
|
private Object futureSelectionIdentifier;
|
||||||
|
private int futureSelectionStart;
|
||||||
|
private int futureSelectionEnd;
|
||||||
|
|
||||||
|
public void onCreate(TextView textView) {
|
||||||
|
final CustomCallback callback = new CustomCallback(textView);
|
||||||
|
textView.setCustomSelectionActionModeCallback(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onUpdate(TextView textView, Object identifier) {
|
||||||
|
if (SUPPORTED) {
|
||||||
|
CustomCallback callback = (CustomCallback) textView.getCustomSelectionActionModeCallback();
|
||||||
|
callback.identifier = identifier;
|
||||||
|
if (futureSelectionIdentifier == identifier) {
|
||||||
|
HANDLER.obtainMessage(MESSAGE_START_SELECTION, new StartSelectionHolder(this,
|
||||||
|
textView, futureSelectionStart, futureSelectionEnd)).sendToTarget();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onBeforeNotifyDataSetChanged() {
|
||||||
|
if (SUPPORTED) {
|
||||||
|
HANDLER.removeMessages(MESSAGE_SEND_RESET);
|
||||||
|
HANDLER.removeMessages(MESSAGE_RESET);
|
||||||
|
HANDLER.removeMessages(MESSAGE_START_SELECTION);
|
||||||
|
if (selectionActionMode != null) {
|
||||||
|
final CharSequence text = selectionTextView.getText();
|
||||||
|
futureSelectionIdentifier = selectionIdentifier;
|
||||||
|
futureSelectionStart = Selection.getSelectionStart(text);
|
||||||
|
futureSelectionEnd = Selection.getSelectionEnd(text);
|
||||||
|
selectionActionMode.finish();
|
||||||
|
selectionActionMode = null;
|
||||||
|
selectionIdentifier = null;
|
||||||
|
selectionTextView = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onAfterNotifyDataSetChanged() {
|
||||||
|
if (SUPPORTED && futureSelectionIdentifier != null) {
|
||||||
|
HANDLER.obtainMessage(MESSAGE_SEND_RESET, this).sendToTarget();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CustomCallback implements ActionMode.Callback {
|
||||||
|
|
||||||
|
private final TextView textView;
|
||||||
|
public Object identifier;
|
||||||
|
|
||||||
|
public CustomCallback(TextView textView) {
|
||||||
|
this.textView = textView;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||||
|
selectionActionMode = mode;
|
||||||
|
selectionIdentifier = identifier;
|
||||||
|
selectionTextView = textView;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyActionMode(ActionMode mode) {
|
||||||
|
if (selectionActionMode == mode) {
|
||||||
|
selectionActionMode = null;
|
||||||
|
selectionIdentifier = null;
|
||||||
|
selectionTextView = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final Field FIELD_EDITOR;
|
||||||
|
private static final Method METHOD_START_SELECTION;
|
||||||
|
private static final boolean SUPPORTED;
|
||||||
|
|
||||||
|
static {
|
||||||
|
Field editor;
|
||||||
|
try {
|
||||||
|
editor = TextView.class.getDeclaredField("mEditor");
|
||||||
|
editor.setAccessible(true);
|
||||||
|
} catch (Exception e) {
|
||||||
|
editor = null;
|
||||||
|
}
|
||||||
|
FIELD_EDITOR = editor;
|
||||||
|
Method startSelection = null;
|
||||||
|
if (editor != null) {
|
||||||
|
String[] startSelectionNames = {"startSelectionActionMode", "startSelectionActionModeWithSelection"};
|
||||||
|
for (String startSelectionName : startSelectionNames) {
|
||||||
|
try {
|
||||||
|
startSelection = editor.getType().getDeclaredMethod(startSelectionName);
|
||||||
|
startSelection.setAccessible(true);
|
||||||
|
break;
|
||||||
|
} catch (Exception e) {
|
||||||
|
startSelection = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
METHOD_START_SELECTION = startSelection;
|
||||||
|
SUPPORTED = FIELD_EDITOR != null && METHOD_START_SELECTION != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isSupported() {
|
||||||
|
return SUPPORTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void startSelection(TextView textView) {
|
||||||
|
startSelection(textView, 0, textView.getText().length());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void startSelection(TextView textView, int start, int end) {
|
||||||
|
final CharSequence text = textView.getText();
|
||||||
|
if (SUPPORTED && start >= 0 && end > start && textView.isTextSelectable() && text instanceof Spannable) {
|
||||||
|
final Spannable spannable = (Spannable) text;
|
||||||
|
start = Math.min(start, spannable.length());
|
||||||
|
end = Math.min(end, spannable.length());
|
||||||
|
Selection.setSelection(spannable, start, end);
|
||||||
|
try {
|
||||||
|
final Object editor = FIELD_EDITOR != null ? FIELD_EDITOR.get(textView) : textView;
|
||||||
|
METHOD_START_SELECTION.invoke(editor);
|
||||||
|
} catch (Exception e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,10 @@
|
||||||
android:id="@+id/copy_text"
|
android:id="@+id/copy_text"
|
||||||
android:title="@string/copy_text"
|
android:title="@string/copy_text"
|
||||||
android:visible="false"/>
|
android:visible="false"/>
|
||||||
|
<item
|
||||||
|
android:id="@+id/select_text"
|
||||||
|
android:title="@string/select_text"
|
||||||
|
android:visible="false"/>
|
||||||
<item
|
<item
|
||||||
android:id="@+id/retry_decryption"
|
android:id="@+id/retry_decryption"
|
||||||
android:title="Retry decryption"
|
android:title="Retry decryption"
|
||||||
|
|
|
@ -331,6 +331,7 @@
|
||||||
<string name="check_x_filesize_on_host">Проверить размер %1$s на %2$s</string>
|
<string name="check_x_filesize_on_host">Проверить размер %1$s на %2$s</string>
|
||||||
<string name="message_options">Опции сообщения</string>
|
<string name="message_options">Опции сообщения</string>
|
||||||
<string name="copy_text">Копировать текст</string>
|
<string name="copy_text">Копировать текст</string>
|
||||||
|
<string name="select_text">Выбрать текст</string>
|
||||||
<string name="copy_original_url">Копировать адрес ссылки</string>
|
<string name="copy_original_url">Копировать адрес ссылки</string>
|
||||||
<string name="send_again">Отправить ещё раз</string>
|
<string name="send_again">Отправить ещё раз</string>
|
||||||
<string name="file_url">URL файла</string>
|
<string name="file_url">URL файла</string>
|
||||||
|
|
|
@ -363,6 +363,7 @@
|
||||||
<string name="check_x_filesize_on_host">Check %1$s size on %2$s</string>
|
<string name="check_x_filesize_on_host">Check %1$s size on %2$s</string>
|
||||||
<string name="message_options">Message options</string>
|
<string name="message_options">Message options</string>
|
||||||
<string name="copy_text">Copy text</string>
|
<string name="copy_text">Copy text</string>
|
||||||
|
<string name="select_text">Select text</string>
|
||||||
<string name="copy_original_url">Copy original URL</string>
|
<string name="copy_original_url">Copy original URL</string>
|
||||||
<string name="send_again">Send again</string>
|
<string name="send_again">Send again</string>
|
||||||
<string name="file_url">File URL</string>
|
<string name="file_url">File URL</string>
|
||||||
|
|
Loading…
Reference in New Issue