Disable EditText context menu

14.4k Views Asked by At

I am making a vertical EditText for traditional Mongolian. I have successfully implemented it by embedding a slightly modified EditText inside of a rotated ViewGroup. I need to create a completely custom context menu because the system one does not support vertical text and is also not rotated when the ViewGroup is rotated. So I want to disable the system context menu altogether.

Note that this is different than these questions that are just trying to disable copy/paste/etc.:

Although I don't get the context menu appearing in the simulator, I get it appearing in my Android 5.0.2 Xiaomi phone.

I have tried:

I'm open to hacks but I need it to consistently work across devices. Mark Murphy (a Commons Guy) wrote some time back in reply to another user trying to do something similar:

I suspect that even if you come up with an answer, it will not work across devices. Device manufacturers have had a tendency to roll their own "context menu" for EditText, defeating developers' attempts to add items into that context menu. My guess is that trying to block that context menu will have similar results.

Am I out of luck?

The only thing I can think of now is to completely rewrite TextView and EditText from scratch (well, by modifying the Android source). I know someone else who did something similar, but his code isn't open source. Before I take this major step, I want to try asking for a simpler solution here on Stack Overflow.

Update: I've been trying modify the TextView source code for the past two days and it looks like a 6 month project. It is a mass of interrelated classes. I need another solution, but I am out of ideas.

MVCE

This is the simplest way I could think of to recreate the problem. There is nothing necessary from my custom EditText. The layout has a single EditText made by replacing the default project Hello World's TextView. I changed the min API to 11 to avoid dealing with deprecated methods.

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        EditText editText = (EditText) findViewById(R.id.edit_text);
        editText.setCustomSelectionActionModeCallback(new ActionMode.Callback() {
            @Override
            public boolean onCreateActionMode(ActionMode actionMode, Menu menu) { return false; }
            @Override
            public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) { return false; }
            @Override
            public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) { return false; }
            @Override
            public void onDestroyActionMode(ActionMode actionMode) { }
        });
    }
}

The context menu in the simulator (running API 24) still shows when I click on the cursor handle (but not on a long click or double click). Here is an image:

enter image description here

On my Xiaomi MIUI phone running Android 5.0, I get the context menu in all situations (cursor handle click, long click, double click).

Update

Aritra Roy's solution is working in the simulator, on some other devices that he has tested, and on my device. I have accepted his answer because it solves my original problem. The only negative side effect is that text selection is also disabled.

8

There are 8 best solutions below

7
On

try this

mEditText.setClickable(false);
mEditText.setEnabled(false);

UPDATE

Try this solution by Extending the Edittext,

import android.content.Context;
import android.util.AttributeSet;
import android.view.ActionMode;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.EditText;

public class NoMenuEditText extends EditText
{
private final Context context;

/** This is a replacement method for the base TextView class' method of the same name. This 
 * method is used in hidden class android.widget.Editor to determine whether the PASTE/REPLACE popup
 * appears when triggered from the text insertion handle. Returning false forces this window
 * to never appear.
 * @return false
 */
boolean canPaste()
{
   return false;
}

/** This is a replacement method for the base TextView class' method of the same name. This method
 * is used in hidden class android.widget.Editor to determine whether the PASTE/REPLACE popup
 * appears when triggered from the text insertion handle. Returning false forces this window
 * to never appear.
 * @return false
 */
@Override
public boolean isSuggestionsEnabled()
{
    return false;
}

public NoMenuEditText(Context context)
{
    super(context);
    this.context = context;
    init();
}

public NoMenuEditText(Context context, AttributeSet attrs)
{
    super(context, attrs);
    this.context = context;
    init();
}

public NoMenuEditText(Context context, AttributeSet attrs, int defStyle)
{
    super(context, attrs, defStyle);
    this.context = context;
    init();
}

private void init()
{
    this.setCustomSelectionActionModeCallback(new ActionModeCallbackInterceptor());
    this.setLongClickable(false);
}


/**
 * Prevents the action bar (top horizontal bar with cut, copy, paste, etc.) from appearing
 * by intercepting the callback that would cause it to be created, and returning false.
 */
private class ActionModeCallbackInterceptor implements ActionMode.Callback
{
    private final String TAG = NoMenuEditText.class.getSimpleName();

    public boolean onCreateActionMode(ActionMode mode, Menu menu) { return false; }
    public boolean onPrepareActionMode(ActionMode mode, Menu menu) { return false; }
    public boolean onActionItemClicked(ActionMode mode, MenuItem item) { return false; }
    public void onDestroyActionMode(ActionMode mode) {}
}
}

Reference: https://stackoverflow.com/a/28893714/5870896

2
On
mEditText.setLongClickable(false);

Its the simplest way to disable the edit text.

1
On

I have made this code for EditText, and it worked fine for such an issue.

try {
    edtName.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            edtName.setSelection(0);
        }
    });
    edtName.setOnLongClickListener(new View.OnLongClickListener() {
        @Override
        public boolean onLongClick(View v) {
            return true;
        }
    });
    edtName.setCustomSelectionActionModeCallback(new ActionMode.Callback() {
        @Override
        public boolean onCreateActionMode(ActionMode actionMode, Menu menu) { return false; }
        @Override
        public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) { return false; }
        @Override
        public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) { return false; }
        @Override
        public void onDestroyActionMode(ActionMode actionMode) { }
    });
} catch (Exception e) {
    e.printStackTrace();
}
1
On

I also tackled this problem recently and I think I have the best solution for the usecase where we want to enable text selection and text select handles but remove the context menus completely.

There's two context menus

  1. customInsertionActionModeCallback - (paste, select all)
  2. customSelectionActionModeCallback - (cut, copy, share etc)

All the solutions target removing the second menu but not the one that the OP is asking which is the InsertionCallback when OP clicks on the cursor control thumb pin. To remove both callbacks, they both must be overridden

For SelectionCallback (cut, copy, paste)

customSelectionActionModeCallback = object : ActionMode.Callback {
    override fun onCreateActionMode(ActionMode mode, Menu menu) {
        menu.clear()
        return true;
    }

   override fun onPrepareActionMode(ActionMode mode, Menu menu) {
        menu.clear();
        return false;
    }

   override fun onActionItemClicked(ActionMode mode, MenuItem item) {
        return false;
    }

    override fun onDestroyActionMode(ActionMode mode) {

    }

Similarly, for InsertionCallback (selectAll, paste)

    customInsertionActionModeCallback = object : ActionMode.Callback { 
    override fun onCreateActionMode(ActionMode mode, Menu menu) {
        menu.clear()
        return true;
    }

    override fun onPrepareActionMode(ActionMode mode, Menu menu) {
        menu.clear();
        return false;
    }

    override fun onActionItemClicked(ActionMode mode, MenuItem item) {
        return false;
    }

    override fun onDestroyActionMode(ActionMode mode) {

    }
5
On

the solution is very simple

public class MainActivity extends AppCompatActivity {

EditText et_0;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    et_0 = findViewById(R.id.et_0);

    et_0.setCustomSelectionActionModeCallback(new ActionMode.Callback() {
        @Override
        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
            //to keep the text selection capability available ( selection cursor)
            return true;
        }

        @Override
        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
            //to prevent the menu from appearing
            menu.clear();
            return false;
        }

        @Override
        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
            return false;
        }

        @Override
        public void onDestroyActionMode(ActionMode mode) {

        }
    });
   }
}

enter image description here

0
On

I tried all the answers above, but I did not get a complete solution. If you want do disable only the PASTE option, you can try this:

override fun getSelectionStart(): Int {
    for (element in Thread.currentThread().stackTrace) {
        if (element.methodName == "canPaste") {
            return -1
        }
    }
    return super.getSelectionStart()
}

It is just a hack, but I didn't find anything better.

if you want to disable the menu and cursor completely, you can try following class instead of your EditText:

class MaskedCodeEditTextView : EditText {
    constructor(context: Context) : super(context) {
        init()
        blockContextMenu()
    }

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
        init()
        blockContextMenu()
    }

    constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(
        context,
        attrs,
        defStyle
    ) {
        init()
        blockContextMenu()
    }

    override fun getSelectionStart(): Int {
        for (element in Thread.currentThread().stackTrace) {
            if (element.methodName == "canPaste") {
                return -1
            }
        }
        return super.getSelectionStart()
    }

    private fun setInsertionDisabled() {
        try {
            val editorField = TextView::class.java.getDeclaredField("mEditor")
            editorField.isAccessible = true
            val editorObject = editorField[this]
            val editorClass = Class.forName("android.widget.Editor")
            val mInsertionControllerEnabledField =
                editorClass.getDeclaredField("mInsertionControllerEnabled")
            mInsertionControllerEnabledField.isAccessible = true
            mInsertionControllerEnabledField[editorObject] = false
        } catch (ignored: Exception) {
            // ignore exception here
        }
    }

    private fun blockContextMenu() {
        this.customSelectionActionModeCallback = ActionModeCallbackInterceptor()
        this.isLongClickable = false
        setOnClickListener { v: View? ->
            v?.let {
                if(!it.isFocused) {
                    requestFocus()
                } else {
                    clearFocus()
                    requestFocus()
                }
            }
        }
    }

    override fun isSuggestionsEnabled(): Boolean {
        return false
    }

    private fun init() {
        this.customSelectionActionModeCallback = ActionModeCallbackInterceptor()
        this.isLongClickable = false
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        if (event.action == MotionEvent.ACTION_DOWN) {
            setInsertionDisabled()
        }
        return super.onTouchEvent(event)
    }

    private inner class ActionModeCallbackInterceptor : ActionMode.Callback {
        override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
            return false
        }

        override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
            return false
        }

        override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
            return false
        }

        override fun onDestroyActionMode(mode: ActionMode) {}
    }
}
 
1
On

This is how you block the copy paste menu from appearing in any way, shape or form. This bug really drove me crazy, and as with any Samsung bug you know its in their code but you also know they won't fix it any time soon. Anyways, here's wonder wall...

  1. Check if Android.Build.Model.toLowerCase().startsWith('sm-g930'). Do not match the whole string, the last letter is a minor version identifier. I stored this boolean in shouldBlockCopyPaste variable which comes up later.

  2. If it matches you want to block the copy paste menu from showing. This is how you ACTUALLY DO IT!!!

Override these 2 functions, you'll notice my shouldBlockCopyPaste boolean, this is so other devices dont get blocked.

   @Override
   public ActionMode StartActionMode (ActionMode.Callback callback){
      if (shouldBlockCopyPaste) {
        return null;
      } else {
        return super.StartActionMode(callback);
      }
    }

   @Override
   public ActionMode StartActionMode (ActionMode.Callback callback, int type){
      if (shouldBlockCopyPaste) {
        return null;
      } else {
        return super.StartActionMode(callback, type);
      }
    }
0
On

This is a hard problem. I've spent hours and hours researching and testing in my Android Studio 3.4.2.

I propose 3 steps:

a) the setCustomSelectionActionModeCallback "solution" in the original question But it keeps showing the select handles (red drop under the cursor) and "Clipboard + Select All" popup, when you click in the red drop.

b) Create a empty image for select handles. I've created a file called ic_empty.xml under res/drawable.

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
</shape>

c) I've created a style in style.xml for all EditTexts.

Under main theme

 <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        ...
        <item name="android:editTextStyle">@style/TStyle</item>
        ....
 </style>

So you can define you style associating an empty image for lef, middle, and right select handles:

   <style name="TStyle" parent="@android:style/Widget.EditText" >
        <item name="android:textSelectHandle">@drawable/ic_empty</item>
        <item name="android:textSelectHandleLeft">@drawable/ic_empty</item>
        <item name="android:textSelectHandleRight">@drawable/ic_empty</item>
    </style>

If the target is from API 23 one can use setTextAppearance for attach a style in a text inside a EditText. However, the above solution works always

The only remaining problem is that I can rid of double click effect. It selects a word in the text, with pink background. However, it is relatively harmless, but awkward, because it does not require user interaction.

A trick that can be done it's set the highlight color transparent.

EditT.setHighlightColor(Color.TRANSPARENT)  // EditT is a EditText