Skip to content

Commit

Permalink
Fix menu item icons being display improperly
Browse files Browse the repository at this point in the history
After the March 2024 security patch, icons in popup menus would be very
small and misaligned. I couldn't figure out the cause or how to fix this
within the existing system, so all icons are now shown using a custom
image span in the title instead of using the built in system
  • Loading branch information
Jacocococo committed Oct 12, 2024
1 parent bce9f10 commit 725b1d8
Show file tree
Hide file tree
Showing 7 changed files with 128 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,21 @@ private void updateOptionsMenu(){
listsMenu.getItem().setVisible(listsMenu.size()>0);
hashtagsMenu.getItem().setVisible(hashtagsMenu.size()>0);

// adds icons to title using image spans
UiUtils.enableOptionsMenuIcons(getContext(), optionsMenu, R.id.menu_add_timeline);

// remove icons from items so they don't appear twice (system + image span)
timelinesMenu.setIcon(null);
UiUtils.removeMenuIcons(timelinesMenu);
listsMenu.setIcon(null);
UiUtils.removeMenuIcons(listsMenu);
hashtagsMenu.setIcon(null);
UiUtils.removeMenuIcons(hashtagsMenu);

// reset header titles to remove icons
timelinesMenu.setHeaderTitle(R.string.sk_timeline);
listsMenu.setHeaderTitle(R.string.sk_list);
hashtagsMenu.setHeaderTitle(R.string.sk_hashtag);
}

private void saveTimelines(){
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -327,26 +327,34 @@ private boolean onFabLongClick(View v) {
private void addListsToOverflowMenu() {
Context ctx = getContext();
listsMenu.clear();
listsMenu.setHeaderTitle(R.string.sk_your_lists); // remove icon
listsMenu.getItem().setVisible(listItems.size() > 0);
UiUtils.insetPopupMenuIcon(ctx, UiUtils.makeBackItem(listsMenu));
MenuItem back = UiUtils.makeBackItem(listsMenu);
UiUtils.insetPopupMenuIcon(ctx, back);
back.setIcon(null); // handled through image span in insetPopupMenuIcon
listItems.forEach((id, list) -> {
MenuItem item = listsMenu.add(Menu.NONE, id, Menu.NONE, list.title);
item.setIcon(R.drawable.ic_fluent_people_24_regular);
UiUtils.insetPopupMenuIcon(ctx, item);
item.setIcon(null); // handled through image span in insetPopupMenuIcon
});
}

private void addHashtagsToOverflowMenu() {
Context ctx = getContext();
hashtagsMenu.clear();
hashtagsMenu.setHeaderTitle(R.string.sk_hashtags_you_follow); // remove icon
hashtagsMenu.getItem().setVisible(hashtagsItems.size() > 0);
UiUtils.insetPopupMenuIcon(ctx, UiUtils.makeBackItem(hashtagsMenu));
MenuItem back = UiUtils.makeBackItem(hashtagsMenu);
UiUtils.insetPopupMenuIcon(ctx, back);
back.setIcon(null); // handled through image span in insetPopupMenuIcon
hashtagsItems.entrySet().stream()
.sorted(Comparator.comparing(x -> x.getValue().name, String.CASE_INSENSITIVE_ORDER))
.forEach(entry -> {
MenuItem item = hashtagsMenu.add(Menu.NONE, entry.getKey(), Menu.NONE, entry.getValue().name);
item.setIcon(R.drawable.ic_fluent_number_symbol_24_regular);
UiUtils.insetPopupMenuIcon(ctx, item);
item.setIcon(null); // handled through image span in insetPopupMenuIcon
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -863,13 +863,13 @@ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
return;
}

menu.findItem(R.id.manage_user_lists).setTitle(getString(R.string.sk_lists_with_user, account.getShortUsername()));
UiUtils.setMenuItemTitle(menu.findItem(R.id.manage_user_lists), getString(R.string.sk_lists_with_user, account.getShortUsername()));
MenuItem mute=menu.findItem(R.id.mute);
mute.setTitle(getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getShortUsername()));
mute.setIcon(relationship.muting ? R.drawable.ic_fluent_speaker_0_24_regular : R.drawable.ic_fluent_speaker_off_24_regular);
UiUtils.insetPopupMenuIcon(getContext(), mute);
menu.findItem(R.id.block).setTitle(getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getShortUsername()));
menu.findItem(R.id.report).setTitle(getString(R.string.report_user, account.getShortUsername()));
UiUtils.setMenuItemTitle(menu.findItem(R.id.block), getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getShortUsername()));
UiUtils.setMenuItemTitle(menu.findItem(R.id.report), getString(R.string.report_user, account.getShortUsername()));
menu.findItem(R.id.manage_user_lists).setVisible(relationship.following);
menu.findItem(R.id.soft_block).setVisible(relationship.followedBy && !relationship.following);
MenuItem hideBoosts=menu.findItem(R.id.hide_boosts);
Expand All @@ -883,13 +883,13 @@ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
}
MenuItem blockDomain=menu.findItem(R.id.block_domain);
if(!account.isLocal()){
blockDomain.setTitle(getString(relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, account.getDomain()));
UiUtils.setMenuItemTitle(blockDomain, getString(relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, account.getDomain()));
blockDomain.setVisible(true);
}else{
blockDomain.setVisible(false);
}
menu.findItem(R.id.edit_note).setTitle(noteWrap.getVisibility()==View.GONE && (relationship.note==null || relationship.note.isEmpty())
? R.string.sk_add_note : R.string.sk_delete_note);
UiUtils.setMenuItemTitle(menu.findItem(R.id.edit_note),
getString(noteWrap.getVisibility()==View.GONE && (relationship.note==null || relationship.note.isEmpty()) ? R.string.sk_add_note : R.string.sk_delete_note));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -530,13 +530,13 @@ private void updateOptionsMenu(){
MenuItem muteConversation=menu.findItem(R.id.mute_conversation);
if(item.status.muted!=null){
muteConversation.setVisible((isOwnPost || item.parentFragment.isInstanceAkkoma()) || item.parentFragment instanceof NotificationsListFragment);
muteConversation.setTitle(item.status.muted ? R.string.unmute_conversation : R.string.mute_conversation);
UiUtils.setMenuItemTitle(muteConversation, item.parentFragment.getString(item.status.muted ? R.string.unmute_conversation : R.string.mute_conversation));
}else{
muteConversation.setVisible(false);
}
if(lp.newEmojiReactionButton==AccountLocalPreferences.NewEmojiReactionButton.REPLACE_BOOKMARK && item.status!=null){
bookmark.setVisible(true);
bookmark.setTitle(item.status.bookmarked ? R.string.remove_bookmark : R.string.add_bookmark);
UiUtils.setMenuItemTitle(bookmark, item.parentFragment.getString(item.status.bookmarked ? R.string.remove_bookmark : R.string.add_bookmark));
}else{
bookmark.setVisible(false);
}
Expand All @@ -559,8 +559,8 @@ private void updateOptionsMenu(){
mute.setTitle(item.parentFragment.getString(relationship!=null && relationship.muting ? R.string.unmute_user : R.string.mute_user, username));
mute.setIcon(relationship!=null && relationship.muting ? R.drawable.ic_fluent_speaker_0_24_regular : R.drawable.ic_fluent_speaker_off_24_regular);
UiUtils.insetPopupMenuIcon(item.parentFragment.getContext(), mute);
block.setTitle(item.parentFragment.getString(relationship!=null && relationship.blocking ? R.string.unblock_user : R.string.block_user, username));
report.setTitle(item.parentFragment.getString(R.string.report_user, username));
UiUtils.setMenuItemTitle(block, item.parentFragment.getString(relationship!=null && relationship.blocking ? R.string.unblock_user : R.string.block_user, username));
UiUtils.setMenuItemTitle(report, item.parentFragment.getString(R.string.report_user, username));
// disabled in megalodon. domain blocks from a post clutters the context menu and looks out of place
// if(!account.isLocal()){
// blockDomain.setVisible(true);
Expand All @@ -572,7 +572,7 @@ private void updateOptionsMenu(){
follow.setTitle(item.parentFragment.getString(following ? R.string.unfollow_user : R.string.follow_user, username));
follow.setIcon(following ? R.drawable.ic_fluent_person_delete_24_regular : R.drawable.ic_fluent_person_add_24_regular);
manageUserLists.setVisible(relationship != null && relationship.following);
manageUserLists.setTitle(item.parentFragment.getString(R.string.sk_lists_with_user, username));
UiUtils.setMenuItemTitle(manageUserLists, item.parentFragment.getString(R.string.sk_lists_with_user, username));
// ic_fluent_person_add_24_regular actually has a width of 25dp -.-
UiUtils.insetPopupMenuIcon(item.parentFragment.getContext(), follow, following ? 0 : V.dp(-1));
}
Expand Down Expand Up @@ -613,9 +613,9 @@ private void workaroundChangingMenuItemWidths(Menu menu, String username) {
float singleSpaceWidth = paint.measureText(" ");
int howManySpaces = (int) Math.ceil(missingWidth / singleSpaceWidth);
String enlargedText = openInBrowserText + " ".repeat(howManySpaces);
menu.findItem(R.id.open_in_browser).setTitle(enlargedText);
UiUtils.setMenuItemTitle(menu.findItem(R.id.open_in_browser), enlargedText);
} else {
menu.findItem(R.id.open_in_browser).setTitle(openInBrowserText);
UiUtils.setMenuItemTitle(menu.findItem(R.id.open_in_browser), openInBrowserText);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package org.joinmastodon.android.ui.text;

import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.text.style.ImageSpan;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

public class CenteredImageSpan extends ImageSpan{
public CenteredImageSpan(@NonNull Drawable d){
super(d, ImageSpan.ALIGN_BOTTOM);
}

@Override
public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, @Nullable Paint.FontMetricsInt fm){
Drawable drawable=getDrawable();
if (fm!=null) {
Paint.FontMetricsInt paintFm=paint.getFontMetricsInt();
int fontHeight=paintFm.descent-paintFm.ascent;
int verticalCenter=paintFm.ascent+(fontHeight/2);
int imgHeight=drawable.getIntrinsicHeight();

fm.ascent=fm.top=verticalCenter-imgHeight/2;
fm.descent=fm.bottom=verticalCenter+imgHeight/2;
}
return drawable.getIntrinsicWidth();
}

@Override
public void draw(@NonNull Canvas canvas, CharSequence text,
int start, int end, float x, int top, int y, int bottom,
@NonNull Paint paint){
Drawable drawable=getDrawable();
canvas.save();

int translateY=bottom-drawable.getIntrinsicHeight();
canvas.translate(x, translateY);

drawable.draw(canvas);
canvas.restore();
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
package org.joinmastodon.android.ui.utils;

import static android.view.Menu.NONE;
import static org.joinmastodon.android.GlobalUserPreferences.ThemePreference.*;
import static org.joinmastodon.android.GlobalUserPreferences.theme;
import static org.joinmastodon.android.GlobalUserPreferences.trueBlackTheme;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
Expand Down Expand Up @@ -69,16 +64,15 @@
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.CacheController;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.MastodonErrorResponse;
import org.joinmastodon.android.api.StatusInteractionController;
import org.joinmastodon.android.api.requests.accounts.AuthorizeFollowRequest;
import org.joinmastodon.android.api.requests.accounts.RejectFollowRequest;
import org.joinmastodon.android.api.requests.accounts.SetAccountBlocked;
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
import org.joinmastodon.android.api.requests.accounts.SetAccountMuted;
import org.joinmastodon.android.api.requests.accounts.SetDomainBlocked;
import org.joinmastodon.android.api.requests.accounts.AuthorizeFollowRequest;
import org.joinmastodon.android.api.requests.accounts.RejectFollowRequest;
import org.joinmastodon.android.api.requests.instance.GetInstance;
import org.joinmastodon.android.api.requests.lists.DeleteList;
import org.joinmastodon.android.api.requests.notifications.DismissNotification;
Expand All @@ -90,11 +84,11 @@
import org.joinmastodon.android.api.session.AccountLocalPreferences;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.ScheduledStatusDeletedEvent;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.events.FollowRequestHandledEvent;
import org.joinmastodon.android.events.NotificationDeletedEvent;
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
import org.joinmastodon.android.events.ScheduledStatusDeletedEvent;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.events.StatusDeletedEvent;
import org.joinmastodon.android.events.StatusUnpinnedEvent;
import org.joinmastodon.android.fragments.ComposeFragment;
Expand All @@ -105,15 +99,16 @@
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.AccountField;
import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.model.ScheduledStatus;
import org.joinmastodon.android.model.SearchResults;
import org.joinmastodon.android.model.Searchable;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.text.CenteredImageSpan;
import org.joinmastodon.android.ui.text.CustomEmojiSpan;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.parceler.Parcels;
Expand Down Expand Up @@ -141,8 +136,8 @@
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
import java.util.function.Function;
Expand All @@ -161,7 +156,6 @@
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager2.widget.ViewPager2;

import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
Expand All @@ -171,6 +165,12 @@
import me.grishka.appkit.utils.V;
import okhttp3.MediaType;

import static android.view.Menu.NONE;
import static org.joinmastodon.android.GlobalUserPreferences.ThemePreference.AUTO;
import static org.joinmastodon.android.GlobalUserPreferences.ThemePreference.DARK;
import static org.joinmastodon.android.GlobalUserPreferences.theme;
import static org.joinmastodon.android.GlobalUserPreferences.trueBlackTheme;

public class UiUtils {
private static Handler mainHandler = new Handler(Looper.getMainLooper());
private static final DateTimeFormatter DATE_FORMATTER_SHORT_WITH_YEAR = DateTimeFormatter.ofPattern("d MMM uuuu"), DATE_FORMATTER_SHORT = DateTimeFormatter.ofPattern("d MMM");
Expand Down Expand Up @@ -918,17 +918,40 @@ public static void insetPopupMenuIcon(Context context, MenuItem item, int addWid
* @param addWidth set if icon is too wide/narrow. if icon is 25dp in width, set to -1dp
*/
public static void insetPopupMenuIcon(MenuItem item, ColorStateList iconTint, int addWidth) {
if(item.getIcon()==null)
return;
Drawable icon=item.getIcon().mutate();
if(Build.VERSION.SDK_INT>=26) item.setIconTintList(iconTint);
else icon.setTintList(iconTint);
int pad=V.dp(8);
boolean rtl=icon.getLayoutDirection()==View.LAYOUT_DIRECTION_RTL;
icon=new InsetDrawable(icon, rtl ? pad+addWidth : pad, 0, rtl ? pad : addWidth+pad, 0);
icon=new InsetDrawable(icon, rtl ? addWidth : 0, 0, rtl ? 0 : addWidth, 0);
item.setIcon(icon);
SpannableStringBuilder ssb = new SpannableStringBuilder(item.getTitle());
addMenuIconToTitle(item);
}

public static void addMenuIconToTitle(MenuItem item) {
setMenuItemTitle(item, item.getTitle());
}

public static void setMenuItemTitle(MenuItem item, CharSequence title) {
Drawable icon = item.getIcon();
if(icon==null)
return;

icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight());
CenteredImageSpan iconSpan = new CenteredImageSpan(icon);
SpannableStringBuilder ssb = new SpannableStringBuilder(" ");
ssb.setSpan(iconSpan, 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
ssb.append(title);
item.setTitle(ssb);
}

public static void removeMenuIcons(Menu m) {
for (int i = 0; i < m.size(); i++) {
m.getItem(i).setIcon(null);
}
}

public static void resetPopupItemTint(MenuItem item) {
if (Build.VERSION.SDK_INT >= 26) {
item.setIconTintList(null);
Expand All @@ -944,7 +967,7 @@ public static void enableOptionsMenuIcons(Context context, Menu menu, @IdRes int
try {
Method m = menu.getClass().getDeclaredMethod("setOptionalIconsVisible", Boolean.TYPE);
m.setAccessible(true);
m.invoke(menu, true);
m.invoke(menu, false); // actually makes sure it's disabled now that it's handled through image spans
enableMenuIcons(context, menu, asAction);
} catch (Exception ignored) {
}
Expand All @@ -966,12 +989,12 @@ public static void enableMenuIcons(Context context, Menu m, @IdRes int... exclud
public static void enablePopupMenuIcons(Context context, PopupMenu menu) {
Menu m = menu.getMenu();
if (Build.VERSION.SDK_INT >= 29) {
menu.setForceShowIcon(true);
menu.setForceShowIcon(false);
} else {
try {
Method setOptionalIconsVisible = m.getClass().getDeclaredMethod("setOptionalIconsVisible", boolean.class);
setOptionalIconsVisible.setAccessible(true);
setOptionalIconsVisible.invoke(m, true);
setOptionalIconsVisible.invoke(m, false);
} catch (Exception ignore) {
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,13 +218,13 @@ public boolean onLongClick(float x, float y){
Account account=item.account;

menu.findItem(R.id.edit_note).setVisible(false);
menu.findItem(R.id.manage_user_lists).setTitle(fragment.getString(R.string.sk_lists_with_user, account.getShortUsername()));
UiUtils.setMenuItemTitle(menu.findItem(R.id.manage_user_lists), fragment.getString(R.string.sk_lists_with_user, account.getShortUsername()));
MenuItem mute=menu.findItem(R.id.mute);
mute.setTitle(fragment.getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getShortUsername()));
mute.setIcon(relationship.muting ? R.drawable.ic_fluent_speaker_0_24_regular : R.drawable.ic_fluent_speaker_off_24_regular);
UiUtils.insetPopupMenuIcon(fragment.getContext(), mute);
menu.findItem(R.id.block).setTitle(fragment.getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getShortUsername()));
menu.findItem(R.id.report).setTitle(fragment.getString(R.string.report_user, account.getShortUsername()));
UiUtils.setMenuItemTitle(menu.findItem(R.id.block), fragment.getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getShortUsername()));
UiUtils.setMenuItemTitle(menu.findItem(R.id.report), fragment.getString(R.string.report_user, account.getShortUsername()));
menu.findItem(R.id.manage_user_lists).setVisible(relationship.following);
menu.findItem(R.id.soft_block).setVisible(relationship.followedBy && !relationship.following);
MenuItem hideBoosts=menu.findItem(R.id.hide_boosts);
Expand All @@ -238,7 +238,7 @@ public boolean onLongClick(float x, float y){
}
MenuItem blockDomain=menu.findItem(R.id.block_domain);
if(!account.isLocal()){
blockDomain.setTitle(fragment.getString(relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, account.getDomain()));
UiUtils.setMenuItemTitle(blockDomain, fragment.getString(relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, account.getDomain()));
blockDomain.setVisible(true);
}else{
blockDomain.setVisible(false);
Expand Down

0 comments on commit 725b1d8

Please sign in to comment.