1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# Assumes ruby class is only one in file that calls ib_outlet / ib_action

require 'osx/cocoa'
include OSX

$outlets = []
$actions = []

class OSX::NSObject
  class << self
    def ns_outlets(*args)
      args.each do |arg|
        puts "found outlet #{arg}"
        $outlets << arg
      end
    end
  
    alias_method :ns_outlet,  :ns_outlets
    alias_method :ib_outlet,  :ns_outlets
    alias_method :ib_outlets, :ns_outlets

    def ns_actions(*args)
      args.each do |arg|
        puts "found action #{arg}"
        $actions << arg
      end
    end
  
    alias_method :ns_action,  :ns_actions
    alias_method :ib_action,  :ns_actions
    alias_method :ib_actions, :ns_actions
  end
end

class ClassesNibUpdater
  def self.update_nib(nib_path, ruby_class, ruby_file)
    updater = new
    plist = updater.parse_plist(nib_path)
    ruby_class_plist = updater.find_ruby_class(plist, ruby_class)
    updater.find_outlets_and_actions(ruby_file)
    updater.update_superclass(ruby_class, ruby_class_plist)
    updater.add_outlets_and_actions_to_plist(ruby_class_plist)
    updater.write_plist_data(nib_path, plist)
  end
  
  def parse_plist(nib_path)
    puts "Parsing: #{nib_path}/classes.nib"
    plist_data = NSData.alloc.initWithContentsOfFile("#{nib_path}/classes.nib")
    plist, format = NSPropertyListSerialization.propertyListFromData_mutabilityOption_format_errorDescription(plist_data, NSPropertyListMutableContainersAndLeaves)
    plist
  end
  
  def find_ruby_class(plist, ruby_class)
    puts "Looking for #{ruby_class} in plist"
    # be nice if NSDictionary had the same methods as hash
    ruby_class_plist = nil
    plist['IBClasses'].each do |klass|
      next unless klass['CLASS'].to_s == ruby_class
      ruby_class_plist = klass
    end
    if ruby_class_plist.nil?
      puts "Didn't find #{ruby_class} in plist, creating dictionary"
      # didn't find one, create a new one
      ruby_class_plist = NSMutableDictionary.alloc.init
      ruby_class_plist['CLASS'] = ruby_class
      ruby_class_plist['LANGUAGE'] = 'ObjC' # Hopefully one day we can put Ruby here :)
      plist['IBClasses'].addObject(ruby_class_plist)
    end
    ruby_class_plist
  end
  
  # we've taken over ns_outlets and ns_actions above, so just requiring the
  # class will cause it to be parsed an the methods to be called so we can get
  # at them
  def find_outlets_and_actions(ruby_file)
    puts "Getting outlets and actions"
    require ruby_file
  end
  
  def update_superclass(ruby_class, ruby_class_plist)
    klass = ruby_class.split("::").inject(Object) { |par, const| par.const_get(const) }
    superklass = klass.superclass.to_s.sub(/OSX::/, '')
    ruby_class_plist.setObject_forKey(superklass, "SUPERCLASS")
  end
  
  def add_outlets_and_actions_to_plist(ruby_class_plist)
    puts "Adding outlets and actions to plist"
    
    updated_outlets = NSMutableDictionary.dictionary
    $outlets.each { |outlet| updated_outlets.setObject_forKey('id', outlet) }
    ruby_class_plist['OUTLETS'] = updated_outlets
    
    updated_actions = NSMutableDictionary.dictionary
    $actions.each { |action| updated_actions.setObject_forKey('id', action) }
    ruby_class_plist['ACTIONS'] = updated_actions
  end
  
  def write_plist_data(nib_path, plist)
    puts "Writing updated classes.nib plist back to file"
    File.open("#{nib_path}/classes.nib", "w+") do |file|
      file.write plist
    end
  end
end

require 'optparse'
class Options
  def self.parse(args)
    options = {}
    opts = OptionParser.new do |opts|
      opts.banner = "Usage: ruby -r <extra libs you require> update_classes_nib.rb [options]"
      opts.on("-c", "--class NAME", "Name of Ruby class") do |klass|
        options[:class] = klass == "" ? nil : klass
      end
      opts.on("-f", "--file PATH", "Path to file containing Ruby class",
                                   "Defaults to lib/<class>.rb") do |file|
        options[:file] = file == "" ? nil : file
      end
      opts.on("-n", "--nib PATH", "Path to .nib to update") do |nib|
        options[:nib] = nib == "" ? nil : nib
      end
      opts.on_tail("-h", "--help", "Show this message") do
        puts opts
        exit
      end  
    end
    opts.parse!(args)
    if options[:class].nil? || options[:nib].nil?
      puts "Must supply the ruby class and the nib paths"
      puts opts
      exit
    end
    options[:file] = "lib/#{options[:class]}.rb" if options[:file].nil?
    options
  end
end

options = Options.parse(ARGV)
ClassesNibUpdater.update_nib(options[:nib], options[:class], options[:file])