Skip to content

feat: iPadのメニューバーとキーボードショートカットに編集画面の操作を載せる - #125

Merged
K0mork merged 5 commits into
mainfrom
feat/ipad-menu-commands
Oct 7, 2026
Merged

K0mork merged 5 commits into
mainfrom
feat/ipad-menu-commands

Conversation

@K0mork

@K0mork K0mork commented Oct 6, 2026 •

Copy link
Copy Markdown
Owner

変更内容

iPadのメニューバー(iPadOS 26以降)とキーボードショートカットから、編集画面の操作を使えるようにしました(#80)。iOS版だけの変更です。

メニュー 項目 キー
ファイル 新しい編み図… ⌘N
ファイル バックアップから復元… ⌘O
ファイル この編み図をバックアップ… ⌘S
ファイル 全データをバックアップ… ⌥⌘S
ファイル PNGで書き出す… ⇧⌘E
ファイル PDFで書き出す… ⌘P
編集 元に戻す ⌘Z
編集 やり直す ⇧⌘Z
ヘルプ 棒針編み図の使い方 ⇧⌘H
  • SwiftUIの.commands(新規ios/App/EditorCommands.swift)で項目を定義しました。選ばれたら、Swiftは操作の名前をknittingEditorNativeCommandイベントでWebViewへ送るだけです。操作の中身は画面のボタンと同じWeb側の処理(新規ios/Web/src/nativeCommands.ts)で、Swiftには複製していません。
  • 元に戻す・やり直すは、Webが{"type":"commandState","canUndo":…,"canRedo":…}で知らせる編集履歴に合わせて、画面のボタンと同じ条件で選べます。どの項目も、編集画面が準備できるまでと、使い方ページの表示中は選べません。
  • Info.plistにUIApplicationSupportsIndirectInputEvents(YES)を足しました。
  • ブリッジの仕様はios/docs/NATIVE_BRIDGE.mdの「メニューバーとキーボードショートカット」に書きました。
  • 共通コードには、packages/editor-core/ui/inputEnvironment.tsのnoteHardwareKeyboard()(とpackage.jsonのexports)だけを足しました。Web版の動きは変わりません。

決めたこと

  • キーの割り当て: iPadOSの慣習(⌘N・⌘O・⌘S・⌘Z・⇧⌘Z)と、Web版の既存のキー操作(⌘Z・⇧⌘Z)に合わせました。⌘Sは自動保存とは別の「この編み図を.knitで保存」、⌥⌘Sは全データ、⌘PはPDF(印刷向けの出力のため)、⇧⌘EはPNG(Export)にしました。
  • 使い方は⇧⌘H: 慣習の⌘?(⇧⌘/も)は、メニューには載るもののiPadのSimulatorで押しても項目が呼ばれませんでした(原因は確かめていません)。押して動くことを確かめた⇧⌘H(Help)にしました。
  • メニューから書き出すPNG・PDFの設定: 保存・出力パネルを初めて開いたときの既定値(PNGは既定の画素数、PDFは全体を1ページ・A4縦)を使います。分割や用紙の向きを変えたいときはパネルから書き出します。設定をパネルと共有するには共通UIの状態を持ち上げる必要があり、feat: 編み図で使っている色の一覧から色を選べるようにする #115・#120と同じ箇所を大きく変えるので見送りました。
  • 標準の「取り消す」「やり直す」を置き換えた: UIKitのundo:・redo:はWeb側の編集履歴に届かないためです。選べる状態のメニュー項目にキーが合うとUIKitが受け取り、ページのkeydownへは届かないので、Web側のキー操作と二重には実行されません(1回の⌘Zで1件だけ戻ることをテストで確かめています)。項目を選べないときは、キーはこれまでどおりWebViewへ届きます。
  • 入力欄で文字を打っている間の⌘Z・⇧⌘Zは、盤面ではなく入力中の文字を取り消します(document.execCommand)。標準の「取り消す」を置き換えたため、そのままでは盤面に戻せる編集が無いときにキーがページへ落ち、文字を戻せなくなります。そこでWebは入力欄にフォーカスがある間、commandStateをcanUndo・canRedoともtrueにして送り、入力欄での⌘Z・⇧⌘Zを常にメニュー経由でexecCommandへ回します。WKWebViewで効くことはUIテストで確かめました。入力欄の判定はWeb版のuseHistoryShortcutsと同じisEditableTarget(packages/editor-core/ui/hooks.tsから書き出して共有。Web版の判定は変えていません)です。
  • 操作を始めないとき: 出力・復元の処理中、アプリ内ダイアログや記号の一覧を開いている間。ネイティブの保存画面・共有シート・Document Pickerを出している間は、メニューの項目を選べない表示にします。元に戻す・やり直すはWeb版のキー操作と同じく、処理中やダイアログを開いている間も受け付けます。
  • キーボードの判定: ⌘Zなどがページに届かないと、記号の一覧の「Escapeで閉じます。」が出ません。ネイティブの操作を受けたらキーボードがあるとみなすことにしました。メニューバーを指で開いて選んだときも同じ扱いになりますが、案内が1文増えるだけです。
  • 他のPRとのコンフリクトを小さくする: 新しいSwiftのコードはEditorCommands.swiftに分け、WebViewContainer.swiftには状態のプロパティ1つとメッセージの分岐1つだけを足しました。Info.plistのキーは他のPRが触らない末尾に置き、UIテストは新しいファイル(KeyboardCommandUITests.swift)にしました。ios/docs/WEB_SYNC.mdはfeat: 編み図で使っている色の一覧から色を選べるようにする #115・feat: 最後のバックアップ日時を表示し、.knitの書き出しを勧める #120・#123と同じ行の近くを変えることになるため触っていません(App.tsxの行の「ネイティブブリッジ」に含まれる扱いです)。

テスト

  • ios/UITests/KnittingEditorUITests/KeyboardCommandUITests.swift(新規、iPadだけで実行): typeKey(_:modifierFlags:)でハードウェアキーボードのキーを送り、⌘N・⌘S・⌥⌘S・⇧⌘E・⌘P・⇧⌘H・⌘Oが、新しい編み図のダイアログ、.knit・PNG・PDFの保存画面(ファイル名まで)、使い方ページ、復元の選択画面へ届くこと。1回の⌘Z・⇧⌘Zで1件ずつだけ戻る・進むこと。⌘Zの後に記号の一覧が「Escapeで閉じます。」を出すこと。新しい編み図のダイアログで文字を打ち、盤面に戻せる編集が無いとき・あるときの両方で、⌘Zで文字が戻り、⇧⌘Zでやり直せ、盤面は戻らないこと。
    • メニューの項目はwebReady・commandStateがSwiftUIのメニューへ反映されるまで選べず、その間のキーは捨てられます。XCUITestからメニューの状態は読めないため、準備中の表示が消えるまで待ち、期待した画面・値の変化が起きるまでキーを送り直します(どの操作も画面を出している間は重ねて実行されません)。⌘Z・⇧⌘Zは画面のボタンが選べる状態になってから押し、記号数が変わらないときだけ押し直します。
  • ios/Tests/KnittingEditorAppTests/EditorCommandsTests.swift(新規): 操作の名前がWeb側と一致すること、イベントのスクリプト、選べる条件(保存画面などの表示中を含む)、commandStateの解読。
  • ios/Web/src/nativeCommands.test.ts(新規): 受け付ける名前、commandStateの送信、各操作の呼び出し、処理中・ダイアログ表示中・文字入力中の扱い、Web版と同じ入力欄の判定、入力中のメニュー状態とフォーカスの見張り、書き出しの既定値。
  • packages/editor-core/ui/inputEnvironment.test.tsx: noteHardwareKeyboard()のテストを追加。

実行した確認

レビューと最初のCI(ios (iPad (A16))でKeyboardCommandUITestsの2件が、⌘O・⌘Nが起動直後に届かず失敗)を受けて直した後の結果です。

  • npm run typecheck: 成功
  • npm test: 26ファイル160件成功
  • npm run build: 成功
  • npm run check:dist: 成功
  • npm run test:e2e: chromium-mobile・webkit-mobile・chromium-desktopで97件成功、2件は既存のスキップ
  • iOS Web tsc -p tsconfig.app.json --noEmit: 成功、vitest run --config vite.config.ts: 4ファイル17件成功
  • xcodegen generate --spec ios/project.ymlとアプリのビルド: 成功
  • iPad Pro 11-inch(M5)・iOS 27.0のSimulator(Xcode 27.0)で、KeyboardCommandUITests(4件)を2回、knittingEditorTests全体(34件、1件は既存のスキップ)、既存のtestGuideNavigationReturnsToUsableEditor・testTwoFingerGestureDoesNotDrawOnBoard・testUndoAndRedoRestoreBoardEdits・testPromptDialogIgnoresBackgroundTaps・testBackupExportSheetDismissesBackToEditor・testPngAndPdfExportsReachNativeFileActions・testDocumentDialogRemainsUsableAfterFocusingInput: すべて成功
  • Simulatorのテスト一式、更新復元、Release Archive、オフラインbundle検査はPRのCIに任せました。

判断していただきたい点

  • ⌘S・⌘Pの割り当て: ⌘Sを「この編み図を.knitでバックアップ」(自動保存とは別)、⌘PをPDF書き出しにしています。iPadOSの慣習では⌘Sは保存、⌘Pは印刷のため、このままでよいかご判断ください。
  • 入力欄の判定: Web版のisEditableTargetをそのまま共有したため、選択欄・スライダーなど文字入力でない入力欄にフォーカスがあるときも、⌘Zは盤面ではなく入力欄へ回ります(Web版のキー操作と同じ動き)。

実機で確かめること(#21)

  • execCommandによる入力欄の文字の取り消し・やり直し(Simulatorでは確認済み)。
  • トラックパッド・マウスでの描画・スクロール・ピンチと、その後にヒント・貼り付けの案内がマウス向けに切り替わること。
  • ⌘キーの長押しで出るショートカットの一覧とメニューバーの表示、保存画面などを出している間に項目が選べない表示になること。

できなかった確認

  • ⌘キーの長押しで出るショートカットの一覧とメニューバーそのものの表示: XCUITestのperform(withKeyModifiers:)はキーを押し続けないため一覧を出せず、メニューバーも上端からのスワイプでは出せませんでした。項目がUIKitのメインメニューに載り、キーで動くことまでは確かめています。
  • トラックパッド・マウスでの描画・スクロール・ピンチと、その後にヒント・貼り付けの案内がマウス向けに切り替わること: SimulatorとXCUITestから間接入力のポインタを送れないためです。
  • 以上は実機の確認([M4] タッチ、ピンチ、パン、マウス、トラックパッドを実機確認する #21)と合わせて行います。完了条件の一部が残るため、このPRでは [P1] iPadのメニューバー・キーボードショートカット・間接入力に対応する #80 を閉じず、残りをIssueにコメントします。

Refs #80

🤖 Generated with Claude Code

iPadのメニューバーとキーボードショートカットから、新規作成・復元・
.knit/PNG/PDFの書き出し・元に戻す/やり直す・使い方を使えるようにする。
ネイティブのコマンドはknittingEditorNativeCommandイベントでWeb側の
既存の操作を呼び、元に戻す/やり直すの可否はcommandStateで受け取る。
UIApplicationSupportsIndirectInputEventsをYESにする。

Refs #80

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 6, 2026 21:27
@K0mork K0mork added the enhancement New feature or request label Oct 6, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@K0mork K0mork left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

レビュー(4ce38fd、CI run 37534057002)

CIの結果

ios (iPad (A16)) が失敗し、ci-gate も失敗しました(deploy・smoke はスキップ)。ほかのジョブ(web、ios_web、release_archive、app_update の iPhone 17・iPad (A16)、ios (iPhone 17)、app_store_docs、CodeQL)は成功しました。

ios (iPad (A16)) の「Run iOS tests」の内訳:

  • EditorCommandsTests(6件)と NativeBridgeMessageTests はすべて成功。
  • 既存の KnittingEditorUITests は、iPad でもすべて成功(testTwoFingerGestureDoesNotDrawOnBoard、testUndoAndRedoRestoreBoardEdits を含む)。間接入力を有効にしたことによる、XCUITest のタップ・2本指の操作への影響は見当たりません。
  • KeyboardCommandUITests は3件のうち2件が、再試行を含めて2回とも失敗しました。
    • testFileAndHelpShortcutsOpenEditorActions: 成功。
    • testRestoreShortcutOpensBackupPicker: 97行目。⌘O の後に Document Picker(Cancel)が出ない。
    • testUndoAndRedoShortcutsStepBoardHistoryOnce: 115行目。⌘Nでダイアログが出ない。

原因の見立て(変更が原因): どちらの失敗も、「保存」ボタンが現れてから約1〜1.5秒でキーを送っています。この時点ではまだ webReady を受けておらず、受けた後もメニューが作り直されていないため、項目は .disabled です。そのためキーは WebView へ落ち、Web 側は ⌘O・⌘N を処理しないので捨てられます。成功した1件目は、アプリの初回起動に約20秒かかり、キーを送るのが遅かったので間に合いました。失敗時のスナップショット(45秒後)では、編集画面は準備済みでダイアログも出ていません。キーが失われたのは送った瞬間だと考えられます。手元で1度だけ起きた testUndoAndRedo… の失敗(⌘Z の後も2個のまま)も、メニューの有効・無効の切り替えが遅れることによる同じ種類の競合だと考えられます。

直し方の案:

  • launchEditor() で、editorLoadingOverlay が消えるのを待つ。
  • そのうえで、最初のショートカットは期待する画面が出るまで送り直す(または、メニュー項目が有効になってから送る)。
  • ⌘Z・⇧⌘Z は、commandState がメニューに反映されるまでの遅れを考えて同じように待つ。

製品の動きとしては、起動直後の短い間ショートカットが効かないだけなので、許容できる範囲だと考えます。

レビューで見つけた点

  1. 【中・要確認】入力欄の文字の取り消し
    • CommandGroup(replacing: .undoRedo) で、UIKit の標準の undo:・redo: のキー割り当てが無くなります。
    • 盤面に戻せる編集が無い(canUndo=false)ときに入力欄で ⌘Z を押すと、キーは WKWebView の keydown へ届きます。useHistoryShortcuts は入力欄では何もしないので、iOS の WebKit が keydown だけで文字を取り消すかどうか次第で、何も起きなくなるおそれがあります。
    • 盤面に戻せる編集があるときは document.execCommand('undo') に頼りますが、ユニットテストはこれをモックしているだけで、実際の WKWebView では確かめていません。
    • 案: 新しい編み図のダイアログで文字を打ってから ⌘Z で戻ることを、履歴が無いときとあるときの両方について UIテストで確かめる。あるいは、入力欄にフォーカスがある間は Web から canUndo/canRedo を true として送り、取り消しの経路をメニューの1本にまとめる。
  2. 【中・実機要確認】トラックパッドのピンチ
    • UIApplicationSupportsIndirectInputEvents=YES にすると、トラックパッドのピンチはタッチ2本として届かなくなります。
    • 盤面の拡大は BoardCanvas.tsx の、ctrl・meta 付きの wheel に頼っています。iPadOS の WebKit がピンチを ctrl 付きの wheel で送らない場合(Safari は gesture イベント)、盤面ではなくページ全体が拡大されます(viewport は maximum-scale=5)。
    • #21 で、この点をはっきり確かめる項目にしてください。
  3. 【低】Web 版との細かな違い
    • runNativeCommand は、処理中(busy)の元に戻す・やり直すを止めます。Web 版の useHistoryShortcuts は止めないので、コメントと NATIVE_BRIDGE.md の「Web 版と同じ」とは少し違います。
    • nativeCommands.ts の isEditable と hooks.ts の isEditableTarget で、color・range の input と SELECT の扱いが違います。どちらかに揃えるか、違う理由を書いてください。
  4. 【低】保存画面などを出している間の表示
    • 保存画面・共有シート・Document Picker を出している間も、メニュー項目は選べる表示のままですが、perform では何も起きません(黙って無視されます)。
  5. 【低・利用者の判断】キーの割り当て
    • ⌘S は押すたびに保存画面が出ます。自動保存のアプリなので、Mac の癖で押す人には煩わしいかもしれません。
    • ⌘P は慣習では「印刷」です。
    • ⇧⌘H はシステムと衝突しないと見ています(⌘H はシステムが使います)。
    • replacing: .newItem は、複数ウィンドウ(#28)に対応するときに「新規ウィンドウ」を消すことになるので、その際に見直しが必要です。
  6. テストの不安定さ: 上の CI の失敗と同じ原因です。メニューの有効・無効の反映を待たずにキーを送ると、取りこぼします。

確かめて問題が無かったこと

  • project.pbxproj は、xcodegen generate で生成し直したものと完全に一致しました。
  • 操作の名前は Swift と Web で一致しています。使い方ページへの遷移先は、既存のリンクと同じ /guide/ です。使い方ページの表示中と読み込み中は webContentReady で止まります。
  • NATIVE_BRIDGE.md の記述は実装と合っています。docs/dev-log/ のエントリは書式どおりで、手元の確認と CI に任せた確認を書き分けています。
  • 差分・PR本文・コミットメッセージに機密情報はありません。Refs #80 とラベル enhancement があります。

満たしていない #80 の完了条件

  • ⌘キーの長押しで一覧が出ることの確認。
  • トラックパッド・マウスでの描画・スクロール・ピンチの確認。

どちらも実機(#21)で確かめる予定なので、Closes ではなく Refs にしているのは適切です。なお #80 には needs-device ラベルがありません。

… tests

- Send canUndo/canRedo as true while a text field has focus so Cmd+Z and
  Shift+Cmd+Z always go through the menu and undo typing via execCommand.
- Accept undo/redo while busy like the web shortcuts, and share
  isEditableTarget with useHistoryShortcuts.
- Disable menu items while the save alert, share sheet, or document picker
  is presented.
- Wait for the editor to be ready and resend shortcuts until the expected
  screen appears in KeyboardCommandUITests; add a text-field undo test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@K0mork

K0mork commented Oct 6, 2026

Copy link
Copy Markdown
Owner Author

レビューの指摘と最初のCIの失敗を fd5d1ef で直しました。

直したこと

  • 入力欄の文字の取り消し: 入力欄にフォーカスがある間、WebはcommandStateをcanUndo・canRedoともtrueで送ります。入力欄での⌘Z・⇧⌘Zは常にメニューを通り、execCommand('undo')・('redo')へ回ります。WKWebViewで効くことはUIテストで確かめました。盤面に戻せる編集が無いとき・あるときの両方で試しています。
  • Web版との違い: 元に戻す・やり直すは、Web版のキー操作と同じく処理中も受け付けるようにしました。入力欄の判定はhooks.tsのisEditableTargetを共有しています。Web版の動きは変わりません。
  • 保存画面・共有シート・Document Pickerを出している間は、メニューの項目を選べない表示にしました。
  • UIテスト(最初のCIでtestRestoreShortcutOpensBackupPickerとtestUndoAndRedoShortcutsStepBoardHistoryOnceが失敗): 起動直後でメニューがまだ選べない間にキーを送ると、キーが捨てられていました。準備中の表示が消えるまで待ち、期待した画面や値の変化が起きるまでキーを送り直すようにしました。⌘Z・⇧⌘Zはボタンが選べる状態になってから押します。記号数が変わらないときだけ押し直し、二重に実行されたときは失敗させます。

CI(run 37538823264): web、ios_web、ios (iPad (A16))、ios (iPhone 17)、app_update (iPad (A16))、app_update (iPhone 17)、release_archive、app_store_docs、CodeQL、ci-gate がすべて成功しました(deploy と smoke はPRのためスキップ)。iPadではKeyboardCommandUITestsの4件が1回目で通っています。

残っていること

K0mork and others added 3 commits October 7, 2026 12:17
Keep both sides in WebViewContainer.swift: the launch colors and loading
style from #118, and the menu command state and native UI flag from #125.
The share sheet completion now clears the native UI flag and reports the
finished export for the review request (#121). Regenerate project.pbxproj
with XcodeGen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The app now has a Japanese localization (#119), so the Document Picker's
close button has the label キャンセル instead of the Cancel identifier on a
Japanese simulator. Match either one in the ⌘O shortcut test, and record
the merge from main in the development log.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With a localization in the app (#119), the Document Picker's close button
loses the Cancel identifier and takes a label in the device language, so
the English CI simulator failed too. Also accept the Cancel label and the
picker's Browse View (Picker) identifier.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@K0mork
K0mork merged commit fcd69d0 into main Oct 7, 2026
15 checks passed
@K0mork
K0mork deleted the feat/ipad-menu-commands branch October 7, 2026 04:11
@K0mork

K0mork commented Oct 7, 2026

Copy link
Copy Markdown
Owner Author

マージ後の確認です。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants