@kubohiroya/turbowarp-title-menu 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +54 -2
- package/README.md +61 -3
- package/dist/extension-manifest.json +51 -1
- package/dist/turbowarp-title-menu.js +92 -13
- package/package.json +1 -1
- package/src/block-definitions.json +56 -0
- package/src/extension.ts +110 -14
package/README.ja.md
CHANGED
|
@@ -11,7 +11,7 @@ TurboWarp拡張やPackager実行環境で使える、汎用のタイトル表示
|
|
|
11
11
|
## できること
|
|
12
12
|
|
|
13
13
|
- 起動直後のタイトル表示として、タイトル、作者名、ライセンス、公式Webサイト、閉じるボタンを表示します。
|
|
14
|
-
- ステージ上にアプリケーションメニューを表示します。共有primitiveの`@kubohiroya/turbowarp-app-shell
|
|
14
|
+
- ステージ上にアプリケーションメニューを表示します。共有primitiveの`@kubohiroya/turbowarp-app-shell`の上に作っており、項目はblockから定義してhatで受けられるため、固定の項目に縛られません。
|
|
15
15
|
- DSLファイル管理ダイアログで、追加、開く、名前の変更、個別削除ができ、名前・更新日時・サイズで並べ替えできます。
|
|
16
16
|
- 複数のDSLファイルをIndexedDBに保管します。TM Kamishibai専用ではありません。
|
|
17
17
|
- ホスト側ランタイムから直接組み込める Composition API を提供します。
|
|
@@ -33,7 +33,7 @@ IndexedDBはoriginごとに分かれます。TurboWarp Web、TurboWarp Desktop
|
|
|
33
33
|
## インストール
|
|
34
34
|
|
|
35
35
|
```bash
|
|
36
|
-
pnpm add --save-exact @kubohiroya/turbowarp-title-menu@0.1
|
|
36
|
+
pnpm add --save-exact @kubohiroya/turbowarp-title-menu@0.2.1
|
|
37
37
|
```
|
|
38
38
|
|
|
39
39
|
TurboWarpでは `dist/turbowarp-title-menu.js` をCustom Extensionとして読み込み、sandboxなしの実行を許可します。
|
|
@@ -49,6 +49,19 @@ when green flag clicked
|
|
|
49
49
|
show title dialog
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
+
メニューは4つの組み込み項目から始まります。合わない場合は、プロジェクト自身の語彙に置き換えます。
|
|
53
|
+
|
|
54
|
+
```text
|
|
55
|
+
when green flag clicked
|
|
56
|
+
clear app menu actions
|
|
57
|
+
add app menu action [pair] labelled [統合PCとつなぐ]
|
|
58
|
+
add app menu action [calibrate] labelled [カメラを校正する]
|
|
59
|
+
show application menu
|
|
60
|
+
|
|
61
|
+
when app menu action [pair v] selected
|
|
62
|
+
broadcast [start pairing v]
|
|
63
|
+
```
|
|
64
|
+
|
|
52
65
|
DSLを読み込んだ後の処理は `when a DSL source is opened` から始めます。
|
|
53
66
|
|
|
54
67
|
```text
|
|
@@ -76,6 +89,42 @@ TurboWarpステージ上にアプリケーションメニューを表示しま
|
|
|
76
89
|
| Type | Command |
|
|
77
90
|
| Opcode | `showMenu` |
|
|
78
91
|
|
|
92
|
+
### `add app menu action [ACTION] labelled [LABEL]`
|
|
93
|
+
|
|
94
|
+
このプロジェクトが持つメニュー項目を追加します。すでに追加済みのIDなら表示名を変更します。
|
|
95
|
+
|
|
96
|
+
| Property | Value |
|
|
97
|
+
|---|---|
|
|
98
|
+
| Type | Command |
|
|
99
|
+
| Opcode | `addAppMenuAction` |
|
|
100
|
+
|
|
101
|
+
### `clear app menu actions`
|
|
102
|
+
|
|
103
|
+
組み込みを含め、すべてのメニュー項目を削除します。プロジェクト自身の項目だけにしたいときに使います。
|
|
104
|
+
|
|
105
|
+
| Property | Value |
|
|
106
|
+
|---|---|
|
|
107
|
+
| Type | Command |
|
|
108
|
+
| Opcode | `clearAppMenuActions` |
|
|
109
|
+
|
|
110
|
+
### `set app menu action [ACTION] enabled [ENABLED]`
|
|
111
|
+
|
|
112
|
+
メニュー項目の1つを有効/無効にします。
|
|
113
|
+
|
|
114
|
+
| Property | Value |
|
|
115
|
+
|---|---|
|
|
116
|
+
| Type | Command |
|
|
117
|
+
| Opcode | `setAppMenuActionEnabled` |
|
|
118
|
+
|
|
119
|
+
### `when app menu action [ACTION] selected`
|
|
120
|
+
|
|
121
|
+
運用者がそのメニュー項目を選んだときに実行されます。
|
|
122
|
+
|
|
123
|
+
| Property | Value |
|
|
124
|
+
|---|---|
|
|
125
|
+
| Type | Hat |
|
|
126
|
+
| Opcode | `whenAppMenuActionSelected` |
|
|
127
|
+
|
|
79
128
|
### `show DSL file manager`
|
|
80
129
|
|
|
81
130
|
保管したDSLファイルの追加、開く、名前の変更、削除、並べ替えを行うダイアログを表示します。
|
|
@@ -152,6 +201,9 @@ IndexedDBに保管しているDSLファイルの件数を返します。
|
|
|
152
201
|
|
|
153
202
|
| 場面 | 動作 |
|
|
154
203
|
|---|---|
|
|
204
|
+
| 組み込みのメニュー項目 | `files`/`reload`/`about`/`close` は最初から登録されており、それぞれの動作を保ちます。同時にhatも発火します。`clear app menu actions` で外せます。 |
|
|
205
|
+
| メニューの変更 | 項目の追加や消去でメニューを作り直します。表示中だった場合は消えずに再表示します。 |
|
|
206
|
+
| 項目が0件のとき | 1件も登録されていない状態では `show application menu` は何もせず、すべて消すと表示中のメニューは閉じます。土台のprimitiveが0件のメニューを作れないため、自分の項目を入れる前に全消しするプロジェクトが途中で止まらないようにしています。 |
|
|
155
207
|
| ファイルの追加 | 「ファイルを追加」はファイル選択ダイアログを開き、選ばれた内容を保管します。開きはしません。使うときは「開く」を押します。 |
|
|
156
208
|
| 名前の変更 | 名前は一意です。他のファイルと同じ名前にしようとすると失敗し、どちらのファイルも変わりません。 |
|
|
157
209
|
| 削除 | 削除には確認のためもう一度クリックが必要です。開いているファイルを削除すると、開いている内容も解除されます。 |
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ Reusable title, application menu, and DSL source storage controls for TurboWarp
|
|
|
9
9
|
## What it does
|
|
10
10
|
|
|
11
11
|
- Shows a reusable title dialog with title, author, license, official website, language, and close controls.
|
|
12
|
-
- Shows a stage-mounted application menu built on the shared `@kubohiroya/turbowarp-app-shell` primitive
|
|
12
|
+
- Shows a stage-mounted application menu built on the shared `@kubohiroya/turbowarp-app-shell` primitive. A project defines its own actions from blocks and reacts to them with a hat, so the menu is not limited to a fixed set.
|
|
13
13
|
- Shows a DSL file manager that adds, opens, renames, and deletes stored files, and sorts them by name, update time, or size.
|
|
14
14
|
- Stores many DSL files in IndexedDB without tying the mechanism to TM Kamishibai.
|
|
15
15
|
- Exposes a small Composition API for projects that want to wire the controls into their own runtime.
|
|
@@ -48,7 +48,7 @@ The reviewed JavaScript build is committed to this repository, so users do not n
|
|
|
48
48
|
Install an exact version that you have reviewed:
|
|
49
49
|
|
|
50
50
|
```bash
|
|
51
|
-
pnpm add --save-exact @kubohiroya/turbowarp-title-menu@0.1
|
|
51
|
+
pnpm add --save-exact @kubohiroya/turbowarp-title-menu@0.2.1
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
Load the standalone bundle from:
|
|
@@ -60,7 +60,7 @@ node_modules/@kubohiroya/turbowarp-title-menu/dist/turbowarp-title-menu.js
|
|
|
60
60
|
A version-pinned CDN URL is:
|
|
61
61
|
|
|
62
62
|
```text
|
|
63
|
-
https://cdn.jsdelivr.net/npm/@kubohiroya/turbowarp-title-menu@0.1
|
|
63
|
+
https://cdn.jsdelivr.net/npm/@kubohiroya/turbowarp-title-menu@0.2.1/dist/turbowarp-title-menu.js
|
|
64
64
|
```
|
|
65
65
|
|
|
66
66
|
## Quick start
|
|
@@ -69,6 +69,20 @@ https://cdn.jsdelivr.net/npm/@kubohiroya/turbowarp-title-menu@0.1.0/dist/turbowa
|
|
|
69
69
|
2. Run `show title dialog` immediately after startup.
|
|
70
70
|
3. Use `show application menu`, or `show DSL file manager` directly, when the project should expose DSL file actions.
|
|
71
71
|
|
|
72
|
+
The menu starts with four built-in actions. Replace them with the project's own vocabulary when the
|
|
73
|
+
built-in ones do not fit:
|
|
74
|
+
|
|
75
|
+
```text
|
|
76
|
+
when green flag clicked
|
|
77
|
+
clear app menu actions
|
|
78
|
+
add app menu action [pair] labelled [Pair with fusion PC]
|
|
79
|
+
add app menu action [calibrate] labelled [Calibrate camera]
|
|
80
|
+
show application menu
|
|
81
|
+
|
|
82
|
+
when app menu action [pair v] selected
|
|
83
|
+
broadcast [start pairing v]
|
|
84
|
+
```
|
|
85
|
+
|
|
72
86
|
```text
|
|
73
87
|
when green flag clicked
|
|
74
88
|
show title dialog
|
|
@@ -98,6 +112,47 @@ Shows the application menu above the TurboWarp stage.
|
|
|
98
112
|
| Type | Command |
|
|
99
113
|
| Opcode | `showMenu` |
|
|
100
114
|
|
|
115
|
+
### `add app menu action [ACTION] labelled [LABEL]`
|
|
116
|
+
|
|
117
|
+
Adds an application menu action this project owns, or relabels one it already added.
|
|
118
|
+
|
|
119
|
+
| Property | Value |
|
|
120
|
+
|---|---|
|
|
121
|
+
| Type | Command |
|
|
122
|
+
| Opcode | `addAppMenuAction` |
|
|
123
|
+
| `ACTION` | String, default: `start` |
|
|
124
|
+
| `LABEL` | String, default: `Start` |
|
|
125
|
+
|
|
126
|
+
### `clear app menu actions`
|
|
127
|
+
|
|
128
|
+
Removes every application menu action, including the built-in ones, so a project can define its own set.
|
|
129
|
+
|
|
130
|
+
| Property | Value |
|
|
131
|
+
|---|---|
|
|
132
|
+
| Type | Command |
|
|
133
|
+
| Opcode | `clearAppMenuActions` |
|
|
134
|
+
|
|
135
|
+
### `set app menu action [ACTION] enabled [ENABLED]`
|
|
136
|
+
|
|
137
|
+
Enables or disables one application menu action.
|
|
138
|
+
|
|
139
|
+
| Property | Value |
|
|
140
|
+
|---|---|
|
|
141
|
+
| Type | Command |
|
|
142
|
+
| Opcode | `setAppMenuActionEnabled` |
|
|
143
|
+
| `ACTION` | String, default: `undefined` |
|
|
144
|
+
| `ENABLED` | Boolean, default: `true` |
|
|
145
|
+
|
|
146
|
+
### `when app menu action [ACTION] selected`
|
|
147
|
+
|
|
148
|
+
Runs when the operator selects the named application menu action.
|
|
149
|
+
|
|
150
|
+
| Property | Value |
|
|
151
|
+
|---|---|
|
|
152
|
+
| Type | Hat |
|
|
153
|
+
| Opcode | `whenAppMenuActionSelected` |
|
|
154
|
+
| `ACTION` | String, default: `undefined` |
|
|
155
|
+
|
|
101
156
|
### `show DSL file manager`
|
|
102
157
|
|
|
103
158
|
Shows the dialog that adds, opens, renames, deletes, and sorts stored DSL files.
|
|
@@ -178,6 +233,9 @@ Returns the most recent DSL storage failure in the interface language, or an emp
|
|
|
178
233
|
|---|---|
|
|
179
234
|
| Startup title | `show title dialog` mounts a dialog over the stage and keeps Scratch sprites untouched. |
|
|
180
235
|
| Official website | The title dialog opens the configured website URL in a new browser tab. |
|
|
236
|
+
| Built-in menu actions | `files`, `reload`, `about`, and `close` are pre-registered and keep their own behavior. They also start the hat, and `clear app menu actions` removes them. |
|
|
237
|
+
| Changing the menu | Adding or clearing an action rebuilds the menu. A menu that was on screen is shown again rather than disappearing. |
|
|
238
|
+
| An empty menu | `show application menu` does nothing while no action is registered, and clearing every action closes an open menu. The underlying primitive refuses to build a menu with no actions, so a project that clears the set before installing its own is not interrupted. |
|
|
181
239
|
| Adding a DSL file | `Add file` opens a browser file picker and stores the chosen source. It does not open the file; the operator presses `Open` when the project should use it. |
|
|
182
240
|
| Renaming | Names are unique. Renaming a file to a name another file already uses fails and leaves both files unchanged. |
|
|
183
241
|
| Deleting | Deleting asks for a second confirming click, and deleting the open file clears the opened source. |
|
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
"formatVersion": 1,
|
|
3
3
|
"id": "kubohiroyaturbowarptitlemenu",
|
|
4
4
|
"blocks": [
|
|
5
|
+
{
|
|
6
|
+
"opcode": "addAppMenuAction",
|
|
7
|
+
"blockType": "COMMAND",
|
|
8
|
+
"arguments": [
|
|
9
|
+
{
|
|
10
|
+
"id": "ACTION",
|
|
11
|
+
"type": "STRING"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"id": "LABEL",
|
|
15
|
+
"type": "STRING"
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"opcode": "clearAppMenuActions",
|
|
21
|
+
"blockType": "COMMAND",
|
|
22
|
+
"arguments": []
|
|
23
|
+
},
|
|
5
24
|
{
|
|
6
25
|
"opcode": "hasSavedDsl",
|
|
7
26
|
"blockType": "BOOLEAN",
|
|
@@ -32,6 +51,21 @@
|
|
|
32
51
|
"blockType": "REPORTER",
|
|
33
52
|
"arguments": []
|
|
34
53
|
},
|
|
54
|
+
{
|
|
55
|
+
"opcode": "setAppMenuActionEnabled",
|
|
56
|
+
"blockType": "COMMAND",
|
|
57
|
+
"arguments": [
|
|
58
|
+
{
|
|
59
|
+
"id": "ACTION",
|
|
60
|
+
"type": "STRING",
|
|
61
|
+
"menu": "menuActions"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"id": "ENABLED",
|
|
65
|
+
"type": "BOOLEAN"
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
},
|
|
35
69
|
{
|
|
36
70
|
"opcode": "showDslFiles",
|
|
37
71
|
"blockType": "COMMAND",
|
|
@@ -47,11 +81,27 @@
|
|
|
47
81
|
"blockType": "COMMAND",
|
|
48
82
|
"arguments": []
|
|
49
83
|
},
|
|
84
|
+
{
|
|
85
|
+
"opcode": "whenAppMenuActionSelected",
|
|
86
|
+
"blockType": "HAT",
|
|
87
|
+
"arguments": [
|
|
88
|
+
{
|
|
89
|
+
"id": "ACTION",
|
|
90
|
+
"type": "STRING",
|
|
91
|
+
"menu": "menuActions"
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
},
|
|
50
95
|
{
|
|
51
96
|
"opcode": "whenDslSourceOpened",
|
|
52
97
|
"blockType": "HAT",
|
|
53
98
|
"arguments": []
|
|
54
99
|
}
|
|
55
100
|
],
|
|
56
|
-
"menus": [
|
|
101
|
+
"menus": [
|
|
102
|
+
{
|
|
103
|
+
"id": "menuActions",
|
|
104
|
+
"acceptReporters": true
|
|
105
|
+
}
|
|
106
|
+
]
|
|
57
107
|
}
|
|
@@ -1183,9 +1183,11 @@
|
|
|
1183
1183
|
});
|
|
1184
1184
|
}
|
|
1185
1185
|
const extensionName = "TurboWarp Title Menu";
|
|
1186
|
-
const
|
|
1186
|
+
const menus = { "menuActions": { "acceptReporters": true, "items": "menuActionItems" } };
|
|
1187
|
+
const blocks = [{ "opcode": "showTitle", "blockType": "COMMAND", "text": "show title dialog", "description": "Shows the configured title dialog above the TurboWarp stage." }, { "opcode": "showMenu", "blockType": "COMMAND", "text": "show application menu", "description": "Shows the application menu above the TurboWarp stage." }, { "opcode": "addAppMenuAction", "blockType": "COMMAND", "text": "add app menu action [ACTION] labelled [LABEL]", "description": "Adds an application menu action this project owns, or relabels one it already added.", "arguments": { "ACTION": { "type": "STRING", "defaultValue": "start" }, "LABEL": { "type": "STRING", "defaultValue": "Start" } } }, { "opcode": "clearAppMenuActions", "blockType": "COMMAND", "text": "clear app menu actions", "description": "Removes every application menu action, including the built-in ones, so a project can define its own set." }, { "opcode": "setAppMenuActionEnabled", "blockType": "COMMAND", "text": "set app menu action [ACTION] enabled [ENABLED]", "description": "Enables or disables one application menu action.", "arguments": { "ACTION": { "type": "STRING", "menu": "menuActions" }, "ENABLED": { "type": "BOOLEAN", "defaultValue": true } } }, { "opcode": "whenAppMenuActionSelected", "blockType": "HAT", "text": "when app menu action [ACTION] selected", "description": "Runs when the operator selects the named application menu action.", "arguments": { "ACTION": { "type": "STRING", "menu": "menuActions" } } }, { "opcode": "showDslFiles", "blockType": "COMMAND", "text": "show DSL file manager", "description": "Shows the dialog that adds, opens, renames, deletes, and sorts stored DSL files." }, { "opcode": "whenDslSourceOpened", "blockType": "HAT", "text": "when a DSL source is opened", "description": "Runs after the operator opens a stored DSL file, or after the opened source is announced again." }, { "opcode": "reloadOpenedDsl", "blockType": "COMMAND", "text": "reload the opened DSL source", "description": "Announces the currently opened DSL source again without showing a dialog." }, { "opcode": "openedDslName", "blockType": "REPORTER", "text": "opened DSL file name", "description": "Returns the name of the DSL file that is currently open, or an empty string." }, { "opcode": "openedDslSource", "blockType": "REPORTER", "text": "opened DSL source", "description": "Returns the text of the DSL file that is currently open, or an empty string." }, { "opcode": "hasSavedDsl", "blockType": "BOOLEAN", "text": "has a saved DSL file?", "description": "Reports whether at least one DSL file is stored in IndexedDB." }, { "opcode": "savedDslCount", "blockType": "REPORTER", "text": "saved DSL file count", "description": "Returns how many DSL files are stored in IndexedDB." }, { "opcode": "lastDslError", "blockType": "REPORTER", "text": "last DSL storage error", "description": "Returns the most recent DSL storage failure in the interface language, or an empty string." }];
|
|
1187
1188
|
const definitions = {
|
|
1188
1189
|
extensionName,
|
|
1190
|
+
menus,
|
|
1189
1191
|
blocks
|
|
1190
1192
|
};
|
|
1191
1193
|
const titleLocales = Object.freeze({
|
|
@@ -1273,6 +1275,7 @@
|
|
|
1273
1275
|
}
|
|
1274
1276
|
const blockDefinitions = definitions.blocks;
|
|
1275
1277
|
const dslFileAccept = ".txt,.yaml,.yml,.json,.k4,.kamishibai";
|
|
1278
|
+
const builtinActionIds = ["files", "reload", "about", "close"];
|
|
1276
1279
|
function stageMount() {
|
|
1277
1280
|
return Scratch.vm?.renderer?.canvas?.parentElement ?? globalThis.document?.body;
|
|
1278
1281
|
}
|
|
@@ -1284,6 +1287,12 @@
|
|
|
1284
1287
|
this.store = null;
|
|
1285
1288
|
this.openedRecord = null;
|
|
1286
1289
|
this.lastError = "";
|
|
1290
|
+
this.menuActions = builtinActionIds.map((id) => ({
|
|
1291
|
+
id,
|
|
1292
|
+
labels: { en: menuLocales.en[id], ja: menuLocales.ja[id] },
|
|
1293
|
+
enabled: true
|
|
1294
|
+
}));
|
|
1295
|
+
this.menuVisible = false;
|
|
1287
1296
|
}
|
|
1288
1297
|
getInfo() {
|
|
1289
1298
|
return {
|
|
@@ -1291,14 +1300,57 @@
|
|
|
1291
1300
|
name: Scratch.translate(definitions.extensionName),
|
|
1292
1301
|
docsURI: extensionConfig.docsURI,
|
|
1293
1302
|
blockIconURI: extensionConfig.blockIconURI,
|
|
1294
|
-
blocks: blockDefinitions.map((block) => this.toScratchBlock(block))
|
|
1303
|
+
blocks: blockDefinitions.map((block) => this.toScratchBlock(block)),
|
|
1304
|
+
menus: definitions.menus
|
|
1295
1305
|
};
|
|
1296
1306
|
}
|
|
1297
1307
|
showTitle() {
|
|
1298
1308
|
this.ensureTitleDialog().show(this.locale());
|
|
1299
1309
|
}
|
|
1310
|
+
/**
|
|
1311
|
+
* Shows the application menu.
|
|
1312
|
+
*
|
|
1313
|
+
* An empty registry does nothing: the underlying primitive requires at least one action, and a
|
|
1314
|
+
* project that cleared the actions to install its own should not be interrupted by an error
|
|
1315
|
+
* between the two steps.
|
|
1316
|
+
*/
|
|
1300
1317
|
showMenu() {
|
|
1318
|
+
if (this.menuActions.length === 0) return;
|
|
1301
1319
|
this.ensureApplicationMenu().show(this.locale());
|
|
1320
|
+
this.menuVisible = true;
|
|
1321
|
+
}
|
|
1322
|
+
/** Backs the dynamic `menuActions` dropdown, so it always lists what the project registered. */
|
|
1323
|
+
menuActionItems() {
|
|
1324
|
+
if (this.menuActions.length === 0) return [{ text: "—", value: "" }];
|
|
1325
|
+
const locale = this.locale();
|
|
1326
|
+
return this.menuActions.map((action) => ({ text: action.labels[locale], value: action.id }));
|
|
1327
|
+
}
|
|
1328
|
+
addAppMenuAction(args) {
|
|
1329
|
+
const id = Scratch.Cast.toString(args.ACTION).trim();
|
|
1330
|
+
if (id.length === 0) return;
|
|
1331
|
+
const label = Scratch.Cast.toString(args.LABEL);
|
|
1332
|
+
const existing = this.menuActions.find((action) => action.id === id);
|
|
1333
|
+
if (existing === void 0) {
|
|
1334
|
+
this.menuActions.push({ id, labels: { en: label, ja: label }, enabled: true });
|
|
1335
|
+
} else {
|
|
1336
|
+
existing.labels = { en: label, ja: label };
|
|
1337
|
+
}
|
|
1338
|
+
this.rebuildMenu();
|
|
1339
|
+
}
|
|
1340
|
+
clearAppMenuActions() {
|
|
1341
|
+
this.menuActions = [];
|
|
1342
|
+
this.rebuildMenu();
|
|
1343
|
+
}
|
|
1344
|
+
setAppMenuActionEnabled(args) {
|
|
1345
|
+
const id = Scratch.Cast.toString(args.ACTION);
|
|
1346
|
+
const action = this.menuActions.find((entry) => entry.id === id);
|
|
1347
|
+
if (action === void 0) return;
|
|
1348
|
+
action.enabled = Scratch.Cast.toBoolean(args.ENABLED);
|
|
1349
|
+
this.applicationMenu?.setActionState(id, { enabled: action.enabled });
|
|
1350
|
+
}
|
|
1351
|
+
/** Started by the menu callback, so the handler only has to accept the match. */
|
|
1352
|
+
whenAppMenuActionSelected() {
|
|
1353
|
+
return true;
|
|
1302
1354
|
}
|
|
1303
1355
|
showDslFiles() {
|
|
1304
1356
|
return this.ensureFilesDialog().show(this.locale());
|
|
@@ -1347,6 +1399,36 @@
|
|
|
1347
1399
|
recordFailure(error) {
|
|
1348
1400
|
this.lastError = describeStoreError(this.locale(), error);
|
|
1349
1401
|
}
|
|
1402
|
+
/**
|
|
1403
|
+
* Rebuilds the menu after its action list changed.
|
|
1404
|
+
*
|
|
1405
|
+
* The app-shell primitive fixes its actions at construction, so a changed list means a new menu.
|
|
1406
|
+
* A menu that was on screen is shown again, because a project that adds an action while the menu
|
|
1407
|
+
* is open should not have it silently disappear. Clearing every action closes the menu instead,
|
|
1408
|
+
* because the primitive refuses to build one with no actions.
|
|
1409
|
+
*/
|
|
1410
|
+
rebuildMenu() {
|
|
1411
|
+
this.applicationMenu?.dispose();
|
|
1412
|
+
this.applicationMenu = null;
|
|
1413
|
+
if (!this.menuVisible) return;
|
|
1414
|
+
if (this.menuActions.length === 0) {
|
|
1415
|
+
this.menuVisible = false;
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
this.showMenu();
|
|
1419
|
+
}
|
|
1420
|
+
selectMenuAction(id) {
|
|
1421
|
+
Scratch.vm?.runtime?.startHats?.(`${extensionConfig.id}_whenAppMenuActionSelected`, { ACTION: id });
|
|
1422
|
+
if (!builtinActionIds.includes(id)) return;
|
|
1423
|
+
if (id === "files") void this.showDslFiles();
|
|
1424
|
+
if (id === "reload") this.reloadOpenedDsl();
|
|
1425
|
+
if (id === "about") this.showTitle();
|
|
1426
|
+
if (id === "close") this.hideMenu();
|
|
1427
|
+
}
|
|
1428
|
+
hideMenu() {
|
|
1429
|
+
this.applicationMenu?.hide();
|
|
1430
|
+
this.menuVisible = false;
|
|
1431
|
+
}
|
|
1350
1432
|
announce(eventName, record) {
|
|
1351
1433
|
dispatchDslSourceEvent(eventName, record);
|
|
1352
1434
|
Scratch.vm?.runtime?.startHats?.(`${extensionConfig.id}_whenDslSourceOpened`);
|
|
@@ -1374,20 +1456,16 @@
|
|
|
1374
1456
|
if (this.applicationMenu) return this.applicationMenu;
|
|
1375
1457
|
const mount = stageMount();
|
|
1376
1458
|
if (mount === void 0) throw new TypeError("a stage container is required to show the menu");
|
|
1377
|
-
const labels = (key) => ({
|
|
1378
|
-
en: menuLocales.en[key],
|
|
1379
|
-
ja: menuLocales.ja[key]
|
|
1380
|
-
});
|
|
1381
1459
|
this.applicationMenu = createAppShellApplicationMenu({
|
|
1382
1460
|
document: globalThis.document,
|
|
1383
1461
|
mount,
|
|
1384
1462
|
initialLocale: this.locale(),
|
|
1385
|
-
actions:
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1463
|
+
actions: this.menuActions.map((action) => ({
|
|
1464
|
+
id: action.id,
|
|
1465
|
+
labels: action.labels,
|
|
1466
|
+
enabled: action.enabled,
|
|
1467
|
+
onSelect: () => this.selectMenuAction(action.id)
|
|
1468
|
+
}))
|
|
1391
1469
|
});
|
|
1392
1470
|
return this.applicationMenu;
|
|
1393
1471
|
}
|
|
@@ -1456,7 +1534,8 @@
|
|
|
1456
1534
|
name,
|
|
1457
1535
|
{
|
|
1458
1536
|
type: Scratch.ArgumentType[argument.type],
|
|
1459
|
-
defaultValue: argument.defaultValue
|
|
1537
|
+
...argument.defaultValue === void 0 ? {} : { defaultValue: argument.defaultValue },
|
|
1538
|
+
...argument.menu === void 0 ? {} : { menu: argument.menu }
|
|
1460
1539
|
}
|
|
1461
1540
|
])
|
|
1462
1541
|
)
|
package/package.json
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"extensionName": "TurboWarp Title Menu",
|
|
3
|
+
"menus": {
|
|
4
|
+
"menuActions": {
|
|
5
|
+
"acceptReporters": true,
|
|
6
|
+
"items": "menuActionItems"
|
|
7
|
+
}
|
|
8
|
+
},
|
|
3
9
|
"blocks": [
|
|
4
10
|
{
|
|
5
11
|
"opcode": "showTitle",
|
|
@@ -13,6 +19,56 @@
|
|
|
13
19
|
"text": "show application menu",
|
|
14
20
|
"description": "Shows the application menu above the TurboWarp stage."
|
|
15
21
|
},
|
|
22
|
+
{
|
|
23
|
+
"opcode": "addAppMenuAction",
|
|
24
|
+
"blockType": "COMMAND",
|
|
25
|
+
"text": "add app menu action [ACTION] labelled [LABEL]",
|
|
26
|
+
"description": "Adds an application menu action this project owns, or relabels one it already added.",
|
|
27
|
+
"arguments": {
|
|
28
|
+
"ACTION": {
|
|
29
|
+
"type": "STRING",
|
|
30
|
+
"defaultValue": "start"
|
|
31
|
+
},
|
|
32
|
+
"LABEL": {
|
|
33
|
+
"type": "STRING",
|
|
34
|
+
"defaultValue": "Start"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"opcode": "clearAppMenuActions",
|
|
40
|
+
"blockType": "COMMAND",
|
|
41
|
+
"text": "clear app menu actions",
|
|
42
|
+
"description": "Removes every application menu action, including the built-in ones, so a project can define its own set."
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"opcode": "setAppMenuActionEnabled",
|
|
46
|
+
"blockType": "COMMAND",
|
|
47
|
+
"text": "set app menu action [ACTION] enabled [ENABLED]",
|
|
48
|
+
"description": "Enables or disables one application menu action.",
|
|
49
|
+
"arguments": {
|
|
50
|
+
"ACTION": {
|
|
51
|
+
"type": "STRING",
|
|
52
|
+
"menu": "menuActions"
|
|
53
|
+
},
|
|
54
|
+
"ENABLED": {
|
|
55
|
+
"type": "BOOLEAN",
|
|
56
|
+
"defaultValue": true
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"opcode": "whenAppMenuActionSelected",
|
|
62
|
+
"blockType": "HAT",
|
|
63
|
+
"text": "when app menu action [ACTION] selected",
|
|
64
|
+
"description": "Runs when the operator selects the named application menu action.",
|
|
65
|
+
"arguments": {
|
|
66
|
+
"ACTION": {
|
|
67
|
+
"type": "STRING",
|
|
68
|
+
"menu": "menuActions"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
},
|
|
16
72
|
{
|
|
17
73
|
"opcode": "showDslFiles",
|
|
18
74
|
"blockType": "COMMAND",
|
package/src/extension.ts
CHANGED
|
@@ -17,11 +17,12 @@ import {createTitleDialog, type TitleDialog} from './title-dialog';
|
|
|
17
17
|
type ApplicationMenu = ReturnType<typeof createAppShellApplicationMenu>;
|
|
18
18
|
|
|
19
19
|
type BlockTypeName = 'COMMAND' | 'REPORTER' | 'BOOLEAN' | 'HAT';
|
|
20
|
-
type ArgumentTypeName = 'STRING';
|
|
20
|
+
type ArgumentTypeName = 'STRING' | 'NUMBER' | 'BOOLEAN';
|
|
21
21
|
|
|
22
22
|
interface DefinitionArgument {
|
|
23
23
|
type: ArgumentTypeName;
|
|
24
|
-
defaultValue
|
|
24
|
+
defaultValue?: string | number | boolean;
|
|
25
|
+
menu?: string;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
interface BlockDefinition {
|
|
@@ -36,6 +37,17 @@ const blockDefinitions = definitions.blocks as readonly BlockDefinition[];
|
|
|
36
37
|
|
|
37
38
|
const dslFileAccept = '.txt,.yaml,.yml,.json,.k4,.kamishibai';
|
|
38
39
|
|
|
40
|
+
/** Reserved action IDs the extension handles itself. A project may still remove or relabel them. */
|
|
41
|
+
const builtinActionIds = ['files', 'reload', 'about', 'close'] as const;
|
|
42
|
+
|
|
43
|
+
type BuiltinActionId = (typeof builtinActionIds)[number];
|
|
44
|
+
|
|
45
|
+
interface MenuActionEntry {
|
|
46
|
+
id: string;
|
|
47
|
+
labels: Record<SupportedLocale, string>;
|
|
48
|
+
enabled: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
39
51
|
function stageMount(): HTMLElement | undefined {
|
|
40
52
|
return Scratch.vm?.renderer?.canvas?.parentElement ?? globalThis.document?.body;
|
|
41
53
|
}
|
|
@@ -47,6 +59,12 @@ export class TurboWarpTitleMenuExtension implements TurboWarpExtension {
|
|
|
47
59
|
private store: DslStore | null = null;
|
|
48
60
|
private openedRecord: DslFileRecord | null = null;
|
|
49
61
|
private lastError = '';
|
|
62
|
+
private menuActions: MenuActionEntry[] = builtinActionIds.map((id) => ({
|
|
63
|
+
id,
|
|
64
|
+
labels: {en: menuLocales.en[id], ja: menuLocales.ja[id]},
|
|
65
|
+
enabled: true
|
|
66
|
+
}));
|
|
67
|
+
private menuVisible = false;
|
|
50
68
|
|
|
51
69
|
public getInfo(): Record<string, unknown> {
|
|
52
70
|
return {
|
|
@@ -54,7 +72,8 @@ export class TurboWarpTitleMenuExtension implements TurboWarpExtension {
|
|
|
54
72
|
name: Scratch.translate(definitions.extensionName),
|
|
55
73
|
docsURI: extensionConfig.docsURI,
|
|
56
74
|
blockIconURI: extensionConfig.blockIconURI,
|
|
57
|
-
blocks: blockDefinitions.map((block) => this.toScratchBlock(block))
|
|
75
|
+
blocks: blockDefinitions.map((block) => this.toScratchBlock(block)),
|
|
76
|
+
menus: definitions.menus
|
|
58
77
|
};
|
|
59
78
|
}
|
|
60
79
|
|
|
@@ -62,8 +81,55 @@ export class TurboWarpTitleMenuExtension implements TurboWarpExtension {
|
|
|
62
81
|
this.ensureTitleDialog().show(this.locale());
|
|
63
82
|
}
|
|
64
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Shows the application menu.
|
|
86
|
+
*
|
|
87
|
+
* An empty registry does nothing: the underlying primitive requires at least one action, and a
|
|
88
|
+
* project that cleared the actions to install its own should not be interrupted by an error
|
|
89
|
+
* between the two steps.
|
|
90
|
+
*/
|
|
65
91
|
public showMenu(): void {
|
|
92
|
+
if (this.menuActions.length === 0) return;
|
|
66
93
|
this.ensureApplicationMenu().show(this.locale());
|
|
94
|
+
this.menuVisible = true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Backs the dynamic `menuActions` dropdown, so it always lists what the project registered. */
|
|
98
|
+
public menuActionItems(): Array<{text: string; value: string}> {
|
|
99
|
+
if (this.menuActions.length === 0) return [{text: '\u2014', value: ''}];
|
|
100
|
+
const locale = this.locale();
|
|
101
|
+
return this.menuActions.map((action) => ({text: action.labels[locale], value: action.id}));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
public addAppMenuAction(args: {ACTION: unknown; LABEL: unknown}): void {
|
|
105
|
+
const id = Scratch.Cast.toString(args.ACTION).trim();
|
|
106
|
+
if (id.length === 0) return;
|
|
107
|
+
const label = Scratch.Cast.toString(args.LABEL);
|
|
108
|
+
const existing = this.menuActions.find((action) => action.id === id);
|
|
109
|
+
if (existing === undefined) {
|
|
110
|
+
this.menuActions.push({id, labels: {en: label, ja: label}, enabled: true});
|
|
111
|
+
} else {
|
|
112
|
+
existing.labels = {en: label, ja: label};
|
|
113
|
+
}
|
|
114
|
+
this.rebuildMenu();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
public clearAppMenuActions(): void {
|
|
118
|
+
this.menuActions = [];
|
|
119
|
+
this.rebuildMenu();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
public setAppMenuActionEnabled(args: {ACTION: unknown; ENABLED: unknown}): void {
|
|
123
|
+
const id = Scratch.Cast.toString(args.ACTION);
|
|
124
|
+
const action = this.menuActions.find((entry) => entry.id === id);
|
|
125
|
+
if (action === undefined) return;
|
|
126
|
+
action.enabled = Scratch.Cast.toBoolean(args.ENABLED);
|
|
127
|
+
this.applicationMenu?.setActionState(id, {enabled: action.enabled});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Started by the menu callback, so the handler only has to accept the match. */
|
|
131
|
+
public whenAppMenuActionSelected(): boolean {
|
|
132
|
+
return true;
|
|
67
133
|
}
|
|
68
134
|
|
|
69
135
|
public showDslFiles(): Promise<unknown> {
|
|
@@ -124,6 +190,39 @@ export class TurboWarpTitleMenuExtension implements TurboWarpExtension {
|
|
|
124
190
|
this.lastError = describeStoreError(this.locale(), error);
|
|
125
191
|
}
|
|
126
192
|
|
|
193
|
+
/**
|
|
194
|
+
* Rebuilds the menu after its action list changed.
|
|
195
|
+
*
|
|
196
|
+
* The app-shell primitive fixes its actions at construction, so a changed list means a new menu.
|
|
197
|
+
* A menu that was on screen is shown again, because a project that adds an action while the menu
|
|
198
|
+
* is open should not have it silently disappear. Clearing every action closes the menu instead,
|
|
199
|
+
* because the primitive refuses to build one with no actions.
|
|
200
|
+
*/
|
|
201
|
+
private rebuildMenu(): void {
|
|
202
|
+
this.applicationMenu?.dispose();
|
|
203
|
+
this.applicationMenu = null;
|
|
204
|
+
if (!this.menuVisible) return;
|
|
205
|
+
if (this.menuActions.length === 0) {
|
|
206
|
+
this.menuVisible = false;
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
this.showMenu();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private selectMenuAction(id: string): void {
|
|
213
|
+
Scratch.vm?.runtime?.startHats?.(`${extensionConfig.id}_whenAppMenuActionSelected`, {ACTION: id});
|
|
214
|
+
if (!builtinActionIds.includes(id as BuiltinActionId)) return;
|
|
215
|
+
if (id === 'files') void this.showDslFiles();
|
|
216
|
+
if (id === 'reload') this.reloadOpenedDsl();
|
|
217
|
+
if (id === 'about') this.showTitle();
|
|
218
|
+
if (id === 'close') this.hideMenu();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private hideMenu(): void {
|
|
222
|
+
this.applicationMenu?.hide();
|
|
223
|
+
this.menuVisible = false;
|
|
224
|
+
}
|
|
225
|
+
|
|
127
226
|
private announce(eventName: string, record: DslFileRecord): void {
|
|
128
227
|
dispatchDslSourceEvent(eventName, record);
|
|
129
228
|
Scratch.vm?.runtime?.startHats?.(`${extensionConfig.id}_whenDslSourceOpened`);
|
|
@@ -153,20 +252,16 @@ export class TurboWarpTitleMenuExtension implements TurboWarpExtension {
|
|
|
153
252
|
if (this.applicationMenu) return this.applicationMenu;
|
|
154
253
|
const mount = stageMount();
|
|
155
254
|
if (mount === undefined) throw new TypeError('a stage container is required to show the menu');
|
|
156
|
-
const labels = (key: keyof (typeof menuLocales)['en']) => ({
|
|
157
|
-
en: menuLocales.en[key],
|
|
158
|
-
ja: menuLocales.ja[key]
|
|
159
|
-
});
|
|
160
255
|
this.applicationMenu = createAppShellApplicationMenu({
|
|
161
256
|
document: globalThis.document,
|
|
162
257
|
mount,
|
|
163
258
|
initialLocale: this.locale(),
|
|
164
|
-
actions:
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
259
|
+
actions: this.menuActions.map((action) => ({
|
|
260
|
+
id: action.id,
|
|
261
|
+
labels: action.labels,
|
|
262
|
+
enabled: action.enabled,
|
|
263
|
+
onSelect: () => this.selectMenuAction(action.id)
|
|
264
|
+
}))
|
|
170
265
|
});
|
|
171
266
|
return this.applicationMenu;
|
|
172
267
|
}
|
|
@@ -241,7 +336,8 @@ export class TurboWarpTitleMenuExtension implements TurboWarpExtension {
|
|
|
241
336
|
name,
|
|
242
337
|
{
|
|
243
338
|
type: Scratch.ArgumentType[argument.type],
|
|
244
|
-
defaultValue: argument.defaultValue
|
|
339
|
+
...(argument.defaultValue === undefined ? {} : {defaultValue: argument.defaultValue}),
|
|
340
|
+
...(argument.menu === undefined ? {} : {menu: argument.menu})
|
|
245
341
|
}
|
|
246
342
|
])
|
|
247
343
|
)
|