25
28

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

メニューバーに常駐するmacOSアプリを作成し、画像をドラッグ・アンド・ドロップで認識できるようにする

Last updated at Posted at 2020-08-11

概要

  • Macの右上のメニューバーにアイコンを表示するアプリを作成します。
  • また最終的に、そのアイコン上に画像ファイルをドラッグ・アンド・ドロップできるよう実装を行います。
  • 上のバーの名称は以下メニューバーと呼ぶこととします。
image

GitHub

dragdemo

基本的な実装

class AppDelegate: NSObject, NSApplicationDelegate {
    
    let statusItem = NSStatusBar.system.statusItem(withLength:NSStatusItem.squareLength)
    
    func applicationDidFinishLaunching(_ aNotification: Notification) {
        
        if let statusBarButton = statusItem.button {
            statusBarButton = NSImage(named:NSImage.Name("StatusBarButtonImage"))
        }
        
        constructMenu()
    }
    
    @objc func printQuote(_ sender: Any?) {
        let quoteText = "Never put off until tomorrow what you can do the day after tomorrow."
        let quoteAuthor = "Mark Twain"
        
        print("\(quoteText)\(quoteAuthor)")
    }
    
    func constructMenu() {
        let menu = NSMenu()
        
        menu.addItem(NSMenuItem(title: "Print Quote", action: #selector(AppDelegate.printQuote(_:)), keyEquivalent: "P"))  // 大文字なので、Commant + "Shitf + p"となる
        menu.addItem(NSMenuItem.separator())
        menu.addItem(NSMenuItem(title: "Quit Quotes", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q"))
        
        statusItem.menu = menu
    }
}
  • これより以下は、記事の内容外の実装を紹介します。

メニュー項目にサブメニューを持たせる

  • 以下のようなメニューを入れ子にすることを考えます。

-w270

  • クラスとしては、下記のように入れ子になっています

-w667

func constructMenu() {
    let menu = NSMenu()
    menu.addItem(NSMenuItem(title: "Parent_01", action: #selector(AppDelegate.printQuote(_:)), keyEquivalent: ""))
    menu.addItem(NSMenuItem(title: "Parent_02", action: #selector(NSApplication.terminate(_:)), keyEquivalent: ""))
    menu.addItem(NSMenuItem.separator())
    
    // サブメニューを持つNSMenuItemを作成する
    let paren_03 = NSMenuItem(title: "Parent_03", action: #selector(AppDelegate.printQuote(_:)), keyEquivalent: "")
    let menuForParent = NSMenu()
    menuForParent.addItem(NSMenuItem(title: "Children_01", action: #selector(AppDelegate.printQuote(_:)), keyEquivalent: ""))
    menuForParent.addItem(NSMenuItem.separator())
    menuForParent.addItem(NSMenuItem(title: "Children_02", action: #selector(NSApplication.terminate(_:)), keyEquivalent: ""))
    paren_03.submenu = menuForParent
    
    menu.addItem(paren_03)
    
    statusItem.menu = menu
}

メニューアイコンにファイルをドラッグ・アンド・ドロップする

概要

  • 以下のようにメニューアイコンに画像ファイルをドラッグし、アプリ側で認識するよう実装を行います。

dragdemo

Conforming Types
NSCollectionView, NSView

実装

  • AppDelegate外でアイコンを変更するため、publicメソッドchangeMenubarIconImageを用意しておく。
    • Notificationを使うほうが疎でいいかも。
class AppDelegate: NSObject, NSApplicationDelegate {
    
    public let statusItem = NSStatusBar.system.draggableStatusItem(withLength: NSStatusItem.squareLength)
    
    public func changeMenubarIconImage(isDragging isDragIcon: Bool) {
        if let statusBarButton = statusItem.button {
            if isDragIcon {
                statusBarButton.image = NSImage(named:NSImage.Name("StatusBarButtonImageDragged"))
            } else {
                statusBarButton.image = NSImage(named:NSImage.Name("StatusBarButtonImage"))
            }
        }
    }
    
    public func receiveImageDraggedOnMenubarIcon(for urls: [URL]) {
        print("🍎URL Detected!")
        for url in urls {
            print(url)
        }
    }
    ...
extension NSStatusBar {
    
    func draggableStatusItem(withLength length: CGFloat) -> NSStatusItem {
        let statusItem = self.statusItem(withLength: length)
        
        if let button = statusItem.button {
            button.image = NSImage(named:NSImage.Name("StatusBarButtonImage"))
            button.registerForDraggedTypes([.fileURL, .tiff, .png, .URL, .string])
        }
        
        return statusItem
    }
}

extension NSStatusBarButton {
    
    
    // MARK: - Helper Methods
    
    private func filteringOptions() -> [NSPasteboard.ReadingOptionKey : Any] {
        return [.urlReadingContentsConformToTypes : NSImage.imageTypes]
    }
    
    private func shouldAllowDrag(_ draggingInfo: NSDraggingInfo) -> Bool {
        var canAccept = false
        let pasteBoard = draggingInfo.draggingPasteboard
        
        if pasteBoard.canReadObject(forClasses: [NSURL.self], options: filteringOptions()) ||
            pasteBoard.canReadObject(forClasses: [NSImage.self], options: filteringOptions()) {
            canAccept = true
        }
        
        return canAccept
    }
    
    private func setDraggingInfo(isDragging: Bool) {
        if let appDelegate = NSApplication.shared.delegate as? AppDelegate {
            appDelegate.changeMenubarIconImage(isDragging: isDragging)
        }
    }
    
    
    // MARK:- NSDraggingDestination Methods
    
    open override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
        print("draggingEntered")
        
        let allow = shouldAllowDrag(sender)
        setDraggingInfo(isDragging: allow)
        
        return allow ? .copy : NSDragOperation()
    }
    
    
    open override func draggingExited(_ sender: NSDraggingInfo?) {
        setDraggingInfo(isDragging: false)
    }
    
    
    open override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
        
        let pasteBoard = sender.draggingPasteboard
        
        // 今回はfileURLがドラッグされた場合のみ実装
        if let urls = pasteBoard.readObjects(forClasses: [NSURL.self], options: filteringOptions()) as? [URL],
            urls.count > 0,
            urls[0].isFileURL {
            
            if let appDelegate = NSApplication.shared.delegate as? AppDelegate {
                appDelegate.receiveImageDraggedOnMenubarIcon(for: urls)
            }
            
            return true
        }
        
        return false
    }
    
    
    open override func draggingEnded(_ sender: NSDraggingInfo) {
        setDraggingInfo(isDragging: false)
        print("draggingEnded")
    }
}

余談

  • registerForDraggedTypes with NSStatusBarButton?
    • NSStatusBarButtonにドラッグ・アンド・ドロップ可能なNSViewaddSubviewすればと回答
    • 試しましたが、アイコンの外観は変更はできましたが、ドラッグのアクションを受け取るのが不可でした。
      • Responder Chainあたりを考えるとうまくいくのだろうか…?

メニューバーのアイコンを非表示にする

  • isVisibleを設定等から設定できるようにすれば良さそうです。
statusItem.isVisible = false  // で非表示になる

ステータスメニューを実装する際にはそれを非表示にすることができるオプションを環境設定などに用意しておくべきです。

その他参考した記事

25
28
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
28

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?