Skip to content

Commit 882748f

Browse files
authored
Allow formatting text from overflow menu.
1 parent 15035f4 commit 882748f

6 files changed

Lines changed: 160 additions & 69 deletions

File tree

app/src/main/java/org/thoughtcrime/securesms/components/ComposeText.java

Lines changed: 56 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import android.text.Annotation;
99
import android.text.Editable;
1010
import android.text.InputType;
11+
import android.text.Selection;
1112
import android.text.Spannable;
1213
import android.text.SpannableString;
1314
import android.text.SpannableStringBuilder;
@@ -22,6 +23,7 @@
2223
import android.view.inputmethod.EditorInfo;
2324
import android.view.inputmethod.InputConnection;
2425

26+
import androidx.annotation.IdRes;
2527
import androidx.annotation.NonNull;
2628
import androidx.annotation.Nullable;
2729
import androidx.core.content.ContextCompat;
@@ -338,48 +340,11 @@ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
338340

339341
@Override
340342
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
341-
Editable text = getText();
342-
343-
if (text == null) {
344-
return false;
345-
}
346-
347-
if (item.getItemId() != R.id.edittext_bold &&
348-
item.getItemId() != R.id.edittext_italic &&
349-
item.getItemId() != R.id.edittext_strikethrough &&
350-
item.getItemId() != R.id.edittext_monospace &&
351-
item.getItemId() != R.id.edittext_spoiler &&
352-
item.getItemId() != R.id.edittext_clear_formatting)
353-
{
354-
return false;
355-
}
356-
357-
int start = getSelectionStart();
358-
int end = getSelectionEnd();
359-
BodyRangeList.BodyRange.Style style = null;
360-
361-
if (item.getItemId() == R.id.edittext_bold) {
362-
style = BodyRangeList.BodyRange.Style.BOLD;
363-
} else if (item.getItemId() == R.id.edittext_italic) {
364-
style = BodyRangeList.BodyRange.Style.ITALIC;
365-
} else if (item.getItemId() == R.id.edittext_strikethrough) {
366-
style = BodyRangeList.BodyRange.Style.STRIKETHROUGH;
367-
} else if (item.getItemId() == R.id.edittext_monospace) {
368-
style = BodyRangeList.BodyRange.Style.MONOSPACE;
369-
} else if (item.getItemId() == R.id.edittext_spoiler) {
370-
style = BodyRangeList.BodyRange.Style.SPOILER;
371-
}
372-
373-
clearComposingText();
374-
375-
if (style != null) {
376-
MessageStyler.toggleStyle(style, text, start, end);
377-
} else if (item.getItemId() == R.id.edittext_clear_formatting) {
378-
MessageStyler.clearStyling(text, start, end);
343+
boolean handled = handleFormatText(item.getItemId());
344+
if (handled) {
345+
mode.finish();
379346
}
380-
381-
mode.finish();
382-
return true;
347+
return handled;
383348
}
384349

385350
@Override
@@ -567,6 +532,56 @@ private static boolean couldBeTimeEntry(@NonNull CharSequence text, int startInd
567532
return TIME_PATTERN.matcher(text.subSequence(startOfToken, endOfToken)).find();
568533
}
569534

535+
public boolean isTextHighlighted() {
536+
return getText() != null && getSelectionStart() < getSelectionEnd();
537+
}
538+
539+
public boolean handleFormatText(@IdRes int id) {
540+
Editable text = getText();
541+
542+
if (text == null) {
543+
return false;
544+
}
545+
546+
if (id != R.id.edittext_bold &&
547+
id != R.id.edittext_italic &&
548+
id != R.id.edittext_strikethrough &&
549+
id != R.id.edittext_monospace &&
550+
id != R.id.edittext_spoiler &&
551+
id != R.id.edittext_clear_formatting)
552+
{
553+
return false;
554+
}
555+
556+
int start = getSelectionStart();
557+
int end = getSelectionEnd();
558+
BodyRangeList.BodyRange.Style style = null;
559+
560+
if (id == R.id.edittext_bold) {
561+
style = BodyRangeList.BodyRange.Style.BOLD;
562+
} else if (id == R.id.edittext_italic) {
563+
style = BodyRangeList.BodyRange.Style.ITALIC;
564+
} else if (id == R.id.edittext_strikethrough) {
565+
style = BodyRangeList.BodyRange.Style.STRIKETHROUGH;
566+
} else if (id == R.id.edittext_monospace) {
567+
style = BodyRangeList.BodyRange.Style.MONOSPACE;
568+
} else if (id == R.id.edittext_spoiler) {
569+
style = BodyRangeList.BodyRange.Style.SPOILER;
570+
}
571+
572+
clearComposingText();
573+
574+
if (style != null) {
575+
MessageStyler.toggleStyle(style, text, start, end);
576+
} else {
577+
MessageStyler.clearStyling(text, start, end);
578+
}
579+
580+
Selection.setSelection(getText(), end);
581+
582+
return true;
583+
}
584+
570585
private static class CommitContentListener implements InputConnectionCompat.OnCommitContentListener {
571586

572587
private static final String TAG = Log.tag(CommitContentListener.class);

app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationOptionsMenu.kt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
package org.thoughtcrime.securesms.conversation
77

8+
import android.text.SpannableString
89
import android.view.Menu
910
import android.view.MenuInflater
1011
import android.view.MenuItem
@@ -165,9 +166,23 @@ internal object ConversationOptionsMenu {
165166
hideMenuItem(menu, R.id.menu_view_media)
166167
}
167168

169+
menu.findItem(R.id.menu_format_text_submenu).subMenu?.clearHeader()
170+
menu.findItem(R.id.edittext_bold).applyTitleSpan(MessageStyler.boldStyle())
171+
menu.findItem(R.id.edittext_italic).applyTitleSpan(MessageStyler.italicStyle())
172+
menu.findItem(R.id.edittext_strikethrough).applyTitleSpan(MessageStyler.strikethroughStyle())
173+
menu.findItem(R.id.edittext_monospace).applyTitleSpan(MessageStyler.monoStyle())
174+
168175
callback.onOptionsMenuCreated(menu)
169176
}
170177

178+
override fun onPrepareMenu(menu: Menu) {
179+
super.onPrepareMenu(menu)
180+
val formatText = menu.findItem(R.id.menu_format_text_submenu)
181+
if (formatText != null) {
182+
formatText.isVisible = callback.isTextHighlighted()
183+
}
184+
}
185+
171186
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
172187
when (menuItem.itemId) {
173188
R.id.menu_call_secure -> callback.handleDial(true)
@@ -189,6 +204,12 @@ internal object ConversationOptionsMenu {
189204
R.id.menu_expiring_messages_off, R.id.menu_expiring_messages -> callback.handleSelectMessageExpiration()
190205
R.id.menu_create_bubble -> callback.handleCreateBubble()
191206
R.id.home -> callback.handleGoHome()
207+
R.id.edittext_bold,
208+
R.id.edittext_italic,
209+
R.id.edittext_strikethrough,
210+
R.id.edittext_monospace,
211+
R.id.edittext_spoiler,
212+
R.id.edittext_clear_formatting -> callback.handleFormatText(menuItem.itemId)
192213
else -> return false
193214
}
194215

@@ -200,6 +221,10 @@ internal object ConversationOptionsMenu {
200221
menu.findItem(menuItem).isVisible = false
201222
}
202223
}
224+
225+
private fun MenuItem.applyTitleSpan(span: Any) {
226+
title = SpannableString(title).apply { setSpan(span, 0, length, MessageStyler.SPAN_FLAGS) }
227+
}
203228
}
204229

205230
/**
@@ -224,6 +249,7 @@ internal object ConversationOptionsMenu {
224249
*/
225250
interface Callback {
226251
fun getSnapshot(): Snapshot
252+
fun isTextHighlighted(): Boolean
227253

228254
fun onOptionsMenuCreated(menu: Menu)
229255

@@ -248,5 +274,6 @@ internal object ConversationOptionsMenu {
248274
fun showExpiring(recipient: Recipient)
249275
fun clearExpiring()
250276
fun showGroupCallingTooltip()
277+
fun handleFormatText(@IdRes id: Int)
251278
}
252279
}

app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -472,8 +472,6 @@ public class ConversationParentFragment extends Fragment
472472
private Callback callback;
473473
private RecentEmojiPageModel recentEmojis;
474474

475-
private ConversationOptionsMenu.Provider menuProvider;
476-
477475
private Set<KeyboardPage> previousPages;
478476

479477
public static ConversationParentFragment create(Intent intent) {
@@ -494,7 +492,6 @@ public static ConversationParentFragment create(Intent intent) {
494492
@Override
495493
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
496494
disposables.bindTo(getViewLifecycleOwner());
497-
menuProvider = new ConversationOptionsMenu.Provider(this, disposables);
498495
SpoilerAnnotation.resetRevealedSpoilers();
499496

500497
if (requireActivity() instanceof Callback) {
@@ -575,10 +572,6 @@ public void handleOnBackPressed() {
575572
};
576573
requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), backPressedCallback);
577574

578-
if (isSearchRequested && savedInstanceState == null) {
579-
menuProvider.onCreateMenu(toolbar.getMenu(), requireActivity().getMenuInflater());
580-
}
581-
582575
sendButton.post(() -> sendButton.triggerSelectedChangedEvent());
583576
}
584577

@@ -990,7 +983,7 @@ public void invalidateOptionsMenu() {
990983
if (!isSearchRequested && getActivity() != null) {
991984
optionsMenuDebouncer.publish(() -> {
992985
if (getActivity() != null) {
993-
menuProvider.onCreateMenu(toolbar.getMenu(), requireActivity().getMenuInflater());
986+
toolbar.invalidateMenu();
994987
}
995988
});
996989
}
@@ -2108,8 +2101,8 @@ private void setToolbarActionItemTint(@NonNull Toolbar toolbar, @ColorInt int ti
21082101
}
21092102

21102103
protected void initializeActionBar() {
2104+
toolbar.addMenuProvider(new ConversationOptionsMenu.Provider(this, disposables));
21112105
invalidateOptionsMenu();
2112-
toolbar.setOnMenuItemClickListener(menuProvider::onMenuItemSelected);
21132106
toolbar.setNavigationContentDescription(R.string.ConversationFragment__content_description_back_button);
21142107
if (isInBubble()) {
21152108
toolbar.setNavigationIcon(DrawableUtil.tint(ContextUtil.requireDrawable(requireContext(), R.drawable.ic_notification),
@@ -2370,6 +2363,11 @@ public void showGroupCallingTooltip() {
23702363
.show(TooltipPopup.POSITION_BELOW);
23712364
}
23722365

2366+
@Override
2367+
public void handleFormatText(@IdRes int id) {
2368+
composeText.handleFormatText(id);
2369+
}
2370+
23732371
private void showStickerIntroductionTooltip() {
23742372
TextSecurePreferences.setMediaKeyboardMode(requireContext(), MediaKeyboardMode.STICKER);
23752373
inputPanel.setMediaKeyboardToggleMode(KeyboardPage.STICKER);
@@ -3679,6 +3677,11 @@ public void handleGoHome() {
36793677
);
36803678
}
36813679

3680+
@Override
3681+
public boolean isTextHighlighted() {
3682+
return composeText.isTextHighlighted();
3683+
}
3684+
36823685
@Override
36833686
public void showExpiring(@NonNull Recipient recipient) {
36843687
titleView.showExpiring(recipient);

app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,6 @@ class ConversationFragment :
330330
private val colorizer = Colorizer()
331331
private val textDraftSaveDebouncer = Debouncer(500)
332332

333-
private lateinit var conversationOptionsMenuProvider: ConversationOptionsMenu.Provider
334333
private lateinit var layoutManager: LinearLayoutManager
335334
private lateinit var markReadHelper: MarkReadHelper
336335
private lateinit var giphyMp4ProjectionRecycler: GiphyMp4ProjectionRecycler
@@ -396,7 +395,6 @@ class ConversationFragment :
396395
disposables.bindTo(viewLifecycleOwner)
397396
FullscreenHelper(requireActivity()).showSystemUI()
398397

399-
conversationOptionsMenuProvider = ConversationOptionsMenu.Provider(ConversationOptionsMenuCallback(), disposables)
400398
markReadHelper = MarkReadHelper(ConversationId.forConversation(args.threadId), requireContext(), viewLifecycleOwner)
401399

402400
initializeConversationThreadUi()
@@ -777,21 +775,20 @@ class ConversationFragment :
777775
}
778776

779777
private fun invalidateOptionsMenu() {
780-
if (!isSearchRequested && activity != null) {
781-
conversationOptionsMenuProvider.onCreateMenu(binding.toolbar.menu, requireActivity().menuInflater)
778+
if (!isSearchRequested) {
779+
binding.toolbar.invalidateMenu()
782780
}
783781
}
784782

785783
private fun presentActionBarMenu() {
784+
binding.toolbar.addMenuProvider(ConversationOptionsMenu.Provider(ConversationOptionsMenuCallback(), disposables))
786785
invalidateOptionsMenu()
787786

788787
when (args.conversationScreenType) {
789788
ConversationScreenType.NORMAL -> presentNavigationIconForNormal()
790789
ConversationScreenType.BUBBLE -> presentNavigationIconForBubble()
791790
ConversationScreenType.POPUP -> Unit
792791
}
793-
794-
binding.toolbar.setOnMenuItemClickListener(conversationOptionsMenuProvider::onMenuItemSelected)
795792
}
796793

797794
private fun presentNavigationIconForNormal() {
@@ -2109,6 +2106,10 @@ class ConversationFragment :
21092106
)
21102107
}
21112108

2109+
override fun isTextHighlighted(): Boolean {
2110+
return composeText.isTextHighlighted
2111+
}
2112+
21122113
override fun onOptionsMenuCreated(menu: Menu) {
21132114
searchMenuItem = menu.findItem(R.id.menu_search)
21142115

@@ -2337,6 +2338,10 @@ class ConversationFragment :
23372338
override fun showGroupCallingTooltip() {
23382339
conversationTooltips.displayGroupCallingTooltip(requireView().findViewById(R.id.menu_video_secure))
23392340
}
2341+
2342+
override fun handleFormatText(id: Int) {
2343+
composeText.handleFormatText(id)
2344+
}
23402345
}
23412346

23422347
private inner class OnReactionsSelectedListener : ConversationReactionOverlay.OnReactionSelectedListener {

0 commit comments

Comments
 (0)