@kubohiroya/turbowarp-title-menu 0.1.0 → 0.2.0

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 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.0
36
+ pnpm add --save-exact @kubohiroya/turbowarp-title-menu@0.2.0
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,8 @@ IndexedDBに保管しているDSLファイルの件数を返します。
152
201
 
153
202
  | 場面 | 動作 |
154
203
  |---|---|
204
+ | 組み込みのメニュー項目 | `files`/`reload`/`about`/`close` は最初から登録されており、それぞれの動作を保ちます。同時にhatも発火します。`clear app menu actions` で外せます。 |
205
+ | メニューの変更 | 項目の追加や消去でメニューを作り直します。表示中だった場合は消えずに再表示します。 |
155
206
  | ファイルの追加 | 「ファイルを追加」はファイル選択ダイアログを開き、選ばれた内容を保管します。開きはしません。使うときは「開く」を押します。 |
156
207
  | 名前の変更 | 名前は一意です。他のファイルと同じ名前にしようとすると失敗し、どちらのファイルも変わりません。 |
157
208
  | 削除 | 削除には確認のためもう一度クリックが必要です。開いているファイルを削除すると、開いている内容も解除されます。 |
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, so a host project can define its own actions instead of a fixed set.
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.0
51
+ pnpm add --save-exact @kubohiroya/turbowarp-title-menu@0.2.0
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.0/dist/turbowarp-title-menu.js
63
+ https://cdn.jsdelivr.net/npm/@kubohiroya/turbowarp-title-menu@0.2.0/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,8 @@ 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. |
181
238
  | 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
239
  | Renaming | Names are unique. Renaming a file to a name another file already uses fails and leaves both files unchanged. |
183
240
  | 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 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": "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." }];
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,7 +1300,8 @@
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() {
@@ -1299,6 +1309,40 @@
1299
1309
  }
1300
1310
  showMenu() {
1301
1311
  this.ensureApplicationMenu().show(this.locale());
1312
+ this.menuVisible = true;
1313
+ }
1314
+ /** Backs the dynamic `menuActions` dropdown, so it always lists what the project registered. */
1315
+ menuActionItems() {
1316
+ if (this.menuActions.length === 0) return [{ text: "—", value: "" }];
1317
+ const locale = this.locale();
1318
+ return this.menuActions.map((action) => ({ text: action.labels[locale], value: action.id }));
1319
+ }
1320
+ addAppMenuAction(args) {
1321
+ const id = Scratch.Cast.toString(args.ACTION).trim();
1322
+ if (id.length === 0) return;
1323
+ const label = Scratch.Cast.toString(args.LABEL);
1324
+ const existing = this.menuActions.find((action) => action.id === id);
1325
+ if (existing === void 0) {
1326
+ this.menuActions.push({ id, labels: { en: label, ja: label }, enabled: true });
1327
+ } else {
1328
+ existing.labels = { en: label, ja: label };
1329
+ }
1330
+ this.rebuildMenu();
1331
+ }
1332
+ clearAppMenuActions() {
1333
+ this.menuActions = [];
1334
+ this.rebuildMenu();
1335
+ }
1336
+ setAppMenuActionEnabled(args) {
1337
+ const id = Scratch.Cast.toString(args.ACTION);
1338
+ const action = this.menuActions.find((entry) => entry.id === id);
1339
+ if (action === void 0) return;
1340
+ action.enabled = Scratch.Cast.toBoolean(args.ENABLED);
1341
+ this.applicationMenu?.setActionState(id, { enabled: action.enabled });
1342
+ }
1343
+ /** Started by the menu callback, so the handler only has to accept the match. */
1344
+ whenAppMenuActionSelected() {
1345
+ return true;
1302
1346
  }
1303
1347
  showDslFiles() {
1304
1348
  return this.ensureFilesDialog().show(this.locale());
@@ -1347,6 +1391,30 @@
1347
1391
  recordFailure(error) {
1348
1392
  this.lastError = describeStoreError(this.locale(), error);
1349
1393
  }
1394
+ /**
1395
+ * Rebuilds the menu after its action list changed.
1396
+ *
1397
+ * The app-shell primitive fixes its actions at construction, so a changed list means a new menu.
1398
+ * A menu that was on screen is shown again, because a project that adds an action while the menu
1399
+ * is open should not have it silently disappear.
1400
+ */
1401
+ rebuildMenu() {
1402
+ this.applicationMenu?.dispose();
1403
+ this.applicationMenu = null;
1404
+ if (this.menuVisible) this.showMenu();
1405
+ }
1406
+ selectMenuAction(id) {
1407
+ Scratch.vm?.runtime?.startHats?.(`${extensionConfig.id}_whenAppMenuActionSelected`, { ACTION: id });
1408
+ if (!builtinActionIds.includes(id)) return;
1409
+ if (id === "files") void this.showDslFiles();
1410
+ if (id === "reload") this.reloadOpenedDsl();
1411
+ if (id === "about") this.showTitle();
1412
+ if (id === "close") this.hideMenu();
1413
+ }
1414
+ hideMenu() {
1415
+ this.applicationMenu?.hide();
1416
+ this.menuVisible = false;
1417
+ }
1350
1418
  announce(eventName, record) {
1351
1419
  dispatchDslSourceEvent(eventName, record);
1352
1420
  Scratch.vm?.runtime?.startHats?.(`${extensionConfig.id}_whenDslSourceOpened`);
@@ -1374,20 +1442,16 @@
1374
1442
  if (this.applicationMenu) return this.applicationMenu;
1375
1443
  const mount = stageMount();
1376
1444
  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
1445
  this.applicationMenu = createAppShellApplicationMenu({
1382
1446
  document: globalThis.document,
1383
1447
  mount,
1384
1448
  initialLocale: this.locale(),
1385
- actions: [
1386
- { id: "files", labels: labels("files"), icon: { text: "📂" }, onSelect: () => this.showDslFiles() },
1387
- { id: "reload", labels: labels("reload"), icon: { text: "↻" }, onSelect: () => this.reloadOpenedDsl() },
1388
- { id: "about", labels: labels("about"), icon: { text: "i" }, onSelect: () => this.showTitle() },
1389
- { id: "close", labels: labels("close"), icon: { text: "x" }, onSelect: () => this.applicationMenu?.hide() }
1390
- ]
1449
+ actions: this.menuActions.map((action) => ({
1450
+ id: action.id,
1451
+ labels: action.labels,
1452
+ enabled: action.enabled,
1453
+ onSelect: () => this.selectMenuAction(action.id)
1454
+ }))
1391
1455
  });
1392
1456
  return this.applicationMenu;
1393
1457
  }
@@ -1456,7 +1520,8 @@
1456
1520
  name,
1457
1521
  {
1458
1522
  type: Scratch.ArgumentType[argument.type],
1459
- defaultValue: argument.defaultValue
1523
+ ...argument.defaultValue === void 0 ? {} : { defaultValue: argument.defaultValue },
1524
+ ...argument.menu === void 0 ? {} : { menu: argument.menu }
1460
1525
  }
1461
1526
  ])
1462
1527
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubohiroya/turbowarp-title-menu",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Reusable title, application menu, and DSL source storage controls for TurboWarp.",
5
5
  "author": "Hiroya Kubo <hiroya@cuc.ac.jp>",
6
6
  "license": "MPL-2.0",
@@ -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: string;
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
 
@@ -64,6 +83,45 @@ export class TurboWarpTitleMenuExtension implements TurboWarpExtension {
64
83
 
65
84
  public showMenu(): void {
66
85
  this.ensureApplicationMenu().show(this.locale());
86
+ this.menuVisible = true;
87
+ }
88
+
89
+ /** Backs the dynamic `menuActions` dropdown, so it always lists what the project registered. */
90
+ public menuActionItems(): Array<{text: string; value: string}> {
91
+ if (this.menuActions.length === 0) return [{text: '\u2014', value: ''}];
92
+ const locale = this.locale();
93
+ return this.menuActions.map((action) => ({text: action.labels[locale], value: action.id}));
94
+ }
95
+
96
+ public addAppMenuAction(args: {ACTION: unknown; LABEL: unknown}): void {
97
+ const id = Scratch.Cast.toString(args.ACTION).trim();
98
+ if (id.length === 0) return;
99
+ const label = Scratch.Cast.toString(args.LABEL);
100
+ const existing = this.menuActions.find((action) => action.id === id);
101
+ if (existing === undefined) {
102
+ this.menuActions.push({id, labels: {en: label, ja: label}, enabled: true});
103
+ } else {
104
+ existing.labels = {en: label, ja: label};
105
+ }
106
+ this.rebuildMenu();
107
+ }
108
+
109
+ public clearAppMenuActions(): void {
110
+ this.menuActions = [];
111
+ this.rebuildMenu();
112
+ }
113
+
114
+ public setAppMenuActionEnabled(args: {ACTION: unknown; ENABLED: unknown}): void {
115
+ const id = Scratch.Cast.toString(args.ACTION);
116
+ const action = this.menuActions.find((entry) => entry.id === id);
117
+ if (action === undefined) return;
118
+ action.enabled = Scratch.Cast.toBoolean(args.ENABLED);
119
+ this.applicationMenu?.setActionState(id, {enabled: action.enabled});
120
+ }
121
+
122
+ /** Started by the menu callback, so the handler only has to accept the match. */
123
+ public whenAppMenuActionSelected(): boolean {
124
+ return true;
67
125
  }
68
126
 
69
127
  public showDslFiles(): Promise<unknown> {
@@ -124,6 +182,33 @@ export class TurboWarpTitleMenuExtension implements TurboWarpExtension {
124
182
  this.lastError = describeStoreError(this.locale(), error);
125
183
  }
126
184
 
185
+ /**
186
+ * Rebuilds the menu after its action list changed.
187
+ *
188
+ * The app-shell primitive fixes its actions at construction, so a changed list means a new menu.
189
+ * A menu that was on screen is shown again, because a project that adds an action while the menu
190
+ * is open should not have it silently disappear.
191
+ */
192
+ private rebuildMenu(): void {
193
+ this.applicationMenu?.dispose();
194
+ this.applicationMenu = null;
195
+ if (this.menuVisible) this.showMenu();
196
+ }
197
+
198
+ private selectMenuAction(id: string): void {
199
+ Scratch.vm?.runtime?.startHats?.(`${extensionConfig.id}_whenAppMenuActionSelected`, {ACTION: id});
200
+ if (!builtinActionIds.includes(id as BuiltinActionId)) return;
201
+ if (id === 'files') void this.showDslFiles();
202
+ if (id === 'reload') this.reloadOpenedDsl();
203
+ if (id === 'about') this.showTitle();
204
+ if (id === 'close') this.hideMenu();
205
+ }
206
+
207
+ private hideMenu(): void {
208
+ this.applicationMenu?.hide();
209
+ this.menuVisible = false;
210
+ }
211
+
127
212
  private announce(eventName: string, record: DslFileRecord): void {
128
213
  dispatchDslSourceEvent(eventName, record);
129
214
  Scratch.vm?.runtime?.startHats?.(`${extensionConfig.id}_whenDslSourceOpened`);
@@ -153,20 +238,16 @@ export class TurboWarpTitleMenuExtension implements TurboWarpExtension {
153
238
  if (this.applicationMenu) return this.applicationMenu;
154
239
  const mount = stageMount();
155
240
  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
241
  this.applicationMenu = createAppShellApplicationMenu({
161
242
  document: globalThis.document,
162
243
  mount,
163
244
  initialLocale: this.locale(),
164
- actions: [
165
- {id: 'files', labels: labels('files'), icon: {text: '\u{1F4C2}'}, onSelect: () => this.showDslFiles()},
166
- {id: 'reload', labels: labels('reload'), icon: {text: '↻'}, onSelect: () => this.reloadOpenedDsl()},
167
- {id: 'about', labels: labels('about'), icon: {text: 'i'}, onSelect: () => this.showTitle()},
168
- {id: 'close', labels: labels('close'), icon: {text: 'x'}, onSelect: () => this.applicationMenu?.hide()}
169
- ]
245
+ actions: this.menuActions.map((action) => ({
246
+ id: action.id,
247
+ labels: action.labels,
248
+ enabled: action.enabled,
249
+ onSelect: () => this.selectMenuAction(action.id)
250
+ }))
170
251
  });
171
252
  return this.applicationMenu;
172
253
  }
@@ -241,7 +322,8 @@ export class TurboWarpTitleMenuExtension implements TurboWarpExtension {
241
322
  name,
242
323
  {
243
324
  type: Scratch.ArgumentType[argument.type],
244
- defaultValue: argument.defaultValue
325
+ ...(argument.defaultValue === undefined ? {} : {defaultValue: argument.defaultValue}),
326
+ ...(argument.menu === undefined ? {} : {menu: argument.menu})
245
327
  }
246
328
  ])
247
329
  )