From b8b98f436de16f8acc2aad20d095553f16060cf4 Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 8 Apr 2024 13:57:24 +0930 Subject: [PATCH] AccessibilityIds --- .../securesms/home/HomeActivity.kt | 6 +- .../securesms/onboarding/LandingActivity.kt | 100 +++++++++++------- .../onboarding/LinkDeviceActivity.kt | 9 +- .../securesms/onboarding/LoadingActivity.kt | 3 +- .../MessageNotificationsActivity.kt | 11 +- .../pickname/PickDisplayNameActivity.kt | 2 +- .../RecoveryPasswordActivity.kt | 11 +- .../thoughtcrime/securesms/ui/Components.kt | 20 ++-- app/src/main/res/layout/activity_settings.xml | 4 +- app/src/main/res/values/strings.xml | 19 +++- 10 files changed, 127 insertions(+), 58 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt index d81f279e3d..570b477824 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt @@ -98,6 +98,7 @@ import org.thoughtcrime.securesms.preferences.SettingsActivity import org.thoughtcrime.securesms.showMuteDialog import org.thoughtcrime.securesms.showSessionDialog import org.thoughtcrime.securesms.ui.AppTheme +import org.thoughtcrime.securesms.ui.GetString import org.thoughtcrime.securesms.ui.OutlineButton import org.thoughtcrime.securesms.ui.PreviewTheme import org.thoughtcrime.securesms.ui.SessionShieldIcon @@ -371,7 +372,8 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), Spacer(Modifier.width(12.dp)) OutlineButton( stringResource(R.string.continue_2), - Modifier.align(Alignment.CenterVertically) + Modifier.align(Alignment.CenterVertically), + contentDescription = GetString(R.string.AccessibilityId_reveal_recovery_phrase_button) ) { startRecoveryPasswordActivity() } } } @@ -394,7 +396,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), tint = Color.Unspecified ) if (newAccount) Text(stringResource(R.string.onboardingAccountCreated), style = MaterialTheme.typography.h4, textAlign = TextAlign.Center) - if (newAccount) Text(stringResource(R.string.onboardingBubbleWelcomeToSession), color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center) + if (newAccount) Text(stringResource(R.string.welcome_to_session), color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center) Divider(modifier = Modifier.padding(vertical = 16.dp)) Text( diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/LandingActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LandingActivity.kt index dedb0ed85e..8ce485a9b5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/LandingActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LandingActivity.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import network.loki.messenger.R @@ -34,7 +35,10 @@ import org.thoughtcrime.securesms.showSessionDialog import org.thoughtcrime.securesms.ui.AppTheme import org.thoughtcrime.securesms.ui.BorderlessButton import org.thoughtcrime.securesms.ui.FilledButton +import org.thoughtcrime.securesms.ui.GetString import org.thoughtcrime.securesms.ui.OutlineButton +import org.thoughtcrime.securesms.ui.PreviewTheme +import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider import org.thoughtcrime.securesms.ui.classicDarkColors import org.thoughtcrime.securesms.ui.contentDescription import org.thoughtcrime.securesms.ui.session_accent @@ -52,7 +56,7 @@ class LandingActivity : BaseActionBarActivity() { setUpActionBarSessionLogo(true) ComposeView(this) - .apply { setContent { LandingScreen() } } + .apply { setContent { AppTheme { LandingScreen() } } } .let(::setContentView) IdentityKeyUtil.generateIdentityKeyPair(this) @@ -63,41 +67,55 @@ class LandingActivity : BaseActionBarActivity() { @Preview @Composable - private fun LandingScreen() { - AppTheme { - Column(modifier = Modifier.padding(horizontal = 36.dp)) { - Spacer(modifier = Modifier.weight(1f)) - Text(stringResource(R.string.onboardingBubblePrivacyInYourPocket), modifier = Modifier.align(Alignment.CenterHorizontally), style = MaterialTheme.typography.h4, textAlign = TextAlign.Center) - Spacer(modifier = Modifier.height(24.dp)) - IncomingText(stringResource(R.string.onboardingBubbleWelcomeToSession)) - Spacer(modifier = Modifier.height(14.dp)) - OutgoingText(stringResource(R.string.onboardingBubbleSessionIsEngineered)) - Spacer(modifier = Modifier.height(14.dp)) - IncomingText(stringResource(R.string.onboardingBubbleNoPhoneNumber)) - Spacer(modifier = Modifier.height(14.dp)) - OutgoingText(stringResource(R.string.onboardingBubbleCreatingAnAccountIsEasy)) - Spacer(modifier = Modifier.weight(1f)) + private fun LandingScreen( + @PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int + ) { + PreviewTheme(themeResId) { + LandingScreen() + } + } - OutlineButton( - text = stringResource(R.string.onboardingAccountCreate), - modifier = Modifier - .width(262.dp) - .align(Alignment.CenterHorizontally)) { startPickDisplayNameActivity() } - Spacer(modifier = Modifier.height(14.dp)) - FilledButton(text = stringResource(R.string.onboardingAccountExists), modifier = Modifier + @Composable + private fun LandingScreen() { + Column(modifier = Modifier.padding(horizontal = 36.dp)) { + Spacer(modifier = Modifier.weight(1f)) + Text(stringResource(R.string.onboardingBubblePrivacyInYourPocket), modifier = Modifier.align(Alignment.CenterHorizontally), style = MaterialTheme.typography.h4, textAlign = TextAlign.Center) + Spacer(modifier = Modifier.height(24.dp)) + IncomingText(stringResource(R.string.onboardingBubbleWelcomeToSession)) + Spacer(modifier = Modifier.height(14.dp)) + OutgoingText(stringResource(R.string.onboardingBubbleSessionIsEngineered)) + Spacer(modifier = Modifier.height(14.dp)) + IncomingText(stringResource(R.string.onboardingBubbleNoPhoneNumber)) + Spacer(modifier = Modifier.height(14.dp)) + OutgoingText(stringResource(R.string.onboardingBubbleCreatingAnAccountIsEasy)) + Spacer(modifier = Modifier.weight(1f)) + + OutlineButton( + text = stringResource(R.string.onboardingAccountCreate), + modifier = Modifier .width(262.dp) - .align(Alignment.CenterHorizontally)) { startLinkDeviceActivity() } - Spacer(modifier = Modifier.height(8.dp)) - BorderlessButton( - text = stringResource(R.string.onboardingTosPrivacy), - modifier = Modifier - .width(262.dp) - .align(Alignment.CenterHorizontally), - fontSize = 11.sp, - lineHeight = 13.sp - ) { openDialog() } - Spacer(modifier = Modifier.height(8.dp)) - } + .align(Alignment.CenterHorizontally), + contentDescription = GetString(R.string.AccessibilityId_create_account_button) + ) { startPickDisplayNameActivity() } + Spacer(modifier = Modifier.height(14.dp)) + FilledButton( + text = stringResource(R.string.onboardingAccountExists), + modifier = Modifier + .width(262.dp) + .align(Alignment.CenterHorizontally), + contentDescription = GetString(R.string.AccessibilityId_restore_account_button) + ) { startLinkDeviceActivity() } + Spacer(modifier = Modifier.height(8.dp)) + BorderlessButton( + text = stringResource(R.string.onboardingTosPrivacy), + modifier = Modifier + .width(262.dp) + .align(Alignment.CenterHorizontally), + contentDescription = GetString(R.string.AccessibilityId_privacy_policy_link), + fontSize = 11.sp, + lineHeight = 13.sp + ) { openDialog() } + Spacer(modifier = Modifier.height(8.dp)) } } @@ -105,8 +123,14 @@ class LandingActivity : BaseActionBarActivity() { showSessionDialog { title(R.string.urlOpen) text(R.string.urlOpenBrowser) - button(R.string.activity_landing_terms_of_service) { open("https://getsession.org/terms-of-service") } - button(R.string.activity_landing_privacy_policy) { open("https://getsession.org/privacy-policy") } + button( + R.string.activity_landing_terms_of_service, + contentDescriptionRes = R.string.AccessibilityId_terms_of_service_link + ) { open("https://getsession.org/terms-of-service") } + button( + R.string.activity_landing_privacy_policy, + contentDescriptionRes = R.string.AccessibilityId_privacy_policy_link + ) { open("https://getsession.org/privacy-policy") } } } @@ -136,8 +160,8 @@ class LandingActivity : BaseActionBarActivity() { private fun ChatText( text: String, color: Color, - textColor: Color = Color.Unspecified, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + textColor: Color = Color.Unspecified ) { Text( text, diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceActivity.kt index a0ec064e71..79fa88101d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceActivity.kt @@ -77,6 +77,7 @@ import org.thoughtcrime.securesms.ui.AppTheme import org.thoughtcrime.securesms.ui.OutlineButton import org.thoughtcrime.securesms.ui.baseBold import org.thoughtcrime.securesms.ui.components.SessionTabRow +import org.thoughtcrime.securesms.ui.contentDescription import org.thoughtcrime.securesms.ui.outlinedTextFieldColors import java.util.concurrent.Executors import javax.inject.Inject @@ -275,6 +276,7 @@ fun RecoveryPassword(state: LinkDeviceState, onChange: (String) -> Unit = {}, on OutlinedTextField( value = state.recoveryPhrase, onValueChange = { onChange(it) }, + modifier = Modifier.contentDescription(R.string.AccessibilityId_recovery_phrase_input), placeholder = { Text(stringResource(R.string.recoveryPasswordEnter)) }, colors = outlinedTextFieldColors(state.error != null), singleLine = true, @@ -289,7 +291,12 @@ fun RecoveryPassword(state: LinkDeviceState, onChange: (String) -> Unit = {}, on ) Spacer(Modifier.size(12.dp)) state.error?.let { - Text(it, style = MaterialTheme.typography.baseBold, color = MaterialTheme.colors.error) + Text( + it, + modifier = Modifier.contentDescription(R.string.AccessibilityId_error_message), + style = MaterialTheme.typography.baseBold, + color = MaterialTheme.colors.error + ) } Spacer(Modifier.weight(2f)) OutlineButton( diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/LoadingActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LoadingActivity.kt index 90bf14ad56..81df868c61 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/LoadingActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LoadingActivity.kt @@ -31,6 +31,7 @@ import org.thoughtcrime.securesms.onboarding.messagenotifications.startPNModeAct import org.thoughtcrime.securesms.onboarding.pickname.startPickDisplayNameActivity import org.thoughtcrime.securesms.ui.AppTheme import org.thoughtcrime.securesms.ui.ProgressArc +import org.thoughtcrime.securesms.ui.contentDescription import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo import javax.inject.Inject @@ -103,7 +104,7 @@ class LoadingActivity: BaseActionBarActivity() { AppTheme { Column { Spacer(modifier = Modifier.weight(1f)) - ProgressArc(animatable.value, modifier = Modifier.align(Alignment.CenterHorizontally)) + ProgressArc(animatable.value, modifier = Modifier.align(Alignment.CenterHorizontally).contentDescription(R.string.AccessibilityId_loading_animation)) Text(stringResource(R.string.waitOneMoment), modifier = Modifier.align(Alignment.CenterHorizontally), style = MaterialTheme.typography.h6) Text(stringResource(R.string.loadAccountProgressMessage), modifier = Modifier.align(Alignment.CenterHorizontally)) Spacer(modifier = Modifier.weight(2f)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotificationsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotificationsActivity.kt index 00b6e6c3e0..71c1786e4c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotificationsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotificationsActivity.kt @@ -38,9 +38,11 @@ import org.thoughtcrime.securesms.BaseActionBarActivity import org.thoughtcrime.securesms.home.HomeActivity import org.thoughtcrime.securesms.notifications.PushRegistry import org.thoughtcrime.securesms.ui.AppTheme +import org.thoughtcrime.securesms.ui.GetString import org.thoughtcrime.securesms.ui.OutlineButton import org.thoughtcrime.securesms.ui.PreviewTheme import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider +import org.thoughtcrime.securesms.ui.contentDescription import org.thoughtcrime.securesms.ui.h8 import org.thoughtcrime.securesms.ui.h9 import org.thoughtcrime.securesms.ui.session_accent @@ -111,6 +113,7 @@ fun MessageNotificationsScreen( R.string.activity_pn_mode_fast_mode, R.string.activity_pn_mode_fast_mode_explanation, R.string.activity_pn_mode_recommended_option_tag, + contentDescription = R.string.AccessibilityId_fast_mode_notifications_button, selected = state.pushEnabled, onClick = { setEnabled(true) } ) @@ -118,6 +121,7 @@ fun MessageNotificationsScreen( NotificationRadioButton( R.string.activity_pn_mode_slow_mode, R.string.activity_pn_mode_slow_mode_explanation, + contentDescription = R.string.AccessibilityId_slow_mode_notifications_button, selected = state.pushDisabled, onClick = { setEnabled(false) } ) @@ -125,8 +129,8 @@ fun MessageNotificationsScreen( OutlineButton( stringResource(R.string.continue_2), modifier = Modifier - .align(Alignment.CenterHorizontally) - .width(262.dp), + .align(Alignment.CenterHorizontally) + .width(262.dp), onClick = onContinue ) Spacer(modifier = Modifier.height(12.dp)) @@ -138,13 +142,14 @@ fun NotificationRadioButton( @StringRes title: Int, @StringRes explanation: Int, @StringRes tag: Int? = null, + @StringRes contentDescription: Int? = null, selected: Boolean = false, onClick: () -> Unit = {} ) { Row { OutlinedButton( onClick = onClick, - modifier = Modifier.weight(1f), + modifier = Modifier.weight(1f).contentDescription(contentDescription), colors = ButtonDefaults.outlinedButtonColors(backgroundColor = MaterialTheme.colors.background, contentColor = Color.White), border = if (selected) BorderStroke(ButtonDefaults.OutlinedBorderSize, session_accent) else ButtonDefaults.outlinedBorder, shape = RoundedCornerShape(8.dp) diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameActivity.kt index fc31cec2aa..720260e7a7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameActivity.kt @@ -106,7 +106,7 @@ class PickDisplayNameActivity : BaseActionBarActivity() { OutlinedTextField( value = state.displayName, - modifier = Modifier.contentDescription(R.string.displayNameEnter), + modifier = Modifier.contentDescription(R.string.AccessibilityId_enter_display_name), onValueChange = { onChange(it) }, placeholder = { Text(stringResource(R.string.displayNameEnter)) }, colors = outlinedTextFieldColors(state.error != null), diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/recoverypassword/RecoveryPasswordActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/recoverypassword/RecoveryPasswordActivity.kt index e57a93a519..b721639014 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/recoverypassword/RecoveryPasswordActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/recoverypassword/RecoveryPasswordActivity.kt @@ -51,6 +51,7 @@ import org.thoughtcrime.securesms.BaseActionBarActivity import org.thoughtcrime.securesms.showSessionDialog import org.thoughtcrime.securesms.ui.AppTheme import org.thoughtcrime.securesms.ui.CellWithPaddingAndMargin +import org.thoughtcrime.securesms.ui.GetString import org.thoughtcrime.securesms.ui.LaunchedEffectAsync import org.thoughtcrime.securesms.ui.LocalExtraColors import org.thoughtcrime.securesms.ui.OutlineButton @@ -59,6 +60,7 @@ import org.thoughtcrime.securesms.ui.SessionShieldIcon import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider import org.thoughtcrime.securesms.ui.classicDarkColors import org.thoughtcrime.securesms.ui.colorDestructive +import org.thoughtcrime.securesms.ui.contentDescription import org.thoughtcrime.securesms.ui.h8 import org.thoughtcrime.securesms.ui.small import kotlin.time.Duration.Companion.seconds @@ -92,7 +94,10 @@ class RecoveryPasswordActivity : BaseActionBarActivity() { title(R.string.recoveryPasswordHidePermanently) text(R.string.recoveryPasswordHidePermanentlyDescription2) cancelButton() - destructiveButton(R.string.yes) { + destructiveButton( + R.string.yes, + contentDescription = R.string.AccessibilityId_confirm_button + ) { viewModel.permanentlyHidePassword() finish() } @@ -102,7 +107,7 @@ class RecoveryPasswordActivity : BaseActionBarActivity() { @Preview @Composable -fun PreviewMessageDetails( +fun PreviewRecoveryPassword( @PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int ) { PreviewTheme(themeResId) { @@ -150,6 +155,7 @@ fun RecoveryPasswordCell(seed: String = "", qrBitmap: Bitmap? = null, copySeed:( Text( seed, modifier = Modifier + .contentDescription(R.string.AccessibilityId_hide_recovery_password_button) .padding(vertical = 24.dp) .border( width = 1.dp, @@ -243,6 +249,7 @@ fun HideRecoveryPasswordCell(onHide: () -> Unit = {}) { } OutlineButton( stringResource(R.string.hide), + contentDescription = GetString(R.string.AccessibilityId_hide_recovery_password_button), modifier = Modifier.align(Alignment.CenterVertically), color = colorDestructive ) { onHide() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt index 8845c69629..cdad3115dd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -71,11 +71,12 @@ import kotlin.math.roundToInt fun OutlineButton( text: String, modifier: Modifier = Modifier, + contentDescription: GetString = GetString(text), color: Color = LocalExtraColors.current.prominentButtonColor, onClick: () -> Unit ) { OutlinedButton( - modifier = modifier.contentDescription(GetString(text)), + modifier = modifier.contentDescription(contentDescription), onClick = onClick, border = BorderStroke(1.dp, color), shape = RoundedCornerShape(50), // = 50% percent @@ -99,7 +100,7 @@ fun OutlineButton( modifier = modifier, onClick = onClick, border = BorderStroke(1.dp, color), - shape = RoundedCornerShape(50), // = 50% percent + shape = RoundedCornerShape(percent = 50), colors = ButtonDefaults.outlinedButtonColors( contentColor = color, backgroundColor = Color.Unspecified @@ -110,7 +111,11 @@ fun OutlineButton( } @Composable -fun FilledButton(text: String, modifier: Modifier = Modifier, onClick: () -> Unit) { +fun FilledButton( + text: String, + modifier: Modifier = Modifier, + contentDescription: GetString? = GetString(text), + onClick: () -> Unit) { OutlinedButton( modifier = modifier.size(108.dp, 34.dp), onClick = onClick, @@ -128,12 +133,13 @@ fun FilledButton(text: String, modifier: Modifier = Modifier, onClick: () -> Uni fun BorderlessButton( text: String, modifier: Modifier = Modifier, + contentDescription: GetString = GetString(text), fontSize: TextUnit = TextUnit.Unspecified, lineHeight: TextUnit = TextUnit.Unspecified, onClick: () -> Unit) { TextButton( onClick = onClick, - modifier = modifier, + modifier = modifier.contentDescription(contentDescription), shape = RoundedCornerShape(50), // = 50% percent colors = ButtonDefaults.outlinedButtonColors( contentColor = MaterialTheme.colors.onBackground, @@ -283,8 +289,10 @@ fun TitledRadioButton(option: RadioOption, onClick: () -> Unit) { @Composable fun Modifier.contentDescription(text: GetString?): Modifier { - val context = LocalContext.current - return text?.let { semantics { contentDescription = it(context) } } ?: this + return text?.let { + val context = LocalContext.current + semantics { contentDescription = it(context) } + } ?: this } @Composable diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml index 1aadcf1cea..43cad996be 100644 --- a/app/src/main/res/layout/activity_settings.xml +++ b/app/src/main/res/layout/activity_settings.xml @@ -85,7 +85,7 @@ android:textColor="?android:textColorPrimary" android:fontFamily="@font/space_mono_regular" android:textAlignment="center" - android:contentDescription="@string/AccessibilityId_session_id" + android:contentDescription="@string/AccessibilityId_account_id" tools:text="05987d601943c267879be41830888066c6a024cbdc9a548d06813924bf3372ea78" /> + android:contentDescription="@string/AccessibilityId_recovery_password_menu_item"> Link Device Session ID + Account ID Continue @@ -132,7 +133,8 @@ User settings Username Privacy - Show recovery password + Recovery password + Recovery password menu item Edit user nickname Apply Cancel @@ -1058,7 +1060,8 @@ Auto-deletes in %1$s Privacy in your pocket. - Welcome to Session + Welcome to Session πŸ‘‹ + Welcome to Session Session is engineered to protect your privacy. "You don’t even need a phone number to sign up. " Creating an account is \ninstant, free, and \nanonymous πŸ‘‡ @@ -1101,4 +1104,16 @@ Save your recovery password Save your recovery password to make sure you don\'t lose access to your account. Account Created + Fast mode notifications button + Slow mode notifications button + Reveal recovery phrase button + Create account button + Restore your session button + Privacy policy link + Terms of service link + Loading animation + Recovery phrase input + Error message + Hide recovery password button + Confirm button