1require 'osx/cocoa'
2include OSX
3
4OSX.require_framework 'PreferencePanes'
5OSX.load_bridge_support_file File.expand_path('../Security.bridgesupport', __FILE__)
6
7require File.expand_path('../passenger_pane_config', __FILE__)
8require File.expand_path('../shared_passenger_behaviour', __FILE__)
9require File.expand_path('../PassengerApplication', __FILE__)
10
11if RUBY_VERSION == "1.8.7" && OSX::RUBYCOCOA_VERSION == "0.13.2"
12  class OSX::NSArray
13    def count
14      oc_count
15    end
16  end
17end
18
19class PrefPanePassenger < NSPreferencePane
20  class << self
21    attr_accessor :sharedInstance
22  end
23  
24  include SharedPassengerBehaviour
25  
26  ib_outlet :installPassengerWarning
27  ib_outlet :authorizationView
28  ib_outlet :applicationsTableView
29  ib_outlet :applicationsController
30  
31  kvc_accessor :applications, :authorized, :dirty_apps, :revertable_apps
32  
33  def mainViewDidLoad
34    self.class.sharedInstance = self
35    setup_authorization_view!
36    setup_applications_table_view!
37    
38    OSX::NSNotificationCenter.defaultCenter.objc_send(
39      :addObserver, self,
40         :selector, 'paneWillBecomeActive:',
41             :name, OSX::NSApplicationWillBecomeActiveNotification,
42           :object, nil
43    )
44  end
45  
46  def paneWillBecomeActive(notification = nil)
47    willSelect
48  end
49  
50  def willSelect
51    @dropping_directories = @dirty_apps = @revertable_apps = false
52    setup_passenger_warning!
53    @applicationsController.content.empty? ? load_appications! : reload_appications!
54  end
55  
56  def applicationMarkedDirty(app)
57    self.revertable_apps = @applicationsController.content.any? { |app| app.revertable? }
58    self.dirty_apps = true
59  end
60  
61  def apply(sender = nil)
62    if authorize!
63      @applicationsController.content.each { |app| app.apply if app.dirty? }
64      self.dirty_apps = self.revertable_apps = false
65    else
66      p "Unable to #{action} because authorization failed."
67    end
68  end
69  
70  def revert(sender = nil)
71    @applicationsController.content.each { |app| app.revert if app.revertable? }
72    self.dirty_apps = self.revertable_apps = false
73  end
74  
75  def restart(sender = nil)
76    @applicationsController.content.each { |app| app.restart unless app.new_app? }
77  end
78  
79  def remove(sender = nil)
80    apps = @applicationsController.selectedObjects
81    existing_apps = apps.reject { |app| app.new_app? }
82    PassengerApplication.removeApplications(existing_apps) unless existing_apps.empty?
83    @applicationsController.removeObjects apps
84  end
85  
86  def rbSetValue_forKey(value, key)
87    super
88    browse if !@dropping_directories and key == 'applications' and !value.empty? and value.last.new_app?
89  end
90  
91  def showPassengerHelp(sender)
92    OSX::HelpHelper.openHelpPage File.expand_path('../English.lproj/PassengerPaneHelp/PassengerPaneHelp.html', __FILE__)
93  end
94  
95  # Select application directory panel
96  
97  def browse(sender = nil)
98    panel = NSOpenPanel.openPanel
99    panel.canChooseDirectories = true
100    panel.canChooseFiles = false
101    panel.objc_send(
102      :beginSheetForDirectory, path_for_browser,
103      :file, nil,
104      :types, nil,
105      :modalForWindow, mainView.window,
106      :modalDelegate, self,
107      :didEndSelector, 'openPanelDidEnd:returnCode:contextInfo:',
108      :contextInfo, nil
109    )
110  end
111  
112  def openPanelDidEnd_returnCode_contextInfo(panel, button, contextInfo)
113    app = @applicationsController.selectedObjects.first
114    if button == OSX::NSOKButton
115      app.setValue_forKey(panel.filename, 'path')
116    else
117      remove if app.new_app? and !app.dirty?
118    end
119  end
120  
121  # Applications NSTableView dataSource drag and drop methods
122  
123  def tableView_validateDrop_proposedRow_proposedDropOperation(tableView, info, row, operation)
124    return OSX::NSDragOperationNone unless @authorized
125    
126    files = info.draggingPasteboard.propertyListForType(OSX::NSFilenamesPboardType)
127    if files.all? { |f| File.directory? f }
128      @applicationsTableView.setDropRow_dropOperation(@applicationsController.content.count, OSX::NSTableViewDropAbove)
129      OSX::NSDragOperationGeneric
130    else
131      OSX::NSDragOperationNone
132    end
133  end
134  
135  def tableView_acceptDrop_row_dropOperation(tableView, info, row, operation)
136    apps = info.draggingPasteboard.propertyListForType(OSX::NSFilenamesPboardType).map { |path| PassengerApplication.alloc.initWithPath(path) }
137    @dropping_directories = true
138    @applicationsController.addObjects apps
139    @dropping_directories = false
140  end
141  
142  def tableView_writeRowsWithIndexes_toPasteboard(tableView, rows, pboard)
143    config_paths = @applicationsController.content.objectsAtIndexes(rows).map { |app| app.config_path }
144    pboard.declareTypes_owner([OSX::NSFilenamesPboardType], self)
145    pboard.setPropertyList_forType(config_paths, OSX::NSFilenamesPboardType)
146    true
147  end
148  
149  # SFAuthorizationView: TODO this should actualy move to the SecurityHelper, but for some reason in prototyping it didn't work, try again when everything is cleaned up.
150  
151  def authorizationViewDidAuthorize(authorizationView = nil)
152    OSX::SecurityHelper.sharedInstance.authorizationRef = @authorizationView.authorization.authorizationRef
153    self.authorized = true
154  end
155  
156  def authorizationViewDidDeauthorize(authorizationView = nil)
157    OSX::SecurityHelper.sharedInstance.deauthorize
158    self.authorized = false
159  end
160  
161  # When the pane wants to be unselected
162  
163  def shouldUnselect
164    if @dirty_apps and !@applicationsController.content.empty?
165      alert = OSX::NSAlert.alloc.init
166      alert.messageText = 'This service has unsaved changes'
167      alert.informativeText = 'Would you like to apply your changes before closing the Passenger preference pane?'
168      alert.addButtonWithTitle 'Apply'
169      alert.addButtonWithTitle 'Cancel'
170      alert.addButtonWithTitle 'Don���t Apply'
171      alert.objc_send(
172        :beginSheetModalForWindow, mainView.window,
173        :modalDelegate, self,
174        :didEndSelector, 'unsavedChangesAlertDidEnd:returnCode:contextInfo:',
175        :contextInfo, nil
176      )
177      return OSX::NSUnselectLater
178    end
179    OSX::NSUnselectNow
180  end
181  
182  APPLY = OSX::NSAlertFirstButtonReturn
183  CANCEL = OSX::NSAlertSecondButtonReturn
184  DONT_APPLY = OSX::NSAlertThirdButtonReturn
185  
186  def unsavedChangesAlertDidEnd_returnCode_contextInfo(alert, returnCode, contextInfo)
187    alert.window.orderOut(self)
188    case returnCode
189    when CANCEL
190      replyToShouldUnselect false
191      return
192    when APPLY
193      apply
194    when DONT_APPLY
195      @applicationsController.removeObjects @applicationsController.content.select { |app| app.new_app? }
196      revert
197    end
198    replyToShouldUnselect true
199  end
200  
201  private
202  
203  def authorize!
204    result = @authorizationView.authorization.objc_send(
205      :permitWithRight, OSX::KAuthorizationRightExecute,
206      :flags, (OSX::KAuthorizationFlagPreAuthorize | OSX::KAuthorizationFlagExtendRights | OSX::KAuthorizationFlagInteractionAllowed)
207    ) == 0
208    authorizationViewDidAuthorize if result
209    result
210  end
211  
212  def setup_authorization_view!
213    @authorized = false
214    @authorizationView.string = OSX::KAuthorizationRightExecute
215    @authorizationView.delegate = self
216    @authorizationView.updateStatus self
217    @authorizationView.autoupdate = true
218  end
219  
220  def setup_applications_table_view!
221    @applications = [].to_ns
222    @applicationsTableView.dataSource = self
223    @applicationsTableView.registerForDraggedTypes [OSX::NSFilenamesPboardType]
224    @applicationsTableView.setDraggingSourceOperationMask_forLocal(OSX::NSDragOperationGeneric, false)
225  end
226  
227  def load_appications!
228    unless (existing_apps = PassengerApplication.existingApplications).empty?
229      @applicationsController.addObjects existing_apps
230      @applicationsController.selectedObjects = [existing_apps.last]
231    end
232  end
233  
234  def reload_appications!
235    @applicationsController.content.each { |app| app.reload! }
236  end
237  
238  def passenger_installed?
239    `#{PassengerPaneConfig::HTTPD_BIN} -t -D DUMP_MODULES 2>&1`.include? 'passenger_module'
240  end
241  
242  def path_for_browser
243    app = @applicationsController.selectedObjects.first
244    app.nil? ? OSX.NSHomeDirectory : app.path
245  end
246  
247  MODRAILS_URL = 'http://www.modrails.com'
248  def setup_passenger_warning!
249    if passenger_installed?
250      @installPassengerWarning.hidden = true
251    else
252      unless @setup_passenger_warning
253        text_field = @installPassengerWarning.subviews.first
254        
255        link_str = OSX::NSMutableAttributedString.alloc.initWithString(MODRAILS_URL)
256        range = OSX::NSMakeRange(0, MODRAILS_URL.length)
257        link_str.addAttribute_value_range OSX::NSLinkAttributeName, MODRAILS_URL, range
258        link_str.addAttribute_value_range OSX::NSForegroundColorAttributeName, OSX::NSColor.blueColor, range
259        link_str.addAttribute_value_range OSX::NSUnderlineStyleAttributeName, OSX::NSSingleUnderlineStyle, range
260        
261        text_parts = text_field.stringValue.split(MODRAILS_URL)
262        
263        str = OSX::NSMutableAttributedString.alloc.initWithString(text_parts.first)
264        str.appendAttributedString link_str
265        str.appendAttributedString OSX::NSAttributedString.alloc.initWithString(text_parts.last)
266        str.addAttribute_value_range OSX::NSFontAttributeName, OSX::NSFont.systemFontOfSize(11), OSX::NSMakeRange(0, str.length)
267        
268        text_field.attributedStringValue = str
269        @setup_passenger_warning = true
270      end
271      
272      @installPassengerWarning.hidden = false
273    end
274  end
275end