tokuhirom's Blog

MacRuby でメニューバーのステータスメニューに常駐するアプリを作るための雛形をつくりました!

MacRuby でささっとメニューバーのステータスメニューに常駐するアプリケーションをつくれたら便利だと思いませんか?

メニューバーのステータスメニューとは↓のような領域のことです。

これができると daemontools で常駐とかさせなくても app を start up にいれるだけでいいし、管理が楽です。

この分野では mynu というライブラリが有名なのだが、これはコードが複雑でとりあつかいづらい上に、ドキュメントをよんでも macruby コマンドで走らせようという話しかでてこなくて不安になる。僕がほしいのは .app だ! という気持になるのだ。

というわけで、通常のアプリ開発フローでステータスバーに常駐するアプリをつくるための雛形を作成した。以下のコードを Xcode で生成した雛形の AppDelegate.rb におきかえ、適当な画像(たとえば このへん など)を icon.png としてプロジェクトに追加するだけで OK。

ステータスバーアプリとして必要そうなコードのサンプルはうめこんであります。NSMenuItem を直でいじるとコールバックの登録がめんどうなので DSL っぽくかけるようにしてありますが、instance_eval してるだけなので誰でもよめるコードになっています。

ステータスバーに常駐するアプリだと、ごちゃごちゃ覚えなくても簡単な ruby アプリを常駐させられるし、配布できるので便利です。webrick のアプリを background でうごかして常駐させるとか、そういうのも簡単にできますし、cron っぽく定期的になんかさせる、みたいなのもできますね。まあそういうの daemontools とか launchctl とか cron でやってもいいんですけど、osx っぽくできるし配布しやすいのでよさそうな気がします。

実際のコードは以下のとおりです。

#
#  AppDelegate.rb
#  StatusBarSample
#
#  Created by Tokuhiro Matsuno on 1/4/13.
#  Copyright 2013 Tokuhiro Matsuno. All rights reserved.
#

class MenuHelper
    attr_reader :menu
    
    
    def self.create(title, &block)
        obj = self.new(title)
        obj.instance_eval &block
        return obj.menu
    end

    def initialize(title)
        @menu = NSMenu.new()
        @menu.initWithTitle('FooApp')
        @menu.autoenablesItems = false
    end

    def item(title, &block)
        item = NSMenuItem.new()
        item.title  = title

        # Create anonnymous class to receive event.
        receiver = Class.new()
        receiver.class_eval do
            define_method(:call) do |sender|
                block.(sender)
             end
        end
        item.action = 'call:'
        item.target = receiver.new

        @menu.addItem item
        
        return item
    end

    def nest(title, &block)
        item = NSMenuItem.new()
        item.title  = title

        submenu = MenuHelper.new(title)
        item.submenu = submenu.menu
        submenu.instance_eval &block
        
        @menu.addItem item
    end

    def separator
        @menu.addItem NSMenuItem.separatorItem
    end
end

class AppDelegate
    attr_accessor :window

    def setupMenu
        MenuHelper.create("FooApp") do
            item 'NSAlertMenu' do |sender|
                alert = NSAlert.new
                alert.messageText = 'This is MacRuby Status Bar Application'
                alert.informativeText = 'Cool, huh?'
                alert.alertStyle = NSInformationalAlertStyle
                alert.addButtonWithTitle("Yeah!")
                response = alert.runModal
            end
            
            item 'Open URL' do |sender|
                url = NSURL.URLWithString("http://www.stackoverflow.com/");
                if !NSWorkspace.sharedWorkspace.openURL(url)
                    puts("Failed to open url: #{url.description}");
                end
            end

            nest 'Project' do
                task1 = item 'Task1' do |sender|
                    puts "Task1"
                    p sender
                end
                item 'Task2' do |sender|
                    task1.enabled = !task1.isEnabled
                end
                nest 'Task3' do
                    item 'Subtask 1' do
                        puts "Task3/Subtask 1"
                    end
                end
            end
            
            item 'Notification Center' do |sender|
                notify = NSUserNotification.alloc.init
                notify.title = 'Title of notification'
                notify.informativeText = 'Body of notification'
                NSUserNotificationCenter.defaultUserNotificationCenter.deliverNotification(notify)
            end
            
            separator

            item 'Quit' do |sender|
                app = NSApplication.sharedApplication
                app.terminate(self)
            end
        end
    end
    
    def initStatusBar(menu)
        status_bar = NSStatusBar.systemStatusBar
        status_item = status_bar.statusItemWithLength(NSVariableStatusItemLength)
        status_item.setMenu menu
        img = NSImage.imageNamed 'icon.png'
        status_item.setImage(img)
        # status_item.title = "StatusBarSample!"
        status_item.highlightMode = true
    end
    
    def applicationDidFinishLaunching(a_notification)
        app = NSApplication.sharedApplication
        initStatusBar(setupMenu())
        
        puts "Your Application code here."
        
        app.run
    end
end

【追記】
s/ステータスバー/メニューバー/g thanks to kazuhooku

【追記】
s/メニューバー/メニューバーのステータスメニュー/g thanks to kazuhooku