1""" 2Instances of WSTConnectionWindowController are the controlling object 3for the document windows for the Web Services Tool application. 4 5Implements a standard toolbar. 6""" 7 8# Note about multi-threading. 9# Although WST does its network stuff in a background thread, with Python 2.2 10# there are still moments where the app appears to hang briefly. This should 11# only be noticable when your DNS is slow-ish. The hang is caused by the 12# socket.getaddrinfo() function, which is used (indirectly) when connecting 13# to a server, which is a frequent operation when using xmlrpclib (it makes 14# a new connection for each request). Up to (and including) version 2.3b1, 15# Python would not grant time to other threads while blocking inside 16# getaddrinfo(). This has been fixed *after* 2.3b1 was released. (jvr) 17 18from Cocoa import * 19 20from twisted.internet import defer 21from twisted.web.xmlrpc import Proxy 22 23import sys 24import types 25import string 26import traceback 27 28#from twisted.python import log 29#log.startLogging(sys.stdout) 30 31kWSTReloadContentsToolbarItemIdentifier = "WST: Reload Contents Toolbar Identifier" 32"""Identifier for 'reload contents' toolbar item.""" 33 34kWSTPreferencesToolbarItemIdentifier = "WST: Preferences Toolbar Identifier" 35"""Identifier for 'preferences' toolbar item.""" 36 37kWSTUrlTextFieldToolbarItemIdentifier = "WST: URL Textfield Toolbar Identifier" 38"""Idnetifier for URL text field toolbar item.""" 39 40def addToolbarItem(aController, anIdentifier, aLabel, aPaletteLabel, 41 aToolTip, aTarget, anAction, anItemContent, aMenu): 42 """ 43 Adds an freshly created item to the toolbar defined by 44 aController. Makes a number of assumptions about the 45 implementation of aController. It should be refactored into a 46 generically useful toolbar management untility. 47 """ 48 toolbarItem = NSToolbarItem.alloc().initWithItemIdentifier_(anIdentifier) 49 50 toolbarItem.setLabel_(aLabel) 51 toolbarItem.setPaletteLabel_(aPaletteLabel) 52 toolbarItem.setToolTip_(aToolTip) 53 toolbarItem.setTarget_(aTarget) 54 if anAction: 55 toolbarItem.setAction_(anAction) 56 57 if type(anItemContent) == NSImage: 58 toolbarItem.setImage_(anItemContent) 59 else: 60 toolbarItem.setView_(anItemContent) 61 bounds = anItemContent.bounds() 62 minSize = (100, bounds[1][1]) 63 maxSize = (1000, bounds[1][1]) 64 toolbarItem.setMinSize_( minSize ) 65 toolbarItem.setMaxSize_( maxSize ) 66 67 if aMenu: 68 menuItem = NSMenuItem.alloc().init() 69 menuItem.setSubmenu_(aMenu) 70 menuItem.setTitle_( aMenu.title() ) 71 toolbarItem.setMenuFormRepresentation_(menuItem) 72 73 aController._toolbarItems[anIdentifier] = toolbarItem 74 75class WSTConnectionWindowController (NSWindowController): 76 """ 77 As per the definition in the NIB file, 78 WSTConnectionWindowController is a subclass of 79 NSWindowController. It acts as a NSTableView data source and 80 implements a standard toolbar. 81 """ 82 methodDescriptionTextView = objc.IBOutlet() 83 methodsTable = objc.IBOutlet() 84 progressIndicator = objc.IBOutlet() 85 statusTextField = objc.IBOutlet() 86 urlTextField = objc.IBOutlet() 87 88 __slots__ = ('_toolbarItems', 89 '_toolbarDefaultItemIdentifiers', 90 '_toolbarAllowedItemIdentifiers', 91 '_methods', 92 '_methodSignatures', 93 '_methodDescriptions', 94 '_server', 95 '_methodPrefix',) 96 97 @classmethod 98 def connectionWindowController(self): 99 """ 100 Create and return a default connection window instance. 101 """ 102 return WSTConnectionWindowController.alloc().init() 103 104 def init(self): 105 """ 106 Designated initializer. 107 108 Returns self (as per ObjC designated initializer definition, 109 unlike Python's __init__() method). 110 """ 111 self = self.initWithWindowNibName_("WSTConnection") 112 113 self._toolbarItems = {} 114 self._toolbarDefaultItemIdentifiers = [] 115 self._toolbarAllowedItemIdentifiers = [] 116 117 self._methods = [] 118 return self 119 120 def awakeFromNib(self): 121 """ 122 Invoked when the NIB file is loaded. Initializes the various 123 UI widgets. 124 """ 125 self.retain() # balanced by autorelease() in windowWillClose_ 126 127 self.statusTextField.setStringValue_("No host specified.") 128 self.progressIndicator.setStyle_(NSProgressIndicatorSpinningStyle) 129 self.progressIndicator.setDisplayedWhenStopped_(False) 130 131 self.createToolbar() 132 133 def windowWillClose_(self, aNotification): 134 """ 135 Clean up when the document window is closed. 136 """ 137 self.autorelease() 138 139 def createToolbar(self): 140 """ 141 Creates and configures the toolbar to be used by the window. 142 """ 143 toolbar = NSToolbar.alloc().initWithIdentifier_("WST Connection Window") 144 toolbar.setDelegate_(self) 145 toolbar.setAllowsUserCustomization_(True) 146 toolbar.setAutosavesConfiguration_(True) 147 148 self.createToolbarItems() 149 150 self.window().setToolbar_(toolbar) 151 152 lastURL = NSUserDefaults.standardUserDefaults().stringForKey_("LastURL") 153 if lastURL and len(lastURL): 154 self.urlTextField.setStringValue_(lastURL) 155 156 def createToolbarItems(self): 157 """ 158 Creates all of the toolbar items that can be made available in 159 the toolbar. The actual set of available toolbar items is 160 determined by other mechanisms (user defaults, for example). 161 """ 162 addToolbarItem(self, kWSTReloadContentsToolbarItemIdentifier, 163 "Reload", "Reload", "Reload Contents", None, 164 "reloadVisibleData:", NSImage.imageNamed_("Reload"), None) 165 addToolbarItem(self, kWSTPreferencesToolbarItemIdentifier, 166 "Preferences", "Preferences", "Show Preferences", None, 167 "orderFrontPreferences:", NSImage.imageNamed_("Preferences"), None) 168 addToolbarItem(self, kWSTUrlTextFieldToolbarItemIdentifier, 169 "URL", "URL", "Server URL", None, None, self.urlTextField, None) 170 171 self._toolbarDefaultItemIdentifiers = [ 172 kWSTReloadContentsToolbarItemIdentifier, 173 kWSTUrlTextFieldToolbarItemIdentifier, 174 NSToolbarSeparatorItemIdentifier, 175 NSToolbarCustomizeToolbarItemIdentifier, 176 ] 177 178 self._toolbarAllowedItemIdentifiers = [ 179 kWSTReloadContentsToolbarItemIdentifier, 180 kWSTUrlTextFieldToolbarItemIdentifier, 181 NSToolbarSeparatorItemIdentifier, 182 NSToolbarSpaceItemIdentifier, 183 NSToolbarFlexibleSpaceItemIdentifier, 184 NSToolbarPrintItemIdentifier, 185 kWSTPreferencesToolbarItemIdentifier, 186 NSToolbarCustomizeToolbarItemIdentifier, 187 ] 188 189 def toolbarDefaultItemIdentifiers_(self, anIdentifier): 190 """ 191 Return an array of toolbar item identifiers that identify the 192 set, in order, of items that should be displayed on the 193 default toolbar. 194 """ 195 return self._toolbarDefaultItemIdentifiers 196 197 def toolbarAllowedItemIdentifiers_(self, anIdentifier): 198 """ 199 Return an array of toolbar items that may be used in the toolbar. 200 """ 201 return self._toolbarAllowedItemIdentifiers 202 203 def toolbar_itemForItemIdentifier_willBeInsertedIntoToolbar_(self, 204 toolbar, 205 itemIdentifier, flag): 206 """ 207 Delegate method fired when the toolbar is about to insert an 208 item into the toolbar. Item is identified by itemIdentifier. 209 210 Effectively makes a copy of the cached reference instance of 211 the toolbar item identified by itemIdentifier. 212 """ 213 newItem = NSToolbarItem.alloc().initWithItemIdentifier_(itemIdentifier) 214 item = self._toolbarItems[itemIdentifier] 215 216 newItem.setLabel_( item.label() ) 217 newItem.setPaletteLabel_( item.paletteLabel() ) 218 if item.view(): 219 newItem.setView_( item.view() ) 220 else: 221 newItem.setImage_( item.image() ) 222 223 newItem.setToolTip_( item.toolTip() ) 224 newItem.setTarget_( item.target() ) 225 newItem.setAction_( item.action() ) 226 newItem.setMenuFormRepresentation_( item.menuFormRepresentation() ) 227 228 if newItem.view(): 229 newItem.setMinSize_( item.minSize() ) 230 newItem.setMaxSize_( item.maxSize() ) 231 232 return newItem 233 234 def setStatusTextFieldMessage_(self, aMessage): 235 """ 236 Sets the contents of the statusTextField to aMessage and 237 forces the fileld's contents to be redisplayed. 238 """ 239 if not aMessage: 240 aMessage = "Displaying information about %d methods." % len(self._methods) 241 # All UI calls should be directed to the main thread 242 self.statusTextField.setStringValue_(aMessage) 243 244 def reloadData(self): 245 """Tell the main thread to update the table view.""" 246 self.methodsTable.reloadData() 247 248 def startWorking(self): 249 """Signal the UI there's work goin on.""" 250 self.progressIndicator.startAnimation_(self) 251 252 def stopWorking(self): 253 """Signal the UI that the work is done.""" 254 self.progressIndicator.stopAnimation_(self) 255 256 @objc.IBAction 257 def reloadVisibleData_(self, sender): 258 """ 259 Reloads the list of methods and their signatures from the 260 XML-RPC server specified in the urlTextField. Displays 261 appropriate error messages, if necessary. 262 """ 263 url = self.urlTextField.stringValue() 264 self._methods = [] 265 self._methodSignatures = {} 266 self._methodDescriptions = {} 267 268 if not url: 269 self.window().setTitle_("Untitled.") 270 self.setStatusTextFieldMessage_("No URL specified.") 271 return 272 273 self.window().setTitle_(url) 274 NSUserDefaults.standardUserDefaults().setObject_forKey_(url, "LastURL") 275 276 self.setStatusTextFieldMessage_("Retrieving method list...") 277 self.getMethods(url) 278 279 def getMethods(self, url): 280 _server = self._server = Proxy(url.encode('utf8')) 281 self.startWorking() 282 return _server.callRemote('listMethods').addCallback( 283 # call self.receivedMethods(result, _server, "") on success 284 self.receivedMethods, _server, "" 285 ).addErrback( 286 # on error, call this lambda 287 lambda e: _server.callRemote('system.listMethods').addCallback( 288 # call self.receievedMethods(result, _server, "system.") 289 self.receivedMethods, _server, 'system.' 290 ) 291 ).addErrback( 292 # log the failure instance, with a method 293 self.receivedMethodsFailure, 'listMethods()' 294 ).addBoth( 295 # stop working nomatter what trap all errors (returns None) 296 lambda n:self.stopWorking() 297 ) 298 299 def receivedMethodsFailure(self, why, method): 300 self._server = None 301 self._methodPrefix = None 302 self.setStatusTextFieldMessage_( 303 ("Server failed to respond to %s. " 304 "See below for more information." ) % (method,) 305 ) 306 #log.err(why) 307 self.methodDescriptionTextView.setString_(why.getTraceback()) 308 309 def receivedMethods(self, _methods, _server, _methodPrefix): 310 self._server = _server 311 self._methods = _methods 312 self._methodPrefix = _methodPrefix 313 314 self._methods.sort() 315 self.reloadData() 316 self.setStatusTextFieldMessage_( 317 "Retrieving information about %d methods." % (len(self._methods),) 318 ) 319 320 # we could make all the requests at once :) 321 # but the server might not like that so we will chain them 322 d = defer.succeed(None) 323 for index, aMethod in enumerate(self._methods): 324 d.addCallback( 325 self.fetchMethodSignature, index, aMethod 326 ).addCallbacks( 327 callback = self.processSignatureForMethod, 328 callbackArgs = (index, aMethod), 329 errback = self.couldntProcessSignatureForMethod, 330 errbackArgs = (index, aMethod), 331 ) 332 return d.addCallback( 333 lambda ig: self.setStatusTextFieldMessage_(None) 334 ).addCallback( 335 lambda ig: self.reloadData() 336 ) 337 338 def fetchMethodSignature(self, ignore, index, aMethod): 339 if (index % 5)==0: 340 self.reloadData() 341 self.setStatusTextFieldMessage_( 342 "Retrieving signature for method %s (%d of %d)." 343 % (aMethod , index, len(self._methods)) 344 ) 345 return self._server.callRemote( 346 self._methodPrefix + 'methodSignature', 347 aMethod 348 ) 349 350 351 def processSignatureForMethod(self, methodSignature, index, aMethod): 352 signatures = None 353 if not len(methodSignature): 354 return 355 for aSignature in methodSignature: 356 if (type(aSignature) == types.ListType) and (len(aSignature) > 0): 357 signature = "%s %s(%s)" % (aSignature[0], aMethod, string.join(aSignature[1:], ", ")) 358 else: 359 signature = aSignature 360 if signatures: 361 signatures = signatures + ", " + signature 362 else: 363 signatures = signature 364 self._methodSignatures[aMethod] = signatures 365 366 def couldntProcessSignatureForMethod(self, why, index, aMethod): 367 368 #log.err(why) 369 self._methodSignatures[aMethod] = ( 370 "<error> %s %s" % (aMethod, why.getBriefTraceback()) 371 ) 372 373 def tableViewSelectionDidChange_(self, sender): 374 """ 375 When the user selects a remote method, this method displays 376 the documentation for that method as returned by the XML-RPC 377 server. If the method's documentation has been previously 378 queried, the documentation will be retrieved from a cache. 379 """ 380 selectedRow = self.methodsTable.selectedRow() 381 selectedMethod = self._methods[selectedRow] 382 383 def displayMethod(methodDescription): 384 self.setStatusTextFieldMessage_(None) 385 self.methodDescriptionTextView.setString_(methodDescription) 386 self.fetchMethodDescription(selectedMethod).addCallback(displayMethod) 387 388 def fetchMethodDescription(self, aMethod): 389 desc = self._methodDescriptions 390 if aMethod in desc: 391 return defer.succeed(desc[aMethod]) 392 393 def cacheDesc(v): 394 v = v or "No description available." 395 desc[aMethod] = v 396 return v 397 398 def _stopWorking(v): 399 self.stopWorking() 400 return v 401 402 desc[aMethod] = "<description is being retrieved>" 403 self.setStatusTextFieldMessage_( 404 "Retrieving signature for method %s..." % (aMethod,) 405 ) 406 self.startWorking() 407 return self._server.callRemote( 408 self._methodPrefix + 'methodHelp', 409 aMethod 410 ).addCallback(_stopWorking).addCallback(cacheDesc) 411 412 413 def numberOfRowsInTableView_(self, aTableView): 414 """ 415 Returns the number of methods found on the server. 416 """ 417 return len(self._methods) 418 419 def tableView_objectValueForTableColumn_row_(self, aTableView, aTableColumn, rowIndex): 420 """ 421 Returns either the raw method name or the method signature, 422 depending on if a signature had been found on the server. 423 """ 424 aMethod = self._methods[rowIndex] 425 if self._methodSignatures.has_key(aMethod): 426 return self._methodSignatures[aMethod] 427 else: 428 return aMethod 429 430 def tableView_shouldEditTableColumn_row_(self, aTableView, aTableColumn, rowIndex): 431 # don't allow editing of any cells 432 return 0 433