今回の実装で、アンドゥとリドゥが追加されます。
undoManagerの作法を理解していきたいね~
JUCEチュートリアル、Using an UndoManager with a ValueTreeで前回作成した、ツリー構造のデータ表示デモアプリに、アンドゥとリドゥの機能を追加していきます。
公式チュートリアルはこちらです。
公式チュートリアルの「Adding Undo/Redo buttons」と「Passing the UndoManager instance as an argument」の項目を進めていきます。
こんな人の役に立つかも
・JUCEでアンドゥ、リドゥの機能を追加したい人
・JUCEチュートリアル「Using an UndoManager with a ValueTree」をやっている人
GUIへテキストボタンの追加
アンドゥとリドゥを実行するボタンをGUIへ追加します。
MainComponent.h
privateなメンバとして、[1]のようにテキストボタンクラスでボタンを追加します。
private:
juce::TreeView tree;
std::unique_ptr<ValueTreeItem> rootItem;
juce::TextButton undoButton, redoButton;//[1]テキストボタンを2つ追加します。
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent)
};
MainComponent.cpp
先ほど作成したテキストボタンクラスのオブジェクトをコンストラクタで初期化します。
MainComponent::MainComponent() : undoButton("Undo"),redoButton("Redo") //[1]
{
//...略...
//[2]
addAndMakeVisible(undoButton);
addAndMakeVisible(redoButton);
undoButton.onClick = [this] {};
redoButton.onClick = [this] {};
setSize(600, 400);
}
[1]のようにメンバイニシャライザから初期化を行い、[2]のコンストラクタ内でボタンの可視化、クリックしたときの処理をラムダ式で定義します。ラムダ式内はまだなにも入れていません。ここに、後ほど、アンドゥ、リドゥの処理を入れていきます。
次に、resized関数でボタンをGUI上に配置します。次のように処理内容を変更しました。今回は、removeFromBottom関数とremoveFromLeft関数での配置となっています。
void MainComponent::resized()
{
auto r = getLocalBounds();
auto buttons = r.removeFromBottom(20);
undoButton.setBounds(buttons.removeFromLeft(100));
redoButton.setBounds(buttons.removeFromLeft(100));
tree.setBounds(r);
}
removeFrom●●という関数の動作については、こちらの記事で調査していますので、ご参照くださいませ。
アンドゥ、リドゥ機能の追加
次に、juce::UndoManagerクラスを利用して、undo・redo機能を実装していきます。
MainComponent.h
まずは、ValueTreeItemクラスの変更をしていきます。undoManagerクラス
ValueTreeItemクラス
まずは、ValueTreeItemクラスのprivateなメンバとして、undoManagerの参照変数を定義します。ここに、MainComponentで定義されたundoManagerのアドレスがわたり、undoManagerのインスタンスを参照できるようにします。
juce::UndoManager& undoManager;//
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ValueTreeItem)
};
ValueTreeItemクラスのコンストラクタに以下のように追記します。
ValueTreeItem(const juce::ValueTree& v, juce::UndoManager& um)//[1]引数にUndoManagerを追加しました。
: tree(v), undoManager(um)//[2]イニシャライザでundoManagerを追加します。
{
tree.addListener(this);
}
[1]で、UndoManagerクラスのインスタンスのアドレスを受け取り、[2]で先ほど定義したundoManagerに入れます。
次に、各種関数へundoManagerを引数として追加します。
itemDropped関数、moveItems関数に以下の[1]、[2]、[3]のようにundoManagerを追加しています。ValueTreeの変更を行うときに、undoMaganerを渡すようにしています。
void itemDropped(const juce::DragAndDropTarget::SourceDetails&, int insertIndex) override
{
juce::OwnedArray<juce::ValueTree> selectedTrees;
getSelectedTreeViewItems(*getOwnerView(), selectedTrees);
//[1]引数、undoManagerを追加します。
moveItems(*getOwnerView(), selectedTrees, tree, insertIndex, undoManager);
}
//[2]関数定義にundoManagerを引数追加します。
static void moveItems(juce::TreeView& treeView, const juce::OwnedArray<juce::ValueTree>& items,
juce::ValueTree newParent, int insertIndex, juce::UndoManager& undoManager)
{
if (items.size() > 0)
{
std::unique_ptr<juce::XmlElement> oldOpenness(treeView.getOpennessState(false));
for (auto i = items.size(); --i >= 0;)
{
auto& v = *items.getUnchecked(i);
if (v.getParent().isValid() && newParent != v && !newParent.isAChildOf(v))
{
if (v.getParent() == newParent && newParent.indexOf(v) < insertIndex)
--insertIndex;
//[3]次の2行でnullptrからundoManagerを渡すように変更します。
v.getParent().removeChild(v, &undoManager);
newParent.addChild(v, insertIndex, &undoManager);
}
}
if (oldOpenness != nullptr)
treeView.restoreOpennessState(*oldOpenness, false);
}
}
ValueTreeItemクラスのprivateなメンバに定義しているrefreshSubItems関数内にも次の[1]のようにundoManagerを追加しています。
void refreshSubItems()
{
clearSubItems();
for (auto i = 0; i < tree.getNumChildren(); ++i)
addSubItem(new ValueTreeItem(tree.getChild(i), undoManager));//[1]undoManagerを引数に追加しました。
}
MainComponentクラス
MainComponentクラスの変更です。
class MainComponent : public juce::Component
,public juce::DragAndDropContainer
{
//...略...
private:
juce::TreeView tree;
std::unique_ptr<ValueTreeItem> rootItem;
juce::TextButton undoButton, redoButton;
juce::UndoManager undoManager;//[1]undoManagerを追加します。
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent)
};
privateなメンバにundoManagerを定義します。これは、先ほどTreeViewItemクラスに参照を渡した本体になります。
MainComponent.cpp
最後に、MainComponentのコンストラクタで、ボタンを押した際に、アンドゥとリドゥの処理を行うようにラムダ式に定義します。
MainComponent::MainComponent() : undoButton("Undo"),redoButton("Redo")
{
//...略...
addAndMakeVisible(undoButton);
addAndMakeVisible(redoButton);
undoButton.onClick = [this] {undoManager.undo();};//[1]ラムダ式にアンドゥ実行の処理
redoButton.onClick = [this] {undoManager.redo();};//[2]ラムダ式にリドゥ実行の処理
setSize(600, 400);
}
ここまでで感じたこと
undoManagerは、とにかく、ValueTreeの変更時に引数として渡してあげるという点がポイントのようですね。ValueTreeの使い方は他のチュートリアルになっており、詳しく勉強する必要がありますが、今回のundoManagerの使い方を見ていると何となく雰囲気がわかってきました。
クラス間でundoManagerを共有するという点も必要です。本体クラスにundoManagerを定義して、利用したいクラスで参照変数として本体のundoManagerのアドレスをもらい、同じundoManagerに操作を蓄積していく、みたいなイメージでしょうか。
また、undo・redoを実行するためには、ボタンなどでundo、redo関数をそれぞれ実行しなければいけないということもわかりました。(よく使うショートカットキーctrl + Zなどを割り当てることもできるのか気になってきました。)
とりあえず、現在作成しているシンプルなハイパスフィルターのAudioProcessorValueTreeStateクラスにundoManagerを反映させていきたいなと思いました。