---
# System prepended metadata

title: Notebook / Extensions / Main-Menu
tags: [Notebook, Elyra, Extension, JupyterLab, Jupyter]

---

Notebook / Extensions / Main-Menu
===
> #ipython, jupyter notebook, python notebook, jupyterlab, elyra
###### tags: `Jupyter`
###### tags: `Jupyter`, `JupyterLab`, `Notebook`, `Elyra`, `Extension`




:::info
:bulb: **主題重點**
1. 如何添加 menu 選項
2. 點了 menu item 後，跳出 hello 對話框
3. 跟系統要設定值
4. 點了 menu item 後，去變更系統設定值，並接收變更事件
:::

<br>

[TOC]

<br>

<hr>

## 官方範例
- ### [jupyterlab / extension-examples / main-menu](https://github.com/jupyterlab/extension-examples/tree/master/main-menu)
- ### [jupyterlab / extension-examples / settings](https://github.com/jupyterlab/extension-examples/tree/master/settings)

<br>

## 如何從最基本範例改起

### Step 1 - 下載範本
```bash=
$ cookiecutter https://github.com/jupyterlab/extension-cookiecutter-ts
author_name []: tj_tsai
author_email []: 
labextension_name [myextension]: menu_aimaker <---
python_name [menu_aimaker]: 
project_short_description [A JupyterLab extension.]: 
has_settings [n]: n
has_server_extension [n]: n
has_binder [n]: y
repository [https://github.com/github_username/menu_aimaker]:

$ cd menu_aimaker
```
- 之後可啟用 has_settings，就可以不用設定 package.json

<br>

### Step 2 - 安裝相依套件 + 編譯 + 安裝
```bash=
# 安裝相依套件
$ jlpm add @jupyterlab/apputils
...
[1/4] Resolving packages...
...
[2/4] Fetching packages...
...
[3/4] Linking dependencies...
...
[4/4] Building fresh packages...
...

# 才能使用 tsc 指令，才能 build
#$ jlpm run build

# 持續編譯與安裝
$ jlpm run watch
```
- 安裝相依套件後，`package.json` 檔案變化
    安裝前
    ```json
      ...
      "dependencies": {
        "@jupyterlab/application": "^3.1.0"
      },
      ...
    ```
    安裝後
    ```
      ...
      "dependencies": {
        "@jupyterlab/application": "^3.1.0",
        "@jupyterlab/apputils": "^3.2.4"
      },
      ...
    ```

<br>

### Step 3 - 添加 menu 選項
1. **修改 `package.json`**
    :::warning
    :warning: 2022/02/21
    1. jlpm watch 需要重啟，才能重新讀取 `package.json`
    2. 必要屬性
        ```json
        "schemaDir": "schema"
        ```
    :::
    ```diff
      ...
      "files": [
        "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}",
    -   "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}"
    +   "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}",
    +   "schema/*.json"
      ],
      ...
      "jupyterlab": {
        "extension": true,
    -   "outputDir": "menu_aimaker/labextension"
    +   "outputDir": "menu_aimaker/labextension",
    +   "schemaDir": "schema"
      },
      ...
    ```
    
2. **新增 `schema/plugin.json` 檔案**
    :::warning
    :warning: 2022/02/21
    1. jlpm watch 需要重啟，才能重新讀取 `plugin.json`
    2. 必要屬性
        ```json
        {
          "type": "object"
        }
        ```
        Advanced Settings Editor 的基本 UI
        ![](https://i.imgur.com/9eA3X2I.png)
    :::
    ```json=
    {
      "title": "AIMaker Settings",
      "description": "Settings for AIMaker",
      "jupyter.lab.menus": {
        "main": [
          {
            "id": "jp-mainmenu-aimaker-settings",
            "label": "AIMaker",
            "rank": 80,
            "items": [
              {
                "command": "aimaker-menu:toggle-flag"
              }
            ]
          }
        ]
      },
      "properties": {
        "limit": {
          "type": "integer",
          "title": "Limit",
          "description": "This is an example of an integer setting.",
          "default": 25
        },
        "flag": {
          "type": "boolean",
          "title": "Simple flag",
          "description": "This is an example of a boolean setting.",
          "default": false
        }
      },
      "additionalProperties": false,
      "type": "object"
    }
    ```
    - `"jupyter.lab.menus"` 會在 main menu 長出來
    - 其餘選項，會出現在 `Settings > Advanced Settings Editor > AIMaker Settings`
    - properties 存取方式
        http://localhost:8888/lab/api/settings/my_online_examples:plugin
    
3. **重啟 build 指令**
    ```bash=
    # Ctrl+C 中斷，再重啟
    $ jlpm run watch
    ```
    
4. **觀看 JupyterLab**
    - **main menu**
        ![](https://i.imgur.com/2KptR48.png)
        對照 menu 設定：
        ```json=4
          "jupyter.lab.menus": {
            "main": [
              {
                "id": "jp-mainmenu-aimaker-settings",
                "label": "AIMaker",
                "rank": 80,
                "items": [
                  {
                    "command": "aimaker-menu:toggle-flag"
                  }
                ]
              }
            ]
          },
        ```
        - 目前 [AIMaker] 長不出子選項 [Toggle Flag and Increment Limit]
            ![](https://i.imgur.com/vPhfhmT.png)
            - 原因是因為 command 指令在系統中找不到
            - 需要在 code 裡面像系統註冊指令，子選項才生成

    - **Settings > Advanced Settings Editor > AIMaker Settings**
        ![](https://i.imgur.com/mEyqzre.png)
        對照 limit 屬性：
        ```json=18
          "properties": {
            "limit": {
              "type": "integer",
              "title": "Limit",
              "description": "This is an example of an integer setting.",
              "default": 25
            },
        ```

<br>

### Step 4.a - 點了 menu item 後，跳出 hello 對話框
> [![](https://i.imgur.com/kcVlfFH.png)](https://i.imgur.com/kcVlfFH.png)
>
> 點選 ==[AIMaker]== > ==[Say Hello]==，會跳出一個訊息。

```typescript=
import {
  JupyterFrontEnd,
  JupyterFrontEndPlugin
} from '@jupyterlab/application';

const PLUGIN_ID = 'aimaker-menu:plugin';
const COMMAND_ID = 'aimaker-menu:toggle-flag';

/**
 * Initialization data for the aimaker-menu extension.
 */
const plugin: JupyterFrontEndPlugin<void> = {
  id: PLUGIN_ID,
  autoStart: true,
  activate: (app: JupyterFrontEnd) => {
    console.log('aimaker-menu is activated!');
    
    app.commands.addCommand(COMMAND_ID, {
      label: 'Say Hello',
      execute: () => {
        alert('hello');
      }
    });;
  }
};

export default plugin;
```
- **`PLUGIN_ID = 'aimaker-menu:plugin'`**
    - `aimaker-menu` 是當前 NPM 套件名稱 (擴充套件名稱)
    - `plugin` 目前沒有用到，可以隨意命名，下個範例會講解用途
- **`app.commands.addCommand(...)`**
    - 向 JupyterFrontEnd 物件(i.e. `app`)的 command 物件，註冊新的指令
    - 添加：key 和 function
    - 之後就可以呼叫 key，執行對應的 function

<br>

### Step 4.b - 跟系統要設定值
> [![](https://i.imgur.com/WH59vKc.png)](https://i.imgur.com/WH59vKc.png)
> 當 JupyterLab 起來的時候，且在 plugin 載入時，讀取當前的 AIMaker 設定值
```javascript=
import {
  JupyterFrontEnd,
  JupyterFrontEndPlugin
} from '@jupyterlab/application';

import { ISettingRegistry } from '@jupyterlab/settingregistry'

const PLUGIN_ID = 'aimaker-menu:plugin';

/**
 * Initialization data for the aimaker-menu extension.
 */
const plugin: JupyterFrontEndPlugin<void> = {
  id: PLUGIN_ID,
  autoStart: true,
  requires: [ISettingRegistry],
  activate: (app: JupyterFrontEnd, settingRegistry: ISettingRegistry) => {
    console.log('aimaker-menu is activated!');
    console.log('settingsRegistry:', settingRegistry);
    
    
    Promise.all([settingRegistry.load(PLUGIN_ID)])
    .then(([settings]) => {
      // settings type: ISettingRegistry.ISettings
      const limit = settings.get('limit').composite;
      const flag = settings.get('flag').composite;
      alert('AIMaker Settings:\n\n' 
        + ' - limit: ' + limit + '(type: ' + typeof(limit) + ')\n'
        + ' - flag: ' + flag + '(type: ' + typeof(flag) + ')\n'
        );
    });

  }
};

export default plugin;
```
- **要求互動的元件：[ISettingRegistry](https://jupyterlab.github.io/jupyterlab/modules/_settingregistry_src_index_.isettingregistry.html)**
    請求 JupyterLab 傳入 ISettingRegistry 元件

- **透過 promise (非同步程序)**
    等待 `settingRegistry.load(...)` 作業完成
    隨後在 `.then(...)` 中進行讀值，再跳出訊息顯示

- **屬性`composite`**
    > `composite` means the setting value is the composition of the default and the user values.

- **印出 `settings`, `limit`, `flag` 物件**
    ![](https://i.imgur.com/5MLIaz4.png)


<br>

### Step 4.c - 點了 menu item 後，去變更系統設定值，並接收變更事件
> [![](https://i.imgur.com/Kb3hgIw.png)](https://i.imgur.com/Kb3hgIw.png)
> 
> 點選 ==[AIMaker]== > ==[Toggle flag and increase limit]==，
> 會將 flag 值反轉並遞增 limit 值，隨後會觸發設定值變更事件

```typescript=
import {
  JupyterFrontEnd,
  JupyterFrontEndPlugin
} from '@jupyterlab/application';

import { ISettingRegistry } from '@jupyterlab/settingregistry'

const PLUGIN_ID = 'aimaker-menu:plugin';
const COMMAND_ID = 'aimaker-menu:toggle-flag';

/**
 * Initialization data for the aimaker-menu extension.
 */
const plugin: JupyterFrontEndPlugin<void> = {
  id: PLUGIN_ID,
  autoStart: true,
  requires: [ISettingRegistry],
  activate: (app: JupyterFrontEnd, settingRegistry: ISettingRegistry) => {
    console.log('aimaker-menu is activated!');
    console.log('settingsRegistry:', settingRegistry);
    
    let limit = 25;
    let flag = false;
    
    function loadSettings(settings: ISettingRegistry.ISettings): void {
      limit = settings.get('limit').composite as number;
      flag = settings.get('flag').composite as boolean;
      console.log('AIMaker Settings:\n\n' 
        + ' - limit: ' + limit + ' (type: ' + typeof(limit) + ')\n'
        + ' - flag: ' + flag + ' (type: ' + typeof(flag) + ')\n'
        );    
    }
    
    function onSettingsChanged(settings: ISettingRegistry.ISettings): void {
      loadSettings(settings);
    }
    
    Promise.all([app.restored, settingRegistry.load(PLUGIN_ID)])
    .then(([_, settings]) => {
      
      // shows the current settings
      loadSettings(settings);
    
      // listen to the changes
      settings.changed.connect(onSettingsChanged);
      
      // command: toggle flag and increase limit
      app.commands.addCommand(COMMAND_ID, {
        label: 'Toggle flag and increase limit',
        execute: () => {
          console.log('executing:', COMMAND_ID);
          settings.set('limit', limit + 1);
          settings.set('flag', !flag);
        }
      });
    });

  }
};

export default plugin;
```

<br>

### Step 4.d - 官方範例
> 將套件名稱修改為 `aimaker-menu`
```typescript=
import {
  JupyterFrontEnd,
  JupyterFrontEndPlugin
} from '@jupyterlab/application';


import { ISettingRegistry } from '@jupyterlab/settingregistry';

const PLUGIN_ID = 'aimaker-menu:plugin';
const COMMAND_ID = 'aimaker-menu:toggle-flag';

/**
 * Initialization data for the aimaker-menu extension.
 */
const plugin: JupyterFrontEndPlugin<void> = {
  id: PLUGIN_ID,
  autoStart: true,
  requires: [ISettingRegistry],
  activate: (app: JupyterFrontEnd, settingRegistry: ISettingRegistry) => {
    console.log('JupyterLab extension aimaker-menu is activated! x4');
    console.log('settingRegistry', settingRegistry);
    
    let limit = 25;
    let flag = false;

    /**
     * Load the settings for this extension
     *
     * @param setting Extension settings
     */
    function loadSetting(setting: ISettingRegistry.ISettings): void {
      // Read the settings and convert to the correct type
      limit = setting.get('limit').composite as number;
      flag = setting.get('flag').composite as boolean;

      console.log(
        `Settings Example extension: Limit is set to '${limit}' and flag to '${flag}'`
      );
    }
    
    Promise.all([app.restored, settingRegistry.load(PLUGIN_ID)])
      .then(([x, setting]) => {
        console.log('x:', x);
        console.log('setting:', setting);
        
        loadSetting(setting);
        
        // Listen for your plugin setting changes using Signal
        //setting.changed.connect(loadSetting);
        
        app.commands.addCommand(COMMAND_ID, {
          label: 'Toggle Flag and Increment Limit',
          isToggled: () => flag,
          execute: () => {
            // Programmatically change a setting
            Promise.all([
                setting.set('flag', !flag),
                setting.set('limit', limit + 1),
              ])
              .then(() => {
                const newLimit = setting.get('limit').composite as number;
                const newFlag = setting.get('flag').composite as boolean;
                window.alert(
                  `Settings Example extension: Limit is set to '${newLimit}' and flag to '${newFlag}'`
                );
              })
              .catch((reason) => {
                console.error(
                  `Something went wrong when changing the settings.\n${reason}`
                );
              });
          },
        });
        
      })
      .catch((reason) => {
        console.error(
          `Something went wrong when reading the settings.\n${reason}`
        );
      });
  }
};

export default plugin;
```

<br>

### Step 99 - 打包你的套件
- ### [使用 python 套件打包](https://jupyterlab.readthedocs.io/en/stable/extension/extension_tutorial.html#packaging-your-extension)
    1. 安裝 build 套件
        ```
        $ pip install build
        ```
    2. 
        a. 打包成原始碼套件 (source_package.tar.gz)
            ```
            $ python -m build -s
            ```
        b. 打包成 wheel 套件 (.whl)
            ```
            $ python -m build
            ```
        