Hatena::ブログ(Diary)

ザリガニが見ていた...。 このページをアンテナに追加 RSSフィード

2010-10-26

テキスト編集のキー操作を自在にコントロールする

その裏で何をしているか?

パソコンで文章を書くという行為は、最も一般的だけど、その裏では高度な技術が動いていたりする。例えば、aというキーを押したら、画面にaと表示されるのが当り前と思ってはいけないのだ。

  • OSは、キーを押した時に発生する電気信号を、キーコードとして受け取るだけである。
  • そのキーコードのよって、画面上の指定された位置に、指定されたフォントで、aという文字が表示されるのは、OSが描き出す幻想である。
  • 最近のフォントは輪郭を数式化された関数で描画する。さらに、アンチエイリアス処理によって、ピクセル間をなめらかな曲線に仕上げる。
  • 人間にとって文字と画像は区別されるモノだが、パソコンにとってはどちらも画像で、CPUにとってはメモリ上に格納された電気信号でしかない。
  • 文字は入力されたらそれでおしまいではなく、絶えず挿入・削除が繰り返され、編集される運命にある。
  • 文字が挿入されたら、その位置から右側にある文字は1文字分右にずれるし、行が折り返していれば、そのズレは次の行、あるいはその段落の最後までの数行に伝播することもある。
  • OSは現在編集中の位置を明示するため、文字カーソルも表示してくれる。
  • その文字カーソルさえ単なる画像なのだが、文字の入力に応じて右側に移動するのは、OSが位置を変えて描き直しているからである。
  • 文字カーソルが点滅するのもすべて、OSが特定の間隔で描いては消すを繰り返しているからである。
  • 上下左右のカーソルキーを押せば、文字カーソルもそれに応じて移動する。
  • shiftキーを組み合わせれば、テキストの背景に色がついて選択中であることが明示される。
  • それらもすべて、OSが位置を変更して、背景色を変更して、描き直しているからである。
  • つまり、画面に表示されていることはすべて、OSが描き出す幻想である。
  • 現実は、キーを押して、それに応じた電気信号が発生する事実しかないのだ。

文章を書くというありふれた行為の中で、OSは上記のような処理*1を黙々とこなしているのである。

このテキスト編集の仕組みは、遥か昔から脈々と進化を続けて今に至る。そこには、達人たちの数多くの創意工夫が込められているはず。

共通の操作性

OSXのCocoaアプリケーションであれば、テキストを編集する操作性というのは、ほぼ一貫している。テキストエディットで文章を書く時、Safariのテキストエリアで文章を書く時、Mailで文章を書く時、Xcodeでコードを書く時、いずれの場合もほぼ同じ操作性で、違和感なく、自然な感覚でキー操作できる。

もし、アプリケーションごとにテキスト編集の操作が異なっていたら、おそらく相当使いにくい環境になると思う。当然のように感じているが、共通の操作性が提供されている、ということは素晴らしいことだ。


      • 以下、文中の半角¥は半角\に置き換えて読む必要がある。

OSXの標準キーバインド

  • それではこの共通の操作性はどこから来ているのかというと、まず、OSXの標準を決める設定ファイルがあった。
/System/Library/Frameworks/AppKit.framework/Resources/StandardKeyBinding.dict
  • StandardKeyBinding.dictは、Property List Editor*2で開けるのだが、特殊なキーを表すコードが文字化けしてしまい、区別できない。

f:id:zariganitosh:20101025102513p:image

  • そこで、デスクトップにText Property List形式で保存し直すと、以下のようなテキストファイルとして確認できた。(= "noop:"の行は除外)
{
	"\U0003" = "insertNewline:";
	"\b" = "deleteBackward:";
	"\t" = "insertTab:";
	"\n" = "insertNewline:";
	"\n" = "insertNewline:";
	"\U0019" = "insertBacktab:";
	"\U001b" = "cancelOperation:";
	"$\Uf700" = "moveUpAndModifySelection:";
	"$\Uf701" = "moveDownAndModifySelection:";
	"$\Uf702" = "moveLeftAndModifySelection:";
	"$\Uf703" = "moveRightAndModifySelection:";
	"$\Uf729" = "moveToBeginningOfDocumentAndModifySelection:";
	"$\Uf72b" = "moveToEndOfDocumentAndModifySelection:";
	"$\Uf72c" = "pageUpAndModifySelection:";
	"$\Uf72d" = "pageDownAndModifySelection:";
	"@ " = "cycleToNextInputScript:";
	"@$\Uf700" = "moveToBeginningOfDocumentAndModifySelection:";
	"@$\Uf701" = "moveToEndOfDocumentAndModifySelection:";
	"@$\Uf702" = "moveToLeftEndOfLineAndModifySelection:";
	"@$\Uf703" = "moveToRightEndOfLineAndModifySelection:";
	"@." = "cancelOperation:";
	"@^ " = "togglePlatformInputSystem:";
	"@^\Uf701" = "makeBaseWritingDirectionNatural:";
	"@^\Uf702" = "makeBaseWritingDirectionRightToLeft:";
	"@^\Uf703" = "makeBaseWritingDirectionLeftToRight:";
	"@~ " = "cycleToNextInputKeyboardLayout:";
	"@~^\Uf701" = "makeTextWritingDirectionNatural:";
	"@~^\Uf702" = "makeTextWritingDirectionRightToLeft:";
	"@~^\Uf703" = "makeTextWritingDirectionLeftToRight:";
	"@^?" = "deleteToBeginningOfLine:";
	"@\Uf700" = "moveToBeginningOfDocument:";
	"@\Uf701" = "moveToEndOfDocument:";
	"@\Uf702" = "moveToLeftEndOfLine:";
	"@\Uf703" = "moveToRightEndOfLine:";
	"^\U0003" = "insertLineBreak:";
	"^\t" = "selectNextKeyView:";
	"^\n" = "insertLineBreak:";
	"^\n" = "insertLineBreak:";
	"^\U0019" = "selectPreviousKeyView:";
	"^\"" = "insertDoubleQuoteIgnoringSubstitution:";
	"^$\Uf702" = "moveToLeftEndOfLineAndModifySelection:";
	"^$\Uf703" = "moveToRightEndOfLineAndModifySelection:";
	"^'" = "insertSingleQuoteIgnoringSubstitution:";
	"^/" = "insertRightToLeftSlash:";
	"^a" = "moveToBeginningOfParagraph:";
	"^A" = "moveToBeginningOfParagraphAndModifySelection:";
	"^b" = "moveBackward:";
	"^B" = "moveBackwardAndModifySelection:";
	"^d" = "deleteForward:";
	"^e" = "moveToEndOfParagraph:";
	"^E" = "moveToEndOfParagraphAndModifySelection:";
	"^f" = "moveForward:";
	"^F" = "moveForwardAndModifySelection:";
	"^h" = "deleteBackward:";
	"^k" = "deleteToEndOfParagraph:";
	"^l" = "centerSelectionInVisibleArea:";
	"^n" = "moveDown:";
	"^N" = "moveDownAndModifySelection:";
	"^o" = (
		"insertNewlineIgnoringFieldEditor:",
		"moveBackward:",
	);
	"^p" = "moveUp:";
	"^P" = "moveUpAndModifySelection:";
	"^t" = "transpose:";
	"^V" = "pageDownAndModifySelection:";
	"^v" = "pageDown:";
	"^y" = "yank:";
	"^~^?" = "deleteWordBackward:";
	"^^?" = "deleteBackwardByDecomposingPreviousCharacter:";
	"^\Uf700" = "scrollPageUp:";
	"^\Uf701" = "scrollPageDown:";
	"^\Uf702" = "moveToLeftEndOfLine:";
	"^\Uf703" = "moveToRightEndOfLine:";
	"~\U0003" = "insertNewlineIgnoringFieldEditor:";
	"~\b" = "deleteWordBackward:";
	"~\t" = "insertTabIgnoringFieldEditor:";
	"~\n" = "insertNewlineIgnoringFieldEditor:";
	"~\n" = "insertNewlineIgnoringFieldEditor:";
	"~\U001b" = "complete:";
	"~$\Uf700" = "moveParagraphBackwardAndModifySelection:";
	"~$\Uf701" = "moveParagraphForwardAndModifySelection:";
	"~$\Uf702" = "moveWordLeftAndModifySelection:";
	"~$\Uf703" = "moveWordRightAndModifySelection:";
	"~^b" = "moveWordBackward:";
	"~^B" = "moveWordBackwardAndModifySelection:";
	"~^f" = "moveWordForward:";
	"~^F" = "moveWordForwardAndModifySelection:";
	"~^?" = "deleteWordBackward:";
	"~\Uf700" = (
		"moveBackward:",
		"moveToBeginningOfParagraph:",
	);
	"~\Uf701" = (
		"moveForward:",
		"moveToEndOfParagraph:",
	);
	"~\Uf702" = "moveWordLeft:";
	"~\Uf703" = "moveWordRight:";
	"~\Uf728" = "deleteWordForward:";
	"~\Uf72c" = "pageUp:";
	"~\Uf72d" = "pageDown:";
	"^?" = "deleteBackward:";
	"\Uf700" = "moveUp:";
	"\Uf701" = "moveDown:";
	"\Uf702" = "moveLeft:";
	"\Uf703" = "moveRight:";
	"\Uf708" = "complete:";
	"\Uf728" = "deleteForward:";
	"\Uf729" = "scrollToBeginningOfDocument:";
	"\Uf72b" = "scrollToEndOfDocument:";
	"\Uf72c" = "scrollPageUp:";
	"\Uf72d" = "scrollPageDown:";
	"\Uf739" = "delete:";
}

設定ファイルの解読

  • 上記の設定ファイルを一見しただけでは、その意味をなかなか理解できないが...
  • 各種記号やコードに以下の意味があると分かれば、だいぶ理解が進む。
意味記号
control^
shift$
option (Alt)~
command (Apple)@
テンキー#
意味文字コード (記号)意味文字コード (記号)
esc\U001BF1\UF704
tab\U0009 (\t)F2\UF705
backtab (shift tab)\U0019F3\UF706
delete\U007F(\b)F4\UF707
forward delete (fn delete)\UF728F5\UF708
return\U000D (\n)F6\UF709
enter\U0003F7\UF70A
home\UF729F8\UF70B
end\UF72BF9\UF70C
page up\UF72CF10\UF70D
page down\UF72DF11\UF70E
\UF700F12\UF70F
\UF701F13\UF710
\UF702F14\UF711
\UF703F15\UF712
例1(基本):
  • StandardKeyBinding.dictの1行目は、以下のようになっている。
"\U0003" = "insertNewline:";
  • コード"\U0003"はenterに対応するので、読み方は以下と同等。
"enter" = "insertNewline:";
  • つまり、enterキーを押すと、insertNewline:という処理が実行されるのだ。(つまり、改行する)
例2(キーコンビネーション):
"@ " = "cycleToNextInputScript:";
  • @はcommandの意。
  • それが半角スペースと組み合わさっているので、command-spaceというお馴染みのショートカットを表現している。
  • OS内部的な処理としては、cycleToNextInputScript:という処理が実行されるのだ。(直前の入力モードに切り替える)
例3(大文字キーの扱い):
"^a" = "moveToBeginningOfParagraph:";
"^A" = "moveToBeginningOfParagraphAndModifySelection:";
  • ^はcontrolの意。
  • それがaと組み合わさっているので、control-aというショートカットを表現している。
    • moveToBeginningOfParagraph:という処理は、文字カーソルを段落の先頭に移動する。
  • さらにAと組み合わさると、通常shift-aでAが入力されので、control-shift-aというショートカットを表現していることになる。
    • moveToBeginningOfParagraphAndModifySelection:という処理は、今のカーソル位置から段落の先頭までを選択状態にする。
例4(複合処理):
"~\Uf700" = (
	"moveBackward:",
	"moveToBeginningOfParagraph:",
);
  • ~はoptionの意。\Uf700は↑の意。つまり、option-↑というショートカットを表現している。
  • それに対応する処理が、今度は()で括られて、その中身の要素がカンマで区切られ、二つある。
  • この場合、moveBackward:とmoveToBeginningOfParagraph:という処理が順に実行されることになる。
    • 文字カーソルを一文字戻る方向に移動して、さらにその段落の先頭に移動する、という処理だ。
    • 実際に操作してみると、文字カーソルが段落の先頭を次々と遡っていく。

OSX標準のキー設定にはないが、以下のように表現することもできるようだ。

例5(末尾の特殊記号):
  • 特殊記号といえども、末尾では文字列そのものと解釈された。
"^$" = "moveBackward:"
  • 上記はcontrol-$である。(control-shiftではない)
例6(キーバインドの連続技):
"^x" = {"u" = "undo:";};
  • control-xに続けて、uでundo:が実行される。(つまり、編集 >> 取り消す が実行される)
  • 上記のように、キー操作の値に、{}で囲ったさらなるキー操作を定義することで、キー操作の連続を表現できる。

以上の例を参考にすれば、OSX標準のテキスト編集キーバインド(ショートカット、あるいはキー操作)をすべて読み解くことができる。理解できない処理は、実際にキー操作を実行してみるのが手っ取り早い。

  • キー操作によっては、機能しないものもあるかもしれない。
  • ショートカットが重複する場合、テキスト編集キーバインドの優先順位は低い。
  • 各アプリケーションが設定しているショートカットや、システム環境設定 >> キーボード >> キーボードショートカット、Quicksilverのショートカットの方が優先された。(気がする)

好みのキーバインドを設定する場所

  • システム標準の上記StandardKeyBinding.dictをルールに従って書き換えれば、自分好みのキーバインドが実現できることは明白だが、そんなことをする必要はない。
  • OSXは、テキスト編集のキーバインドをちゃんとカスタマイズする仕組みを用意してくれていた。
# 全ユーザー共通の設定(ルート直下のLibrary)
/Library/KeyBindings/DefaultKeyBinding.dict

# そのユーザー独自の設定(ホーム直下のLibrary)
~/Library/KeyBindings/DefaultKeyBinding.dict
  • 上記のようにKeyBindingsというフォルダを新規追加して、DefaultKeyBinding.dictというファイルを用意すれば、OSXはそれらのキーバインドを優先してくれるのだ。
  • ところで自分のMacBookには、どこにもKeyBindingsというフォルダは存在しなかった。
  • つまり、これまでOSX標準のキーバインドしか利用していなかった、という証拠である。

この仕組みを知ってしまった今、実に勿体ない話しだ...。(今までかなり損してたよ、トホホ...)

設定可能なアクション

  • OSX標準のテキスト編集キーバインド(ショートカット、あるいはキー操作)には、実に様々な処理が設定されている。
  • ところで、キー操作に対応して設定されるこのinsertNewline:、moveToBeginningOfParagraph:等の処理とは一体何なのか?
  • なんと!その実体は、NSResponderクラスが持つアクションなのである。
  • Respondとは返事をするという意味の動詞で、Responderとはそれにerを付けて擬人化した、返事をする人という造語だと思われる。
    • Speak(話すという意味の動詞)にerを付けたSpeakerが話しをする人、という意味になるのと同様に。
  • NSResponderは、様々なキー入力やマウスクリックといったイベント メッセージに反応する(返事する)仕組みを提供しているのだ。
  • OSXで利用するGUIオブジェクトのほとんどすべてが、NSObject >> NSResponderから継承して作られている。
  • GUIがNSResponderを継承するということは、つまり、NSResponderのアクションが共通のアクションとして必ず存在しているのだ。
  • enterキーを押すと、StandardKeyBinding.dictの設定からinsertNewline:というアクション(メッセージ)がテキストビューに投げられ、
  • 最終的にはNSResponderに設定されるアクション(メソッド)insertNewline:が反応して、その処理が実行されていたのだ。
  • だから、NSResponderのアクションを知ることは、OSX標準で用意されているテキスト操作の基本機能を知ることに繋がる。
  • また、テキストの編集環境は、NSTextViewというオブジェクトが担っている。
  • もちろんNSResponderを継承しているのでNSResponderのアクションに反応するのは当然のこと、
  • さらに、NSTextView自体が実装するアクションにも反応するのだ。
  • 基本的に英語環境で起動した時に表示されるメニューの名称は、そのままアクション名として実装されているはずである。
    • メニューの名称に:を追加する。
    • メニュー末尾の...は取り除く。
  • 一体どんなアクションがあるのだろうか?
alignCenter:
alignJustified:
alignLeft:
alignRight:
breakUndoCoalescing
cancelOperation:
capitalizeWord:
center
centerSelectionInVisibleArea:
changeCaseOfLetter:
checkSpelling:
clearRecentDocuments:
complete:
copy:
copyFont:
copyRuler:
cut:
delete:
deleteBackward:
deleteBackwardByDecomposingPreviousCharacter:
deleteForward:
deleteToBeginningOfLine:
deleteToBeginningOfParagraph:
deleteToEndOfLine:
deleteToEndOfParagraph:
deleteToMark:
deleteWordBackward:
deleteWordForward:
hide:
ignoreSpelling:
indent:
insertBacktab:
insertContainerBreak:
insertLineBreak:
insertNewline:
insertNewlineIgnoringFieldEditor:
insertParagraphSeparator:
insertTab:
insertTabIgnoringFieldEditor:
insertText:
loosenKerning:
lowerBaseline:
lowercaseWord:
moveBackward:
moveBackwardAndModifySelection:
moveDown:
moveDownAndModifySelection:on:
moveForward:cking:
moveForwardAndModifySelection:
moveLeft:
moveLeftAndModifySelection:
moveRight:
moveRightAndModifySelection:
moveToBeginningOfDocument:
moveToBeginningOfDocumentAndModifySelection:
moveToBeginningOfLine:
moveToBeginningOfLineAndModifySelection:
moveToBeginningOfParagraph:
moveToEndOfDocument:
moveToEndOfDocumentAndModifySelection:
moveToEndOfLineAndModifySelection:
moveToEndOfLine:
moveToEndOfParagraph:
moveUp:
moveUpAndModifySelection:
moveWordBackward:
moveWordBackwardAndModifySelection:
moveWordForward:
moveWordForwardAndModifySelection:
moveWordLeft:
moveWordLeftAndModifySelection:
moveWordRight:
moveWordRightAndModifySelection:
newDocument:
openDocument:
orderBack:
orderFront:
orderFrontLinkPanel:
orderFrontListPanel:
orderFrontSpacingPanel:
orderFrontTablePanel:
outline:
pageDown:
pageUp:
paste:
pasteAsPlainText:
pasteAsRichText:
pasteFont:
pasteRuler:
performClose:
performMiniaturize:
performZoom:
printDocument:
raiseBaseline:
redo:
revertDocumentToSaved:
runPageLayout:
saveAllDocuments:
saveDocument:
saveDocumentAs:
saveDocumentTo:
scrollLineDown:
scrollLineUp:
scrollPageDown:
scrollPageUp:
selectAll:
selectLine:
selectParagraph:
selectToMark:
selectWord:
setMark:
showContextHelp:
showGuessPanel:
startSpeaking:
stopSpeaking:
subscript:
superscript:
swapWithMark:
terminate:
tightenKerning:
toggleBaseWritingDirecti
toggleContinuousSpellChe
toggleRuler:
transpose:
transposeWords:
turnOffKerning:
turnOffLigatures:
underline:
undo:
unscript:
uppercaseWord:
useAllLigatures:
useStandardKerning:
useStandardLigatures:
yank:
  • 上記のアクション名一覧*3は、すべてを網羅できていない。
  • また、中には動作しないアクションもあるかもしれない。
  • アクションの意味やさらなる詳細は、以下ページが詳しい。
  • アクション名が:で終わっているのは、各アクションが何らかの引数を取るからであり、アクション名は必ず:まで含める必要がある。(省略できない)

自分仕様のキーバインドを設定する

  • 以上の仕組みを理解して、早速自分が欲しい機能を試してみた。
    • 設定ファイルの保存場所は以下のパスにした。
    • ~/Library/KeyBindings/DefaultKeyBinding.dict
  • 実験にはテキストエディットを利用した。
  • DefaultKeyBinding.dictに保存して、テキストエディットを再起動すると、キー操作に反映された。
設定する時の注意
  • control-shift-o = control-O = "^O"である。("^$o"ではエラーになる)
  • control-shift-1 = control-! = "^!"である。("^$1"ではエラーになる)
  • つまり、shift操作が有効なキーには、shift操作で発生する文字を書いて、shiftマークの$は書かない。
  • だから、shiftマークの$を書くのは、矢印キーの操作ぐらいしかないと思う。
基本
{
  "^l" = "lowercaseWord:";
  "^u" = "uppercaseWord:";
  "^c" = "capitalizeWord:";
}
  • control-l:カーソル右側の単語、あるいは選択範囲のすべての単語を小文字にする。
  • control-u:カーソル右側の単語、あるいは選択範囲のすべての単語を大文字にする。
  • control-c:カーソル右側の単語、あるいは選択範囲のすべての単語の頭文字だけ大文字にする。
  • control-l、control-cはうまく動いたが、control-uだけ反応しない...。
  • たぶん、control-uが他のショートカットと重複しているのだと思う。
  • ちなみに、上記の機能は、編集 >> 変換 >> に続くメニューに、システム環境設定からショートカットを割り当てる方法でも可能だ。
キーバインドの連続技
    "^/" = {
            "l" = "lowercaseWord:";
            "u" = "uppercaseWord:";
            "c" = "capitalizeWord:";
    };
  • もはや、自分のMacBookはショートカットで溢れている。
  • 新たなショートカットを見つけ出すのは結構面倒くさい。
  • テキスト編集キーバインドの良い所は、キー操作を連続技で認識してくれる所だ。
  • 上記のように設定しておけば...
    • control-/に続けてlで、小文字に変換する。
    • control-/に続けてuで、大文字に変換する。
    • control-/に続けてcで、頭文字だけ大文字に変換する。
  • この設定なら3つともちゃんと機能した!
アクションの複合技(基本)
{
    "~\Uf700" = (
	"moveBackward:",
	"moveToBeginningOfParagraph:",
    );
}
  • 上記の設定は、OSXの標準キーバインドの設定にある。
  • option-↑で...
    • moveBackward: ......カーソルを戻る方向に一つ移動する。
    • moveToBeginningOfParagraph: ......カーソルを段落の先頭に移動する。
  • つまり、option-↑を繰り返せば、カーソルが段落の先頭を遡っていくのだ。
アクションの複合技(タイトル化)
{
    "^~u" = ("selectParagraph:",
             "uppercaseWord:",
             "moveToEndOfParagraph:",
             "insertNewline:",
             "insertNewline:");
}
  • control-option-uで...
    • selectParagraph: ......カーソルを含む段落を選択状態にする。
    • uppercaseWord: ......選択範囲を大文字にする。
    • moveToEndOfParagraph: ......カーソルを段落の末尾へ移動する。
    • insertNewline: ......改行を追加する。
  • つまり、入力した行でcontrol-option-uを押すと、その行をタイトルとみなし単語すべてを大文字に変換して、空行を1行入れて、入力待ち状態になるのだ。
アクションの複合技(タグ化)
{
    "^H" = ("setMark:",
            "moveWordBackward:",
            "deleteToMark:",
            "insertText:", "<", "yank:", "insertText:", ">",
            "setMark:",
            "insertText:", "</", "yank:", "insertText:", ">",
            "swapWithMark:");
}
  • ちょっと複雑になってきたが、基本に忠実にアクションを一つずつ見ていくことにする。
  • control-shift-hで...
    • setMark: ......カーソル位置を記憶する。
    • moveWordBackward: ......カーソルを単語の先頭に移動する。
    • deleteToMark: ......カーソル位置からマークした位置まで削除する。
    • insertText: ......これに続くテキストを挿入する。
    • yank: ......直前に削除したテキストをペーストする。
    • swapWithMark: ......カーソルをマークした位置に移動する。
  • つまり、何をやっているかというと、こんな時は実際に試してみるのが一番いい。
  • divと入力して、control-shift-hを操作してみると、以下のようになった。
<div></div>
  • なんと、入力した単語をタグ化してくれるのだ!
  • ちゃんと閉じるダグまで準備してくれる!
  • おまけに、カーソル位置をタグの真ん中にして!

これは便利!

  • 今まで、こんな処理をするにはAppleScriptやAutomatorでコードを書く必要があると思っていた。
  • そして、Quicksilverでショートカットを設定したり、サービスメニューに組み込んだりする必要があると思っていた。
  • ところが、キーバインドの設定なら、このような単純なアクションの組み合わせで実現できてしまうのだ!
  • さらに、キー操作にも瞬時に反応し、AppleScriptやAutomatorとは比較にならないほど軽快。

素晴らしい!

  • キーバインドの連続技、アクションの複合技には、新たな可能性を感じる。
新たな可能性を感じて作った設定
  • 調子に乗って作ってみた。
  • USキーボードのMacBook。
{
/* 小文字・大文字・頭文字だけ大文字へ変換(control-/、l・u・c) */
  "^/" = {
    "l" = "lowercaseWord:";
    "u" = "uppercaseWord:";
    "c" = "capitalizeWord:";
  };

/* カーソル位置の行をすべて大文字タイトルにする(control-option-u) */
    "^~u" = ("selectParagraph:",
             "uppercaseWord:",
             "moveToEndOfParagraph:",
             "insertNewline:",
             "insertNewline:");

/* 直前の単語をタグにする(control-shift-h) */
    "^H" = ("setMark:",
            "moveWordBackward:",
            "deleteToMark:",
            "insertText:", "<", "yank:", "insertText:", ">",
            "setMark:",
            "insertText:", "</", "yank:", "insertText:", ">",
            "swapWithMark:");

/* 選択範囲の両端を1文字ずつ拡張する(control-shift-=) */
    "^+" = (
            "setMark:",
            "moveForward:",
            "moveForward:",
            "selectToMark:",
            "moveBackwardAndModifySelection:",
    );

/* 選択範囲を''""[]{}()<>「」囲う(control-x、'・"・[・{・(・<・control-[) */
  "^x" = {
    "'" = (
            "setMark:",
            "deleteToMark:",
            "insertText:", "'", "yank:", "insertText:", "'",
    );
    "\"" = (
            "setMark:",
            "deleteToMark:",
            "insertText:", "\"", "yank:", "insertText:", "\"",
    );
    "[" = (
            "setMark:",
            "deleteToMark:",
            "insertText:", "[", "yank:", "insertText:", "]",
    );
    "{" = (
            "setMark:",
            "deleteToMark:",
            "insertText:", "{", "yank:", "insertText:", "}",
    );
    "(" = (
            "setMark:",
            "deleteToMark:",
            "insertText:", "(", "yank:", "insertText:", ")",
    );
    "<" = (
            "setMark:",
            "deleteToMark:",
            "insertText:", "<", "yank:", "insertText:", ">",
    );
    "^[" = (
            "setMark:",
            "deleteToMark:",
            "insertText:", "「", "yank:", "insertText:", "」",
    );
  };

}
  • 選択範囲の両端を1文字ずつ縮小する アクションが思い浮かばない...。無理だろうか?
有名なキーバインド

いくつものキーバインドを正確に設定するのは意外と大変。そんな時は達人の設定が公開されていた。

参考ページ

上記のページとそこからリンクされるページには、非常に興味深い情報が盛りだくさん。(感謝です!)


  • 久々にワクワクしながら、ブログをむさぼり読む感覚を味わってしまった。
  • こんな仕組みがOSXに用意されていたとは...。
  • いつまで経っても分からないことだらけだ...。

*1:一例でしかない、もっと、もっと多くの処理が隠れているはず。

*2:Developer Tools(Xcode)をインストールすると、そこに含まれている。

*3NSResponder Class Referenceのaction messageとメニュー名称のアクション

ひろしひろし 2012/11/11 23:47 現在のカーソル位置の前後の文字列を
**NSRespoderクラスから取得したいのですが
する方法はわかりますか。
(探してはいるのですが見つかりません。)
それを使って
その情報を常に更新して
アドレスブックの自宅電話番号のコンテクストメニューの
『大きな文字で表示』コマンドみたいなかんじで
表示し続けるAppleScriptを作りたいです。

欲を言えば日本語入力の
**入力中(アンダーラインが表示されているとき)の文字列も取得したい。
アンダーラインが表示されているときはその部分のみを
確定後は段落内のカーソル前後の文字列を大きく表示する。
こんなことができたらいいです。

zariganitoshzariganitosh 2012/11/13 05:36 自分もなんちゃってCocoaレベルなので正確なことは言えませんが、
NSTextViewにはカーソル位置や必要な文字列を取得するメソッドがあるはずです。
(NSRespoderは共通のイベント処理を取りまとめたGUIに継承されることを目的としたクラス。NSTextViewは、NSRespoderを継承してテキストエディットに必要な処理をすべて持っているクラス。)
そのメソッドさえ見つかれば、カーソル位置の前後の文字は取得できます。

しかし、それが簡単にできるのは、自分が作るアプリケーションの中でのこと。
人様の作ったアプリケーションの一部から、テキスト情報を取得しようとしたら、
自分が今すぐに思いつく方法はコピー&ペーストぐらいです。
(Cocoaの達人なら、NSTextViewを拡張したり、アプリケーションの処理を横取りする方法で実現可能かもしれませんが、その方法は自分にはまったく分かりません。)

ところで、OSX 10.8ではピクチャ・イン・ピクチャという画面拡大の方法が追加されました。
iOSで文字カーソルを移動する時に表示されるような、画面の一部を拡大鏡で表示するGUIです。
拡大鏡の表示エリア、拡大率なども自由に設定可能です。
(システム環境設定 >> アクセシビリティ >> ズーム機能 で詳細な設定ができます)
また、10.6環境であってもシステム環境設定 >> ユニバーサルアクセスで画面全体をズーム機能で拡大することはできます。(ショートカットで瞬時に)

もし、この質問が文字を大きく表示したいという「実用」を目的としているなら、この方法で満足できませんか?
(まだ10.6環境であるなら、この機会に10.8にアップデートしてみるのも良いかもしれません。)
もし、純粋にプログラミングすることを楽しみたいなら、まずはCocoaの基礎を学ぶ必要があります。
そして、望む機能がどれほどの手間で実現可能なのか、判断する知識が必要です。

ひろしひろし 2012/11/13 22:47 返答ありがとうございます。
感謝しています。

キーバインドに関してですが
例えば
複数の修飾キー+→で**次の単語を選択して
辞書をポップアップで表示する。
これができるとショートカットだけで連続して辞書を引くことができる。

キーバインドの**アクションに辞書を引くコマンドや
その他のメニューコマンドが実行できたら
さらにすばらしいですね。

zariganitoshzariganitosh 2012/11/14 05:29 > 複数の修飾キー+→で**次の単語を選択して
> 辞書をポップアップで表示する。
> これができるとショートカットだけで連続して辞書を引くことができる。

わざわざ次の単語を選択するまでもなく、できますよ。(連続ポップアップのことです)
command-control-Dでマウスカーソル下の単語が辞書検索されて、ポップアップされるのはご存知ですよね?
その時、修飾キーであるcommand-controlを押し続けたまま、マウスカーソルを移動させてみてください。
次々とマウスカーソル下の文字が辞書検索されて、ポップアップが切り替わって表示されます!

但し、連続ポップアップされるのはテキストエディットなどの特定の環境に限られますが...。
SafariのURLや検索のテキストフィールドはOKでしたが、コメント入力欄はNGでした。

英語のページを翻訳する時などとても役に立ちます。
テキストエディットなどにコピーしてぜひお試しを!

ひろしひろし 2012/11/14 21:48 自分の環境の
MacOSX10.6.8では連続ポップアップできること知っていましたが
ずいぶん前に近くの電気屋で
MacOSX10.7のマックを操作したときに
できなかったのでてっきり廃止されたのかと思いました。
自分の環境のSafari5.1.7の最新版では急に連続ポップアップできなくなりました。

最新のMacOSX10.8ではすべてのテキストに対応してはいないようですね。

MacOSX10.6.8ではこの辞書ポップアップの機能は
/System/Library/CoreServices/DictionaryPanel.bundleに収められています。
さらに『パッケージの内容を開く』で
/Contents/Resourcesフォルダの中に**DCSDictionaryPanelStyle.cssファイルがあります。
/Contents/Resources/Japanese.lproj/DCSDictionaryPanel.nibを展開すると
***keyedobjects.nibがでてきます。
これをProperty List Editorで開くと内容を編集できそうです。
このCSSファイルとnibファイルの内容を変更することで
通常は文字列しか表示されない内容に**画像(イメージ)を表示できたり
ポップアップの***ウインドウサイズや**フォントサイズを変更できたり
できそうです。(たぶん)

最新のMacOSX10.8ではこれらのファイルはどこにあるのでしょうか。
**10.7ではなくなっていた気がする。

zariganitoshzariganitosh 2012/11/15 05:28 > MacOSX10.6.8では連続ポップアップできること知っていましたが

そうでしたか、ご存知でしたか。失礼しました。
そして、連続ポップアップはおっしゃる通り自分の10.8環境でも機能しません。
機能として選択肢が減ってしまったことは残念なのですが、
その代わり、トラックパッドジェスチャーの3本指タップでポップアップ辞書が表示されるようになりました。
また、10.8ではポップアップ辞書にデフォルトインストールされる有効な辞書すべてが、一覧表示されるようになりました。
(有効にしておけば、Wikipediaの検索結果まで表示されます)
いろいろな面でポップアップ辞書の仕様が変更されています。
個人的には、複数の辞書が一覧でポップアップされるようになった利便性が嬉しいので、満足しています。

> 自分の環境のSafari5.1.7の最新版では急に連続ポップアップできなくなりました。

そうですよね、昔はSafariの本文でも連続ポップアップできた気がしていたので、
今回10.6.8で試してSafariの本文で連続ポップアップができなかったことに違和感を感じていたのでした。
昔はできたと確認できて、スッキリしました。

ポップアップ辞書のカスタマイズについては、初めて知りました。
なるほど、これを弄れば何かできそうな予感がしますね。
機能としてプップアップ辞書があるのだから、10.8でもどこかにあるはず、と思っています。
(辞書なのだから、辞書.appの中とか怪しい気がします。まだ探してませんが)

ひろしひろし 2012/11/16 22:04 段落や単語を次々と選択してくれる
キーバインドを考えてみました。

文字の選択状態を解除するのは
"cancelOperation:"でよいのだろうか。
そもそも解除する必要がないのかもしれない。
あとカーソルの右側の**単語を選択するのに
"selectWord:"を使っています。

1段落の選択切り替え
"?????" = (***→進む方向
"cancelOperation:",   **キャンセル?
"moveToEndOfParagraph:", **カーソルを段落の末尾に移動する
"moveForward:",      **カーソルを進む方向に一つ移動する
"moveToBeginningOfParagraph:",**カーソルを段落の先頭に移動する
"selectParagraph:", **段落を選択する
);
"?????" = (***←戻る方向
"cancelOperation:",
"moveToBeginningOfParagraph:", **カーソルを段落の先頭に移動する
"moveBackward:", **カーソルを戻る方向に一つ移動する
"moveToBeginningOfParagraph:",
"selectParagraph:",
);
2単語の選択切り替え
"?????" = (***→進む方向
"cancelOperation:",   **キャンセル?
"moveWordForward:", **カーソルを単語の末尾に移動する
"selectWord:", **カーソルの右側を選択する??
);
"?????" = (***←戻る方向
"cancelOperation:",   **キャンセル?
"moveWordBackward:", **カーソルを単語の先頭に移動する
"moveBackward:", **カーソルを戻る方向に一つ移動する
"selectWord:",
);
"?????" = (***←戻る方向**別パターン
"cancelOperation:",   **キャンセル?
"moveWordBackward:", **カーソルを単語の先頭に移動する
"moveWordBackward:", **カーソルを単語の先頭に移動する
"selectWord:",
);

zariganitoshzariganitosh 2012/11/17 05:52 どうやら壮大な勘違いをしていたようです。(笑)
最初のコメントで目指していたのは、こうゆうことだったんですね。
てっきりNSRespoderクラスというキーワードから、Cocoaアプリケーションを作りたいのかと思ってしまいました。
「ひろしさんは、なんて壮大な計画を考えるんだろうと、しかも、2年前のブログのコメントで...。」
そんなふうに思って返信していたものだから、どうも話が噛み合ない訳です。(笑)

素晴らしいキーバインドだと思います!
ちなみに、最終的にどのような状況で活用して、どんな利便性があるのか、なるべく具体的に教えていただけませんか?
ひろしさんの発想は、私の感覚を超えていることが多いので、
直近の目的だけ見ていては、いつも最後に何が出来上がっているのか謎なのです。

最終成果物を作るまでの過程と、その使い方を公開したら、きっと面白いブログになると思いますよ。

ひろしひろし 2012/11/18 00:34 単語や段落の選択の切り替えに関しては
具体的に使用する目的が決まっているわけではないのですが
**選択中の文字列はサービスメニューに渡せます。
さらにサービスメニューからAppleScriptに引数として
**選択中の文字列を渡せますのでいろいろな可能性があると思います。

具体的には
1辞書を連続ポップアップで表示する。
これを一番やってみたい。
辞書.app用の辞書はオリジナルのものを作ることができ、
イメージも埋め込むこともできるので
それを使えば連続してその単語のイメージを表示させることも可能になるかと思います。
(ただ、MacOSX10.8ではこの辞書ポップアップの機能の関連ファイルが
 どこにあるかわからないので今のところ**文字列だけの表示になってしまいます。)

そのイメージは外部から読み込むのではなく
辞書パッケージ内に収められていて
その画像はFinderで表示させることもクイックルックさせることもできます。
何が言いたいかというと
単語の中に埋め込みたいファイルの**相対リンクを張っておくと
Finderで表示させることできる。
データベースの各項目にファイルを埋め込みパッケージ化する的な
使い方もできます。(**ポップアップには関係ないですが)

2『大きな文字で表示』
アドレスブックのように
文字の大きさが可変で画面の中央に表示するのは良い方法ではないですが
次々と**段落の文字列を大きく表示してくれたら
文字を読むときに気持ちいいかなと思います。
さらにOSX10.7からは日本語スピーチ機能がありますので
それと組み合わせてもおもしろいと思います。

その他に
3文字列の検索対象に使う
4読み上げ
5スピーチを録音
6ことえり単語登録
7Googleで検索
8文字列の編集加工などに使えます。

利便性は確実にあると思います。
もし同じことをマウスでしようと思ったら
何度もマウスカーソルを移動して
ダブルクリックあるいはトリプルクリックをし続けなくてはいけません。

ただ文字カーソルという概念があるかどうかよくわからない
Safariなどのアプリでは
単語や段落の選択の切り替えはうまく機能するだろうか。
クリックした位置に隠れ文字カーソルなるものが作られているのだろうか。

最初のコメントでやりたかったことを説明します。
(**言葉足らずで申し訳ありません)
1仮に大きく表示するウインドウを
(背景は黒の半透明で文字は白、行数は1文字数17)
(ウインドウサイズは段落の文字数によって可変しスクリーンの中央揃え/上下は自由)とします。
2例えば段落の文字数が37文字の場合
 カーソル位置が段落の先頭(0とする)にある場合は
段落内の1文字目から17文字目を表示する。(**カーソルの位置は0
 カーソル位置が段落の先頭から4文字目の右(4)にある場合は
段落内の1文字目から17文字目を表示する。(**カーソルの位置は4
 カーソル位置が段落の先頭から25文字目の右(25)にある場合は
段落内の17文字目から34文字目を表示する。(**カーソルの位置は9**中央

段落の先頭を0とすると
0文字目から8文字目はカーソルの位置は0から8
 **9文字目から28文字目のカーソル位置は9**中央)
29文字目から37文字目のカーソル位置は10から17**

3カーソルの位置が変更されるたびに表示する文字列を更新する。

(**カーソルを表示することができた場合の大きく表示する側のカーソル位置です。)

1つ質問があります。
このホームページのNSRespoderクラスのアクションを
applescriptで使いたいのですが何か方法があれば教えてください。

まだキーバインドは試していません。ごめんなさい。

zariganitoshzariganitosh 2012/11/19 06:12 まず、質問に対する考えです。
> このホームページのNSRespoderクラスのアクションを
> applescriptで使いたいのですが何か方法があれば教えてください。

目的のNSRespoderクラスのアクションに対応したキー操作を、
AppleScriptのGUIスクリプティングで操作すれば良さそうな気がしますが、いかがでしょう。

それから、詳細な説明ありがとうございます。
ひろしさんが、どんなことを目指しているのか、少しわかりました。
その上で、もし実用的に使うことが目的なら、自分だったら以下のようにします。
(10.8の環境で考えています)

1連続ポップアップ
  3本指タップで代用
2大きな文字で表示
  ・アクセシビリティのズーム機能で代用
  ・2本指タップのズームで代用
  ・Retina環境なら解像度を変更して代用
  ・確か以前、字幕リーダーのようなテキストビューアーがあったような気がします。そうゆうものを探して、代用
3文字列の検索対象に使う
  command-E、command-Fの連続ショートカットはいかがですか?
4読み上げ
  OSX10.8の標準機能でoption-escで選択された文字列を読み上げてくれます。
5スピーチを録音
  sayコマンドでどうぞ。
  say -o ~/Desktop/out "こんにちは"
6ことえり単語登録
7Googleで検索
8文字列の編集加工などに使えます。
  文字列が選択さえされていれば、AppleScriptで何でもできる訳ですから、
  目的のスクリプトを書いておけばよいと思います。

一方、いろいろと想像するのは利便性だけが目的とは限りません。
AppleScriptで操作することが面白かったり、その可能性を追求するとワクワクします。
コメントの一番最初の質問に戻って考えれば、以下のキー操作を使うのはいかがですか?
shift-option-→ or ←  =単語の選択
shift-command-→ or ←  =段落の選択
以上の操作はキーバインドの設定でももちろん可能です。
そして、この日記本文の「アクションの複合技(タグ化)」の項目を見て頂ければ、
キーバインドによって、カーソル位置のコントロールもある程度できると思います。

ひろしひろし 2012/11/19 22:48 どうしてこのような質問をしたかというと
キーバインドの
ショートカットの優先順位は他と比べて低いと
どこかで書かれていた記憶があって
条件によっては動かない場合がある可能性を無くしたかったのと

『Objective-Cクラスの中の**メソッドや**アクションを
Applescriptから呼び出す』には
どうすればよいのかという技術的な興味があったからです。
AppleScriptからメソッドを呼び出す call method? (不安定?)
Applescriptの中のruby_cocoaから**を呼び出す?
推察するにたぶんこんなかんじですかね。

話は少しそれますが
自分で作ったAppleScriptはどのように実行されていますか。
Quicksliverでショートカットを割り当てて呼び出していると
ブログに書いてありましたね。
Quicksliverは確かに高速にApplescriptを実行してくれます。
普通これが正解だと思います。
自分は次のような方法でApplescriptにショートカットを
割り当てています。

1最初にテキストエディタで次のように書きます。
**(osascript スクリプトのあるアドレス)
osascript ~/Downloads/選択部分へジャンプテスト用のコピー.scpt⏎
2ファイル名を『選択部分へジャンプテスト用のコピー.command』として書き出します。
**(ファイルの種類はターミナル・シェル・スクリプトとなります。)
3ターミナルで2のファイルにchmod +x で実行権限を付加します。
chmod +x ~/Downloads/選択部分へジャンプテスト用のコピー.command⏎
**(.commandファイルのあるアドレス)
4sparkというアプリケーションでショートカットを割り当てます。
1ウインドウの左上の歯車(ショートカットの追加ボタンです)から**Documentを選びます。
2ドロワーが表示されます
1Shortcut:
2Name:**分かりやすい名前を自由に付けられます。(日本語OK)
3Actionメニュー:**Open File/Folder
4Choose..ボタン**3の.commandファイルを指定します。
以上でおわりです。

今回のHotkey Typeは**Documentです。
**applescriptもありますがこれは実行までの速度が遅く使えません。
ほかに**Text/keyboardがあります。
keyboardタブでは複数のキーストロークを設定できます。
(数が多くなる場合はドロワーのウインドウサイズをドラッグして大きくしてから入力してください。)

sparkを使う利点は
登録してあるショートカットを分かりやすく表示してくれるところです。
**すべてのアプリケーションと**ある特定のアプリケーションのみの
ショートカットをそれぞれ設定できますが
その表示のされかたが最高なのです。
実行までの速度はQuicksliverと比べてどうかは測りようがないので
なんともいえないのですが
自分はこれで満足しています。
spark3b9
http://www.shadowlab.org/softwares/spark.php

もしよろしければ試してみてください。

zariganitoshzariganitosh 2012/11/20 05:30 > **applescriptもありますがこれは実行までの速度が遅く使えません。

そんなに遅いのかと試してみましたが、Quicksilverと同じくらい快適に使えましたよ。
  Spark Version 3.0b9 (461)
  初代MacBook OSX 10.6.8
登録したショートカットは一つだけですが。

以下、自分の使い分けです。
- ショートカットは忘れてしまってはまったく意味がないので、よく使うものだけ登録する。
- あまり使わないけど、たまに素早く呼び出したいものは、Automatorでサービスとして登録する。
-- こうしておけば、二本指タップ一発で必要なサービスを呼び出せます。
-- メニューに表示されるのでショートカットを覚える必要もありません。

ショートカット割り当てには、どのような環境を使っても良いと思います。
自分は以前からQuicksilverを快適に使っているので、そのままありがたく使わせて頂いています。
しかし、わざわざosascriptを使って、シェルスクリプトにして、実行権限を割り当てて、書類を開くという方法で呼び出すのは、なんだか壮大な遠回りのような気がします。
自分の環境では素早く起動したので、一度ゲスト環境で起動するなどして、クリーンな環境で起動が本当に遅いのかどうか、試してみた方が良いかもしれません。

ひろしひろし 2012/11/20 20:39 失礼しました。
一番最初のApplescriptの実行までの速度は遅くはありませんね。
ただ連続してショートカットを入力して実行した場合に
2回目以降 思いどおりに動かない**ものがありました。

確かにめんどくさいですね。

ただsparkの利便性とApplescriptの安定性と実行速度の
両方を満たす選択肢を考えると
いまのところこのようにせざるおえませんでした。
ほかにsparkを使ってApplescriptを実行できる方法があればよいですが。

zariganitoshzariganitosh 2012/11/21 05:34 > ただ連続してショートカットを入力して実行した場合に
> 2回目以降 思いどおりに動かない**ものがありました。

その後、自分の環境ではWindowを操作するAppleScriptを4つ登録してみましたが、
上下左右にウィンドウが軽快に動いてくれます。
ショートカットの連打しまくりです。
また、リピート設定を100msにして押し続けてもOKでした。

ひろしひろし 2012/11/21 20:16 試してみてくださったことに感謝します。

自分が試してみてうまく動かなかったのは
以前お世話になった
ruby_cocoaを使った**move_mouseハンドラと**cliclikを
含むApplescriptです。

Quicksliverやシェルスクリプトを使ったものは
同じ内容でまったく問題ないのに
なぜ何でしょうかね。

zariganitoshzariganitosh 2012/11/22 06:34 どうしよう、RubyCocoaのmove_mouseハンドラを含むスクリプトも動いてしまった...。
もう少し複雑な操作にすれば動かなくなるのかな?と思って、修正したスクリプトをショートカットで起動しようとして、動かない!(喜びました)
そして気付きました。
あれ?修正したはずなのに修正前の動作になってる。
つまり、Sparkはファイルパスを登録しているのではなく、AppleScriptコードそのものを登録しているのでは?
修正後にもう一度Sparkで同じAppleScriptファイルを指定してアップデートすれば、修正後のAppleScriptもちゃんと動きました。

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証