mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-18 15:56:24 +01:00
Allow formatting text from overflow menu.
This commit is contained in:
@@ -8,6 +8,7 @@ import android.os.Bundle;
|
||||
import android.text.Annotation;
|
||||
import android.text.Editable;
|
||||
import android.text.InputType;
|
||||
import android.text.Selection;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
@@ -22,6 +23,7 @@ import android.view.MenuItem;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputConnection;
|
||||
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
@@ -338,48 +340,11 @@ public class ComposeText extends EmojiEditText {
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||||
Editable text = getText();
|
||||
|
||||
if (text == null) {
|
||||
return false;
|
||||
boolean handled = handleFormatText(item.getItemId());
|
||||
if (handled) {
|
||||
mode.finish();
|
||||
}
|
||||
|
||||
if (item.getItemId() != R.id.edittext_bold &&
|
||||
item.getItemId() != R.id.edittext_italic &&
|
||||
item.getItemId() != R.id.edittext_strikethrough &&
|
||||
item.getItemId() != R.id.edittext_monospace &&
|
||||
item.getItemId() != R.id.edittext_spoiler &&
|
||||
item.getItemId() != R.id.edittext_clear_formatting)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int start = getSelectionStart();
|
||||
int end = getSelectionEnd();
|
||||
BodyRangeList.BodyRange.Style style = null;
|
||||
|
||||
if (item.getItemId() == R.id.edittext_bold) {
|
||||
style = BodyRangeList.BodyRange.Style.BOLD;
|
||||
} else if (item.getItemId() == R.id.edittext_italic) {
|
||||
style = BodyRangeList.BodyRange.Style.ITALIC;
|
||||
} else if (item.getItemId() == R.id.edittext_strikethrough) {
|
||||
style = BodyRangeList.BodyRange.Style.STRIKETHROUGH;
|
||||
} else if (item.getItemId() == R.id.edittext_monospace) {
|
||||
style = BodyRangeList.BodyRange.Style.MONOSPACE;
|
||||
} else if (item.getItemId() == R.id.edittext_spoiler) {
|
||||
style = BodyRangeList.BodyRange.Style.SPOILER;
|
||||
}
|
||||
|
||||
clearComposingText();
|
||||
|
||||
if (style != null) {
|
||||
MessageStyler.toggleStyle(style, text, start, end);
|
||||
} else if (item.getItemId() == R.id.edittext_clear_formatting) {
|
||||
MessageStyler.clearStyling(text, start, end);
|
||||
}
|
||||
|
||||
mode.finish();
|
||||
return true;
|
||||
return handled;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -567,6 +532,56 @@ public class ComposeText extends EmojiEditText {
|
||||
return TIME_PATTERN.matcher(text.subSequence(startOfToken, endOfToken)).find();
|
||||
}
|
||||
|
||||
public boolean isTextHighlighted() {
|
||||
return getText() != null && getSelectionStart() < getSelectionEnd();
|
||||
}
|
||||
|
||||
public boolean handleFormatText(@IdRes int id) {
|
||||
Editable text = getText();
|
||||
|
||||
if (text == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (id != R.id.edittext_bold &&
|
||||
id != R.id.edittext_italic &&
|
||||
id != R.id.edittext_strikethrough &&
|
||||
id != R.id.edittext_monospace &&
|
||||
id != R.id.edittext_spoiler &&
|
||||
id != R.id.edittext_clear_formatting)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int start = getSelectionStart();
|
||||
int end = getSelectionEnd();
|
||||
BodyRangeList.BodyRange.Style style = null;
|
||||
|
||||
if (id == R.id.edittext_bold) {
|
||||
style = BodyRangeList.BodyRange.Style.BOLD;
|
||||
} else if (id == R.id.edittext_italic) {
|
||||
style = BodyRangeList.BodyRange.Style.ITALIC;
|
||||
} else if (id == R.id.edittext_strikethrough) {
|
||||
style = BodyRangeList.BodyRange.Style.STRIKETHROUGH;
|
||||
} else if (id == R.id.edittext_monospace) {
|
||||
style = BodyRangeList.BodyRange.Style.MONOSPACE;
|
||||
} else if (id == R.id.edittext_spoiler) {
|
||||
style = BodyRangeList.BodyRange.Style.SPOILER;
|
||||
}
|
||||
|
||||
clearComposingText();
|
||||
|
||||
if (style != null) {
|
||||
MessageStyler.toggleStyle(style, text, start, end);
|
||||
} else {
|
||||
MessageStyler.clearStyling(text, start, end);
|
||||
}
|
||||
|
||||
Selection.setSelection(getText(), end);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static class CommitContentListener implements InputConnectionCompat.OnCommitContentListener {
|
||||
|
||||
private static final String TAG = Log.tag(CommitContentListener.class);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
package org.thoughtcrime.securesms.conversation
|
||||
|
||||
import android.text.SpannableString
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
@@ -165,9 +166,23 @@ internal object ConversationOptionsMenu {
|
||||
hideMenuItem(menu, R.id.menu_view_media)
|
||||
}
|
||||
|
||||
menu.findItem(R.id.menu_format_text_submenu).subMenu?.clearHeader()
|
||||
menu.findItem(R.id.edittext_bold).applyTitleSpan(MessageStyler.boldStyle())
|
||||
menu.findItem(R.id.edittext_italic).applyTitleSpan(MessageStyler.italicStyle())
|
||||
menu.findItem(R.id.edittext_strikethrough).applyTitleSpan(MessageStyler.strikethroughStyle())
|
||||
menu.findItem(R.id.edittext_monospace).applyTitleSpan(MessageStyler.monoStyle())
|
||||
|
||||
callback.onOptionsMenuCreated(menu)
|
||||
}
|
||||
|
||||
override fun onPrepareMenu(menu: Menu) {
|
||||
super.onPrepareMenu(menu)
|
||||
val formatText = menu.findItem(R.id.menu_format_text_submenu)
|
||||
if (formatText != null) {
|
||||
formatText.isVisible = callback.isTextHighlighted()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||
when (menuItem.itemId) {
|
||||
R.id.menu_call_secure -> callback.handleDial(true)
|
||||
@@ -189,6 +204,12 @@ internal object ConversationOptionsMenu {
|
||||
R.id.menu_expiring_messages_off, R.id.menu_expiring_messages -> callback.handleSelectMessageExpiration()
|
||||
R.id.menu_create_bubble -> callback.handleCreateBubble()
|
||||
R.id.home -> callback.handleGoHome()
|
||||
R.id.edittext_bold,
|
||||
R.id.edittext_italic,
|
||||
R.id.edittext_strikethrough,
|
||||
R.id.edittext_monospace,
|
||||
R.id.edittext_spoiler,
|
||||
R.id.edittext_clear_formatting -> callback.handleFormatText(menuItem.itemId)
|
||||
else -> return false
|
||||
}
|
||||
|
||||
@@ -200,6 +221,10 @@ internal object ConversationOptionsMenu {
|
||||
menu.findItem(menuItem).isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun MenuItem.applyTitleSpan(span: Any) {
|
||||
title = SpannableString(title).apply { setSpan(span, 0, length, MessageStyler.SPAN_FLAGS) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -224,6 +249,7 @@ internal object ConversationOptionsMenu {
|
||||
*/
|
||||
interface Callback {
|
||||
fun getSnapshot(): Snapshot
|
||||
fun isTextHighlighted(): Boolean
|
||||
|
||||
fun onOptionsMenuCreated(menu: Menu)
|
||||
|
||||
@@ -248,5 +274,6 @@ internal object ConversationOptionsMenu {
|
||||
fun showExpiring(recipient: Recipient)
|
||||
fun clearExpiring()
|
||||
fun showGroupCallingTooltip()
|
||||
fun handleFormatText(@IdRes id: Int)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -472,8 +472,6 @@ public class ConversationParentFragment extends Fragment
|
||||
private Callback callback;
|
||||
private RecentEmojiPageModel recentEmojis;
|
||||
|
||||
private ConversationOptionsMenu.Provider menuProvider;
|
||||
|
||||
private Set<KeyboardPage> previousPages;
|
||||
|
||||
public static ConversationParentFragment create(Intent intent) {
|
||||
@@ -494,7 +492,6 @@ public class ConversationParentFragment extends Fragment
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
disposables.bindTo(getViewLifecycleOwner());
|
||||
menuProvider = new ConversationOptionsMenu.Provider(this, disposables);
|
||||
SpoilerAnnotation.resetRevealedSpoilers();
|
||||
|
||||
if (requireActivity() instanceof Callback) {
|
||||
@@ -575,10 +572,6 @@ public class ConversationParentFragment extends Fragment
|
||||
};
|
||||
requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), backPressedCallback);
|
||||
|
||||
if (isSearchRequested && savedInstanceState == null) {
|
||||
menuProvider.onCreateMenu(toolbar.getMenu(), requireActivity().getMenuInflater());
|
||||
}
|
||||
|
||||
sendButton.post(() -> sendButton.triggerSelectedChangedEvent());
|
||||
}
|
||||
|
||||
@@ -990,7 +983,7 @@ public class ConversationParentFragment extends Fragment
|
||||
if (!isSearchRequested && getActivity() != null) {
|
||||
optionsMenuDebouncer.publish(() -> {
|
||||
if (getActivity() != null) {
|
||||
menuProvider.onCreateMenu(toolbar.getMenu(), requireActivity().getMenuInflater());
|
||||
toolbar.invalidateMenu();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -2108,8 +2101,8 @@ public class ConversationParentFragment extends Fragment
|
||||
}
|
||||
|
||||
protected void initializeActionBar() {
|
||||
toolbar.addMenuProvider(new ConversationOptionsMenu.Provider(this, disposables));
|
||||
invalidateOptionsMenu();
|
||||
toolbar.setOnMenuItemClickListener(menuProvider::onMenuItemSelected);
|
||||
toolbar.setNavigationContentDescription(R.string.ConversationFragment__content_description_back_button);
|
||||
if (isInBubble()) {
|
||||
toolbar.setNavigationIcon(DrawableUtil.tint(ContextUtil.requireDrawable(requireContext(), R.drawable.ic_notification),
|
||||
@@ -2370,6 +2363,11 @@ public class ConversationParentFragment extends Fragment
|
||||
.show(TooltipPopup.POSITION_BELOW);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleFormatText(@IdRes int id) {
|
||||
composeText.handleFormatText(id);
|
||||
}
|
||||
|
||||
private void showStickerIntroductionTooltip() {
|
||||
TextSecurePreferences.setMediaKeyboardMode(requireContext(), MediaKeyboardMode.STICKER);
|
||||
inputPanel.setMediaKeyboardToggleMode(KeyboardPage.STICKER);
|
||||
@@ -3679,6 +3677,11 @@ public class ConversationParentFragment extends Fragment
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTextHighlighted() {
|
||||
return composeText.isTextHighlighted();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showExpiring(@NonNull Recipient recipient) {
|
||||
titleView.showExpiring(recipient);
|
||||
|
||||
@@ -330,7 +330,6 @@ class ConversationFragment :
|
||||
private val colorizer = Colorizer()
|
||||
private val textDraftSaveDebouncer = Debouncer(500)
|
||||
|
||||
private lateinit var conversationOptionsMenuProvider: ConversationOptionsMenu.Provider
|
||||
private lateinit var layoutManager: LinearLayoutManager
|
||||
private lateinit var markReadHelper: MarkReadHelper
|
||||
private lateinit var giphyMp4ProjectionRecycler: GiphyMp4ProjectionRecycler
|
||||
@@ -396,7 +395,6 @@ class ConversationFragment :
|
||||
disposables.bindTo(viewLifecycleOwner)
|
||||
FullscreenHelper(requireActivity()).showSystemUI()
|
||||
|
||||
conversationOptionsMenuProvider = ConversationOptionsMenu.Provider(ConversationOptionsMenuCallback(), disposables)
|
||||
markReadHelper = MarkReadHelper(ConversationId.forConversation(args.threadId), requireContext(), viewLifecycleOwner)
|
||||
|
||||
initializeConversationThreadUi()
|
||||
@@ -777,12 +775,13 @@ class ConversationFragment :
|
||||
}
|
||||
|
||||
private fun invalidateOptionsMenu() {
|
||||
if (!isSearchRequested && activity != null) {
|
||||
conversationOptionsMenuProvider.onCreateMenu(binding.toolbar.menu, requireActivity().menuInflater)
|
||||
if (!isSearchRequested) {
|
||||
binding.toolbar.invalidateMenu()
|
||||
}
|
||||
}
|
||||
|
||||
private fun presentActionBarMenu() {
|
||||
binding.toolbar.addMenuProvider(ConversationOptionsMenu.Provider(ConversationOptionsMenuCallback(), disposables))
|
||||
invalidateOptionsMenu()
|
||||
|
||||
when (args.conversationScreenType) {
|
||||
@@ -790,8 +789,6 @@ class ConversationFragment :
|
||||
ConversationScreenType.BUBBLE -> presentNavigationIconForBubble()
|
||||
ConversationScreenType.POPUP -> Unit
|
||||
}
|
||||
|
||||
binding.toolbar.setOnMenuItemClickListener(conversationOptionsMenuProvider::onMenuItemSelected)
|
||||
}
|
||||
|
||||
private fun presentNavigationIconForNormal() {
|
||||
@@ -2109,6 +2106,10 @@ class ConversationFragment :
|
||||
)
|
||||
}
|
||||
|
||||
override fun isTextHighlighted(): Boolean {
|
||||
return composeText.isTextHighlighted
|
||||
}
|
||||
|
||||
override fun onOptionsMenuCreated(menu: Menu) {
|
||||
searchMenuItem = menu.findItem(R.id.menu_search)
|
||||
|
||||
@@ -2337,6 +2338,10 @@ class ConversationFragment :
|
||||
override fun showGroupCallingTooltip() {
|
||||
conversationTooltips.displayGroupCallingTooltip(requireView().findViewById(R.id.menu_video_secure))
|
||||
}
|
||||
|
||||
override fun handleFormatText(id: Int) {
|
||||
composeText.handleFormatText(id)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class OnReactionsSelectedListener : ConversationReactionOverlay.OnReactionSelectedListener {
|
||||
|
||||
Reference in New Issue
Block a user