Google Colab上で実行しているループをボタンで止める

## 経緯
　Colabを使っていると、Jupyter notebookのセル上で回しているループを停止させたい場面があったりする。セルの停止はエラーで赤くなって見栄えが悪いし、どうせならボタンを用意して止めたい......。
 
 　なんて思って「colab」「ループ」「停止」「ボタン」なんて調べると、意外に情報がほとんどない。
  
　[かろうじてヒットした一件](https://teratail.com/questions/t3ekt3tnjwi6k3)は、ipywidgetsを使ってスレッドで非同期処理を用いて行うもののよう。残念なことにipywidgetsそのままだとセル実行中は処理されないらしく、これをスレッドにより回避している。ただ、実際に動かしてみるとループごとの表示ができない様子。ボタンで停止した直後にまとめてprint分が出力される。

　ここで、colabにおいてボタンを用意する方法はもう一つあるわけで。ipywidgetsではなくjava scriptを用いる方法を自分のような人のために残しておく。

## 要するに何のコード
　Google Colaboratoryのセルでループを回しながら、java scriptで表示したボタンの押下判定を得るコード。（ループ実行中にボタンでインタラクションするコード）

```python:loopStopButton.ipynb
from IPython.display import display, Javascript
from google.colab import output
from google.colab.output import eval_js
import time

def show_LoopStopButton():
  js = Javascript('''
    var div = null;
    var clicked = false;

    function removeDom() {
      div.remove();
      div = null;
    }

    function jsOnClick() {
      if (clicked) {
        removeDom();
        clicked = false;
        return true;
      }
      return false
    }

    async function js_Interaction() {
      div = document.createElement('div');
      document.body.appendChild(div);

      var button = document.createElement('button');
      button.textContent = "ボタン名";
      button.onclick = function(){ clicked = true; }
      div.appendChild(button)

      return
      } ''')
  display(js)
  eval_js('js_Interaction()')

show_LoopStopButton()

while True:
  if eval_js('jsOnClick()'): break; #要するにループ中にボタンが押されたかの判定をとれる。
  time.sleep(0.1)
print("終了")
```
　Colab上で実行すると、ボタンが表示される。
 
　ボタンを押すとループから抜けて、「終了」とprint出力される。

　ループを止めるだけじゃなくても、ボタン増やして、javascript側の関数の返り値をいじったり、関数自体を増やしたりすれば、セルで回しているループに対してたぶんインタラクションできるはず。

## おまけ

　ループ中にインタラクションするコードの例。シューティングゲームの自機的なものを動かす。print分を毎度消しては書き直す方式をコメントアウトして残しているが、これだと表示がちらつくので、python側の処理をjava script側に渡して表示している。
　正直ラグがひどいし、これだとpython側で処理する意味はまったくない。

```python:colab_LoopInteractionExample.ipynb
from IPython.display import display, Javascript
from google.colab import output
from google.colab.output import eval_js
import time

#ボタン用関数
def show_LoopStopButton():
  js = Javascript('''
    var div = null;
    var div2 = null;
    var textNode = null;
    var clicked = false;
    var Rclicked = false;
    var Lclicked = false;

    function removeDom() {
      div.remove();
      div = null;
    }

    function jsOnClick() {
      if (clicked) {
        removeDom(); //終了時に破棄する用。ループ終了ボタン以外では呼び出さない。
        clicked = false;
        return true;
      }
      return false
    }

    function LbuttonOnClick() {
      if (Lclicked) {
        Lclicked = false;
        return true;
      }
      return false
    }

    function RbuttonOnClick() {
      if (Rclicked) {
        Rclicked = false;
        return true;
      }
      return false
    }

    function textNodeChange(text){
      textNode.textContent = text
      return true
    }

    async function js_Interaction() {
      div = document.createElement('div');
      document.body.appendChild(div);
      div2 = document.createElement('div');
      div.appendChild(div2)

      const button = document.createElement('button');
      button.textContent = "終了";
      button.onclick = function(){ clicked = true; }
      div.appendChild(button)

      var buttonL = document.createElement('button');
      buttonL.textContent = "左";
      buttonL.onclick = function(){ Lclicked = true; }
      div.appendChild(buttonL)

      var buttonR = document.createElement('button');
      buttonR.textContent = "右";
      buttonR.onclick = function(){ Rclicked = true; }
      div.appendChild(buttonR)

      textNode = document.createTextNode('Initializing...');
      div2.appendChild(textNode)

      return
      } ''')
  display(js)
  eval_js('js_Interaction()')

#ここから本文

show_LoopStopButton() #表示の呼び出し

string = "　　　凸　　　" #シューティングゲームの自機的な
while True:
  #ボタン判定ごとに表示文字列（string）の更新
  if eval_js('RbuttonOnClick()'): #ボタン判定
    if string[6] != "凸": string = "　" + string[:6] #右端からさらに右には行けない
  if eval_js('LbuttonOnClick()'): #ボタン判定
    if string[0] != "凸": string = string[1:] + "　" #左端からさらに左にはいけない

  if eval_js('jsOnClick()'): break; #終了ボタンが押されたらブレイク

  #output.clear() #出力をコードからクリア
  #print("｜" + string + "｜") #出力（この方法は描画に少し支障があるのでコメントアウト。js側で描写する）
  eval_js('textNodeChange("{}")'.format("|"+ string + "|")) #js側で表示するテキストを渡す

  time.sleep(0.025)
print("ループから抜けました")
```