I have been unable to attach drawings to cells. Instead, I use a visual illusion to make a cell look similar to a button, then use onSelectionChange() to run a script when that cell is clicked. You can't click twice in a row, though. You have to click outside the cell and back in again.
For the visual 3D shading illusion, I use thick colored borders. I give the "button" cell:
- A thick bottom and right border that is a darker color than the rest of the table cells
- A thick top and left border that is lighter than the the bottom/right border and lighter than the rest of the cells

If I need multiple buttons next to each other, I put an extra column between them or it looks wrong to me because the border between them can only have one color. It's a bit annoying to work with. Here they are without an extra column:

Here they are with an extra column:

App script can't listen for someone to click on a cell. It can listen for someone changing which cell is selected, though. When you do that, it's important to check that the click is in the right cell:
function onSelectionChange(event) {
// Act based on text content
var activeCell = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getActiveCell();
var text = activeCell.getValue();
console.log("text", text); // for the debugger
if (text === "thing 1") { function1(); }
else if (text === "thing 2") { function2(); }
}
I had a bunch of these "buttons" and I found it annoying to interact with those cells when I wanted to edit them, so I also used a "property" to activate and deactivate that interaction:
function onSelectionChange(event) {
var isActive = PropertiesService.getScriptProperties().getProperty("active")
console.log("Cells are active", JSON.stringify(isActive));
if ( isActive !== "true" ) { return; }
// Act based on text content
var activeCell = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getActiveCell();
var text = activeCell.getValue();
console.log("text", text); // for the debugger
if (text === "thing 1") { function1(); }
else if (text === "thing 2") { function2(); }
}
function activate() {
// I think these get saved as strings even if you give
// a boolean value, so I just gave a string
PropertiesService.getScriptProperties().setProperty("active", "true");
console.log("After activated", JSON.stringify(PropertiesService.getScriptProperties().getProperty("active")));
}
function deactivate() {
PropertiesService.getScriptProperties().setProperty("active", "false");
console.log("After deactivated", JSON.stringify(PropertiesService.getScriptProperties().getProperty("active")));
}
I did use drawings as buttons with the activate and deactivate functions instead of cells because that was less annoying to program. I did have a lot of cell-type "buttons", so it was worth it.
I also changed the colors of the cells when I activated and deactivated them. This part is a bit untested. My code was much more specific to my situation. I was coloring bigger ranges and using different colors.
function activate() {
PropertiesService.getScriptProperties().setProperty("active", "true");
setStyle("#cccccc", "#000000", "A1");
}
function deactivate() {
PropertiesService.getScriptProperties().setProperty("active", "false");
setStyle("#b7b7b7", "#999999", "A1"); // Maybe not enough contrast for some people
}
function setStyle(backgroundColor, fontColor, cellAddress) {
var range = SpreadsheetApp.getActiveSheet().getRange(cellAddress);
range.setBackground(backgroundColor);
range.setFontColor(fontColor);
}