🙆‍♀️

MuseScore Studioにコマンドパレットを実装してみた

に公開

概要

この記事は、東京大学工学部 電子情報工学科・電気電子工学科の学生実験「大規模ソフトウェアを手探る」で取り組んだ、MuseScore Studio へのコマンドパレット機能追加の実装記録です。

MuseScore は C++・Qtによって構築された大規模な OSS であり、新機能を追加する際には構造を完全に理解する必要があります。

特に、今回のコマンドパレット実装では以下の 3 レイヤが重要になります。

  • C++(アプリケーションロジック & アクション登録)
  • C++(モデル実装)
  • QML(UI & キー操作)
    この記事では、それぞれのレイヤで必要な処理をどのファイルに、どのように追加したかを解説します。

実装の全体像

MuseScore にコマンドパレットを導入するには、次の 3 つの処理が必要です。

  1. C++ 側でアクションを登録して起動できるようにする
  2. CommandPaletteModel を実装してアクション一覧・検索・実行を行えるようにする
  3. QML で UI を構築し、ユーザー入力とモデルを連携させる

この構造に従うことで、「検索 → 選択 → 実行」 という動作がスムーズに実現できます。

1. アクションの追加(C++ / アプリケーション層)

なぜアクション登録が必要なのか?

MuseScore の内部では、ユーザーが行うすべての操作は UiAction という共通の仕組みで管理されています。
これは、アプリケーション全体で統一的な操作体系を実現するための重要な設計思想です。

たとえば、以下の操作はすべて「アクション ID」によって識別されます。

  • メニューの操作
  • 各種ショートカット
  • UI ボタンのクリック
  • 内部ロジックによるアクション呼び出し
  • テストや自動処理によるアクション実行

内部的には、file-saveundo のような 文字列 ID(アクションコード) が割り当てられており、
ユーザーの操作は最終的に「アクションの実行」という形にまとめられています。

コマンドパレットは「UI を開く」という操作ですが、
これも ショートカットから実行できるようにするためにはアクション化が必須です。

アクションとして登録することで:

  • ショートカット割り当てが可能になる
  • メニューやプラグインから呼び出すことが可能になる
  • アプリケーション内部のディスパッチャで一元管理できる

という利点があります。


1-1. アクションを登録する

対象ファイル: /MuseScore/src/appshell/internal/applicationactioncontroller.cpp

アクションの登録は ApplicationActionController::init() の中で行われます。
ここに show-command-palette というアクションを追加します。

dispatcher()->reg(
    this,
    "show-command-palette",
    this,
    &ApplicationActionController::openCommandPalette
);

ここでは
"show-command-palette" というアクション ID を新規登録しこのアクションが実行されたとき、openCommandPalette() を呼ぶ

つまり:
「show-command-palette」が実行されたらコマンドパレット画面を開く
という関連づけを行っています。

実際の処理(登録されたアクションが実行されると呼ばれる関数)

void ApplicationActionController::openCommandPalette()
{
    interactive()->open("muse://commandpalette");
}

ここで使われている interactive()->open() は URI ベースで QML 画面を開く ための手段です。

MuseScore には

muse://home

muse://publish

muse://preferences

などの URI が用意されており、
それぞれが特定の QML 画面に紐づいています。
今回の実装では、新たにmuse://commandpalette
を QML 画面(CommandPalette.qml)に関連づけました。

1-2. UI 側にアクションを登録する

アクションを内部に追加しただけでは不十分で、
ユーザーが「ショートカット設定」から確認できるよう UI 側にも登録する必要があります。

対象ファイル: /MuseScore/src/appshell/internal/applicationuiactions.cpp

UI で認識されるアクションとして次のように追記します:

UiAction(
    "show-command-palette",
    mu::context::UiCtxAny,
    mu::context::CTX_ANY,
    TranslatableString("action", "Command palette…"),
    TranslatableString("action", "Show command palette")
),

この登録によって

  • 「環境設定 → ショートカット一覧」 に「Show command palette」が自動で表示されるようになります。
  • 必要に応じてショートカットをユーザーが再割り当てできます。

1-3. デフォルトショートカットの設定

コマンドパレットの呼び出しには、VSCode と同様に

Ctrl + Shift + P

を採用します。

対象ファイル: /MuseScore/src/app/configs/data/shortcuts.xml

ここにショートカットを追加します。

<SC>
    <key>show-command-palette</key>
    <seq>Ctrl+Shift+P</seq>
</SC>

これにより、ユーザーは Ctrl+Shift+P で即座にコマンドパレットを呼び出せる ようになります。
ここまでで一度機能を確認すると環境設定にあるショートカット一覧に追加されていました。

2. コマンドパレットのモデル実装(C++)

コマンドパレットの中心となるのが CommandPaletteModel です。
このモデルは、コマンドパレットの中心にあたる部分であり、以下の機能を担当します。

  • 全アクションのロード
  • 検索文字列に応じたフィルタリング
  • コマンドの実行処理
  • 履歴(最近使ったコマンド)管理

2-1. アクション一覧の取得

対象ファイル: /MuseScore/src/appshell/view/commandpalettemodel.cpp

MuseScore ではアプリ起動時に全 UiAction が登録されており、
以下で一括取得できます。

std::vector<UiAction> actions = actionsRegister()->actionList();

これをコマンドパレット用に整形します。

for (const UiAction& action : actions) {
CommandItem item;
item.code = action.code;
item.title = action.title.translated().toQString();
item.shortcut = getShortcutForAction(action.code);
m_allCommands.append(item);
}
UiAction は内部情報も含む複雑な構造体なので、
UI に表示するには以下だけに軽量化して見やすくする必要があります。

  • タイトル

  • 説明

  • ショートカット

2-2. 検索処理

対象ファイル: /MuseScore/src/appshell/view/commandpalettemodel.cpp

ユーザーが検索欄に文字を入れると setSearchText() が呼ばれます。

void CommandPaletteModel::setSearchText(const QString& text)
{
    m_searchText = text;
    filterCommands();
}

フィルタ処理は以下のようになっています。

if (item.title.contains(lowerSearch)
 || item.description.contains(lowerSearch)
 || QString::fromStdString(item.code).contains(lowerSearch))
{
    m_filteredCommands.append(item);
}

検索の流れとしては

  1. m_searchTextに検索文字列を設定する
  2. filterComandsを呼び出す
  3. タイトル、説明文、コードと部分一致するものを候補として表示する
  4. フィルタ結果をリストとして保存しUI側に反映させるようにする
    という流れです。これにより検索機能が実装できます。

2-3. コマンドの実行

コマンドの実行は非常にシンプルです。

dispatcher()->dispatch(actionCode);

disptcherの役割

  • アプリ全体に登録されたアクション一覧を管理する

  • 「このアクション ID を実行して」と依頼されると、正しい関数へ処理をつなぐ

  • QML / C++ / キーボードショートカットなど、あらゆる入力経路から統一的にアクションを呼び出す

これとactionCodeにより実際にコマンドをdispatch(actionCode)で実行しています。

3. QML による UI 作成

3-1. CommandPaletteModel を QML で使えるようにする

対象ファイル:src/appshell/internal/applicationactioncontroller.cpp

QML(画面側)で C++ のクラスを使うためには、
まず C++ クラスを QML に登録する必要があります。

そのために使うのが qmlRegisterType<> です。

qmlRegisterType<CommandPaletteModel>(
    "MuseScore.AppShell", 1, 0, "CommandPaletteModel"
);
  • CommandPaletteModel : QML から使いたい C++ クラス
  • "MuseScore.AppShell" QML で使う名前空間(モジュール名)
  • 1, 0 バージョン番号(1.0)
  • "CommandPaletteModel" QML での型名

この登録によって、
QML は "MuseScore.AppShell" というモジュールに
CommandPaletteModel という型があることを認識できるようになります。

  1. QML で実際に使えるようになる
    登録後は QML ファイルで次のように書くだけで、
    C++ の CommandPaletteModel をインスタンス化できます。
CommandPaletteModel {
    id: commandPaletteModel
}

これによりcommandPaletteModel.load() などで内部ロジックを実行できるようになります

3-2. UI 構造

対象ファイル: /MuseScore/src/appshell/qml/CommandPalette.qml

UI は以下の2パートから成ります。

  • 検索欄(SearchField)

  • 全コマンド一覧(ListView)

主要部分は以下のようになります。

SearchField {
    onSearchTextChanged: commandPaletteModel.searchText = searchText
}

ListView {
    model: commandPaletteModel
    onActivated: commandPaletteModel.executeSelectedCommand()
}

3-3. 操作フロー

1. コマンドパレットを開く

// アクション登録
dispatcher()->reg(this, "show-command-palette",
                  this, &ApplicationActionController::openCommandPalette);

// 実際の処理(コマンドパレットを開く)
void ApplicationActionController::openCommandPalette()
{
    interactive()->open("muse://commandpalette");
}

これによりUIでもコマンドパレットが自動で開かれるようになります。

2. 全コマンドを読み込む

CommandPalette.qml

Component.onCompleted: {
    commandPaletteModel.load()
}

commandpalettemodel.cpp

void CommandPaletteModel::load()
{
    std::vector<UiAction> actions = actionsRegister()->actionList();
    // → CommandItem に詰め替えて m_allCommands へ格納
}

何が起きている?

QML が表示された瞬間(Component.onCompleted)
→ C++ 側の load() が呼ばれる
→ actionsRegister()->actionList() ですべてのアクションが取得される
→ コマンドパレットで使える一覧が完成
という流れです。

3. 検索欄に文字を入力 → モデル側でフィルタリング

CommandPalette.qml

onSearchTextChanged: {
    commandPaletteModel.searchText = searchText
}

commandpalettemodel.cpp

void CommandPaletteModel::setSearchText(const QString& text)
{
    m_searchText = text;
    filterCommands();
}

void CommandPaletteModel::filterCommands()
{
    // title / description / code に対して部分一致検索
    if (item.title.contains(lowerSearch)
        || item.description.contains(lowerSearch)
        || QString::fromStdString(item.code).contains(lowerSearch)) {
        m_filteredCommands.append(item);
    }
}

QML の検索欄に文字を入力するたびに

  1. searchTextChanged が発火
  2. C++ の setSearchText() が呼ばれる
  3. filterCommands() でコマンド一覧を絞り込む
    4.1文字入力するたびに自動で更新
    という流れです。

4. 上下キーで候補を選択

CommandPalette.qml

Keys.onDownPressed: commandsList.incrementCurrentIndex()
Keys.onUpPressed: commandsList.decrementCurrentIndex()

onCurrentIndexChanged: {
    commandPaletteModel.selectedIndex = currentIndex
}

QML の ListView が上下キーを受け取りcurrentIndex(選択行)が更新されます。
その結果、C++ 側が持つselectedIndex が常にUIと同期するようになってます

5. Enter キーでアクション実行 → ダイアログが閉じる

CommandPalette.qml

Keys.onReturnPressed: {
    commandPaletteModel.executeSelectedCommand()
}

commandpalettemodel.cpp

void CommandPaletteModel::executeSelectedCommand()
{
    dispatcher()->dispatch(selectedItem.code);
}

applicationactioncontroller.cpp(登録部分)

dispatcher()->reg(this, "show-command-palette",
                  this, &ApplicationActionController::openCommandPalette);

流れとしては

  1. Enter が押される
  2. executeSelectedCommand() が呼ばれる
  3. dispatch(actionCode) でアクション ID を実行
  4. 対応する C++ 関数が自動で呼ばれる
  5. 処理が完了したらcloseRequested シグナル → QML 側がダイアログを閉じる
    となっています。

4. 履歴機能の追加

頻繁に使うコマンドをすぐ再実行できるよう、履歴機能を追加しました。

4-1. 履歴の保存(QSettings)

コマンドパレットを何度も使うと、
よく使うアクションを上に出したい
直前に使ったコマンドをすぐ実行したい
といったニーズが出てきます。

そのため、今回は「最近使ったコマンド」を記録して
次回起動時にも復元されるようにしました。


4-1. 履歴の保存処理(C++ / QSettings)

📂 対象ファイル

/MuseScore/src/appshell/view/commandpalettemodel.cpp

履歴保存には QtのクラスQSettingsを使います。

void CommandPaletteModel::saveRecentCommands()
{
    QSettings settings;
    settings.beginGroup("CommandPalette");

    QStringList recentCodes;
    for (const CommandItem& item : m_recentCommands) {
        recentCodes.append(QString::fromStdString(item.code));
    }

    settings.setValue("RecentCommands", recentCodes);
    settings.endGroup();
    settings.sync();
}
1. QSettings の作成

QSettings settings;

→ OSに依存した「設定ファイル」が自動で作られる
(macOS: plist, Windows: Registry, Linux: INI など)

2. “CommandPalette” という名前のセクションを開く

settings.beginGroup("CommandPalette");

→ CommandPalette/RecentCommands のような形式で保存される

3. 今持っている履歴リスト(m_recentCommands)を文字列のリストに変換

recentCodes.append(QString::fromStdString(item.code));

これにより使用したコマンドの ID(例:"file-save")が保存されます。

4. 書き込み
```cpp
settings.setValue("RecentCommands", recentCodes);
  1. 実際にファイルへ即時書き込み
settings.sync();

[
"file-save",
"show-mixer",
"note-input",
"show-palette"
]
保存するのはアクションコードだけでタイトルやショートカットは保存しません。
→ 起動後に最新の UiAction 情報と突き合わせて復元 するからです。

4-2. 履歴の読み込み(アプリ起動時)

同じファイル内で、読み込み処理も定義します。

void CommandPaletteModel::loadRecentCommands()
{
    QSettings settings;
    settings.beginGroup("CommandPalette");

    QStringList recentCodes = settings.value("RecentCommands").toStringList();
    settings.endGroup();

    m_recentCommands.clear();

    for (const QString& code : recentCodes) {
        CommandItem item;
        item.code = code.toStdString();
        item.title = code;        
        item.isEnabled = true;
        m_recentCommands.append(item);
    }
}

アクションコードだけ保存しているため、UI タイトル・ショートカットは最新の定義に基づいて再構築できるようになっています。

4-3. QML での履歴表示

対象ファイル:/MuseScore/src/appshell/qml/CommandPalette.qml

// 履歴表示
ListView {
    id: recentList
    model: commandPaletteModel.recentCommands
    visible: searchField.searchText === ""
    onClicked: commandPaletteModel.executeCommandByCode(modelData.code)
}
  1. モデル(C++)の recentCommands をそのまま表示
    C++ 側は QVariantList を返すため QML からそのまま読める

  2. 検索文字列が空のときのみ表示

  3. クリックすると実行
    という流れになっています。

4-4. 履歴が UI にどう反映されるかの流れ

アプリ起動

loadRecentCommands()

前回保存した RecentCommands を復元

ユーザーがコマンドを実行

executeCommand()

履歴の先頭に追加

saveRecentCommands() で永続化

次回起動時に UI の "Recent Commands" に反映

この一連の流れにより、履歴機能が実現しています。

動作確認

以下がコマンドパレットになっています。全ての動作について問題なく動くことが確認できました。

感想

今回のコマンドパレット実装では、MuseScore の内部構造の理解とデバッグが大変した。
バックエンド側はC++で書かれていますがそれぞれショートカットだけでもいくつものファイルに分かれているので何用のファイルなのかをまず分析し、コマンドパレットをどこに配置すれば良いのかに悩みました。ただ新しいファイルを作るだけではOSSの管理者の観点からしても使いにくいものであるので大変でしたが新たな視点を得ることができました。
一方で、他の記事でも解説していますが、GUI アプリの複雑さゆえに呼び出し元の追跡(C++↔QML)、
URI を介した画面遷移、アクション登録の階層構造などデバッグが難しい点もありました。ただこれを通じてデバッグの最低限の素養を得ることができたと思っています。

Discussion