アクティブでないウインドウからドラッグ&ドロップを開始する

昨日、BathyScaphe のプレビューインスペクタにドラッグ&ドロップ機能を追加する作業を行っていたときのことを今日はメモしておく。
まず、今回実装したドラッグ&ドロップとは、

  1. イメージをドラッグすると...
  2. ...そのイメージのファイルがドロップされる

というものだ。このような場合は、あまり面倒なことを考えなくても NSView の次のメソッドを使えば良い:

- (BOOL)dragFile:(NSString *)fullPath fromRect:(NSRect)aRect slideBack:(BOOL)slideBack event:(NSEvent *)theEvent


これを mouseDown: で呼び出してやるだけ。いやぁ、ドラッグ&ドロップって実装が面倒そうで先延ばしにしてたけど、案外ドンピシャなメソッドがあるんだねぇ、とウキウキしながら早速動作試験をした。


ところが、なぜかドラッグができない。「???」ムキになって何度かクリック、ドラッグ、クリック、…と繰り返していると、突然ドラッグが始まったりする有様だ。一体どうなっているのか?


しばらく観察した末、気付いたのは「アクティブでない*1ウインドウからドラッグしようとしている」ことだった。つまり、アクティブでないウインドウ上では、最初のクリックがまず、そのウインドウをアクティブにする作業に奪われてしまうので、ドラッグ&ドロップを開始しようとしてもうまくいかないのだ。一度クリックしてアクティブになった後、再びマウスボタンを押すことで mouseDown: が捕まり、ドラッグが開始されるという訳だったのである。
しかし、である。私は他のアプリケーションでそんな不自由な挙動など見たことが無い。例えば、カラーパネルから色をドラッグすることを思い出してみればいい。カラーパネルは全くキーウインドウになっていないまま、色をスーイスイとドラッグ&ドロップ開始できるじゃないか。


そこでどうしたらいいのかしばらく探した末、見つけてきたのが同じ NSView のこのメソッドだった:

- (BOOL)acceptsFirstMouse:(NSEvent *)theEvent

Overridden by subclasses to return YES if the receiver should be sent a mouseDown: message for theEvent, an initial mouse-down event over the receiver in its window, NO if not. The receiver can either return a value unconditionally, or use the location of theEvent to determine whether or not it wants the event. NSView’s implementation ignores theEvent and returns NO.
Override this method in a subclass to allow instances to respond to initial mouse-down events. For example, most view objects refuse an initial mouse-down event, so the event simply activates the window. Many control objects, however, such as NSButton and NSSlider, do accept them, so the user can immediately manipulate the control without having to release the mouse button.

説明を読むと、正にこのメソッドが今回のキモであることが分かるだろう。ドラッグを開始したいビューで*2、この acceptsFirstMouse: に YES を返しておくことで、ウインドウがアクティブでない状態からでもすぐにドラッグ&ドロップを開始できる。こうして、無事ドラッグ&ドロップ機能が実装されたのだった。


…と、ここまで書いてみたが、割と開発者にとっては常識で、今更何言ってんの、この女子校生、って感じかもしれないし、そうでないかもしれないね。よくわからん。とにかく、もし、誰かが現在そして未来、この覚え書きを参考にすることがあるかもしれないという期待によってのみ、このトピックを残しておく次第。…BathyScapheWiki の方に書いといた方がよかったかも。

*1:より正確には、キーウインドウでない

*2:正確には、ドラッグを開始したいビューをサブクラス化して、そのサブクラスで