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 threading import Thread 21from Queue import Queue 22 23import xmlrpclib 24import sys 25import types 26import string 27import traceback 28 29kWSTReloadContentsToolbarItemIdentifier = "WST: Reload Contents Toolbar Identifier" 30"""Identifier for 'reload contents' toolbar item.""" 31 32kWSTPreferencesToolbarItemIdentifier = "WST: Preferences Toolbar Identifier" 33"""Identifier for 'preferences' toolbar item.""" 34 35kWSTUrlTextFieldToolbarItemIdentifier = "WST: URL Textfield Toolbar Identifier" 36"""Idnetifier for URL text field toolbar item.""" 37 38def addToolbarItem(aController, anIdentifier, aLabel, aPaletteLabel, 39 aToolTip, aTarget, anAction, anItemContent, aMenu): 40 """ 41 Adds an freshly created item to the toolbar defined by 42 aController. Makes a number of assumptions about the 43 implementation of aController. It should be refactored into a 44 generically useful toolbar management untility. 45 """ 46 toolbarItem = NSToolbarItem.alloc().initWithItemIdentifier_(anIdentifier) 47 48 toolbarItem.setLabel_(aLabel) 49 toolbarItem.setPaletteLabel_(aPaletteLabel) 50 toolbarItem.setToolTip_(aToolTip) 51 toolbarItem.setTarget_(aTarget) 52 if anAction: 53 toolbarItem.setAction_(anAction) 54 55 if type(anItemContent) == NSImage: 56 toolbarItem.setImage_(anItemContent) 57 else: 58 toolbarItem.setView_(anItemContent) 59 bounds = anItemContent.bounds() 60 minSize = (100, bounds[1][1]) 61 maxSize = (1000, bounds[1][1]) 62 toolbarItem.setMinSize_( minSize ) 63 toolbarItem.setMaxSize_( maxSize ) 64 65 if aMenu: 66 menuItem = NSMenuItem.alloc().init() 67 menuItem.setSubmenu_(aMenu) 68 menuItem.setTitle_( aMenu.title() ) 69 toolbarItem.setMenuFormRepresentation_(menuItem) 70 71 aController._toolbarItems[anIdentifier] = toolbarItem 72 73 74class WorkerThread(Thread): 75 76 def __init__(self): 77 """Create a worker thread. Start it by calling the start() method.""" 78 self.queue = Queue() 79 Thread.__init__(self) 80 81 def stop(self): 82 """Stop the thread a.s.a.p., meaning whenever the currently running 83 job is finished.""" 84 self.working = 0 85 self.queue.put(None) 86 87 def scheduleWork(self, func, *args, **kwargs): 88 """Schedule some work to be done in the worker thread.""" 89 self.queue.put((func, args, kwargs)) 90 91 def run(self): 92 """Fetch work from a queue, block when there's nothing to do. 93 This method is called by Thread, don't call it yourself.""" 94 self.working = 1 95 while self.working: 96 work = self.queue.get() 97 if work is None or not self.working: 98 break 99 func, args, kwargs = work 100 pool = NSAutoreleasePool.alloc().init() 101 try: 102 func(*args, **kwargs) 103 finally: 104 # delete all local references; if they are the last refs they 105 # may invoke autoreleases, which should then end up in our pool 106 del func, args, kwargs, work 107 del pool 108 109 110class WSTConnectionWindowController(NSWindowController): 111 methodDescriptionTextView = objc.IBOutlet() 112 methodsTable = objc.IBOutlet() 113 progressIndicator = objc.IBOutlet() 114 statusTextField = objc.IBOutlet() 115 urlTextField = objc.IBOutlet() 116 117 __slots__ = ('_toolbarItems', 118 '_toolbarDefaultItemIdentifiers', 119 '_toolbarAllowedItemIdentifiers', 120 '_methods', 121 '_methodSignatures', 122 '_methodDescriptions', 123 '_server', 124 '_methodPrefix', 125 '_workQueue', 126 '_working', 127 '_workerThread', 128 '_windowIsClosing') 129 130 @classmethod 131 def connectionWindowController(self): 132 """ 133 Create and return a default connection window instance. 134 """ 135 return WSTConnectionWindowController.alloc().init() 136 137 def init(self): 138 """ 139 Designated initializer. 140 141 Returns self (as per ObjC designated initializer definition, 142 unlike Python's __init__() method). 143 """ 144 self = self.initWithWindowNibName_("WSTConnection") 145 146 self._toolbarItems = {} 147 self._toolbarDefaultItemIdentifiers = [] 148 self._toolbarAllowedItemIdentifiers = [] 149 150 self._methods = [] 151 self._working = 0 152 self._windowIsClosing = 0 153 self._workerThread = WorkerThread() 154 self._workerThread.start() 155 return self 156 157 def awakeFromNib(self): 158 """ 159 Invoked when the NIB file is loaded. Initializes the various 160 UI widgets. 161 """ 162 self.retain() # balanced by autorelease() in windowWillClose_ 163 164 self.statusTextField.setStringValue_("No host specified.") 165 self.progressIndicator.setStyle_(NSProgressIndicatorSpinningStyle) 166 self.progressIndicator.setDisplayedWhenStopped_(False) 167 168 self.createToolbar() 169 170 def windowWillClose_(self, aNotification): 171 """ 172 Clean up when the document window is closed. 173 """ 174 # We must stop the worker thread and wait until it actually finishes before 175 # we can allow the window to close. Weird stuff happens if we simply let the 176 # thread run. When this thread is idle (blocking in queue.get()) there is 177 # no problem and we can almost instantly close the window. If it's actually 178 # in the middle of working it may take a couple of seconds, as we can't 179 # _force_ the thread to stop: we have to ask it to to stop itself. 180 self._windowIsClosing = 1 # try to stop the thread a.s.a.p. 181 self._workerThread.stop() # signal the thread that there is no more work to do 182 self._workerThread.join() # wait until it finishes 183 self.autorelease() 184 185 def createToolbar(self): 186 """ 187 Creates and configures the toolbar to be used by the window. 188 """ 189 toolbar = NSToolbar.alloc().initWithIdentifier_("WST Connection Window") 190 toolbar.setDelegate_(self) 191 toolbar.setAllowsUserCustomization_(True) 192 toolbar.setAutosavesConfiguration_(True) 193 194 self.createToolbarItems() 195 196 self.window().setToolbar_(toolbar) 197 198 lastURL = NSUserDefaults.standardUserDefaults().stringForKey_("LastURL") 199 if lastURL and len(lastURL): 200 self.urlTextField.setStringValue_(lastURL) 201 202 def createToolbarItems(self): 203 """ 204 Creates all of the toolbar items that can be made available in 205 the toolbar. The actual set of available toolbar items is 206 determined by other mechanisms (user defaults, for example). 207 """ 208 addToolbarItem(self, kWSTReloadContentsToolbarItemIdentifier, 209 "Reload", "Reload", "Reload Contents", None, 210 "reloadVisibleData:", NSImage.imageNamed_("Reload"), None) 211 addToolbarItem(self, kWSTPreferencesToolbarItemIdentifier, 212 "Preferences", "Preferences", "Show Preferences", None, 213 "orderFrontPreferences:", NSImage.imageNamed_("Preferences"), None) 214 addToolbarItem(self, kWSTUrlTextFieldToolbarItemIdentifier, 215 "URL", "URL", "Server URL", None, None, self.urlTextField, None) 216 217 self._toolbarDefaultItemIdentifiers = [ 218 kWSTReloadContentsToolbarItemIdentifier, 219 kWSTUrlTextFieldToolbarItemIdentifier, 220 NSToolbarSeparatorItemIdentifier, 221 NSToolbarCustomizeToolbarItemIdentifier, 222 ] 223 224 self._toolbarAllowedItemIdentifiers = [ 225 kWSTReloadContentsToolbarItemIdentifier, 226 kWSTUrlTextFieldToolbarItemIdentifier, 227 NSToolbarSeparatorItemIdentifier, 228 NSToolbarSpaceItemIdentifier, 229 NSToolbarFlexibleSpaceItemIdentifier, 230 NSToolbarPrintItemIdentifier, 231 kWSTPreferencesToolbarItemIdentifier, 232 NSToolbarCustomizeToolbarItemIdentifier, 233 ] 234 235 def toolbarDefaultItemIdentifiers_(self, anIdentifier): 236 """ 237 Return an array of toolbar item identifiers that identify the 238 set, in order, of items that should be displayed on the 239 default toolbar. 240 """ 241 return self._toolbarDefaultItemIdentifiers 242 243 def toolbarAllowedItemIdentifiers_(self, anIdentifier): 244 """ 245 Return an array of toolbar items that may be used in the toolbar. 246 """ 247 return self._toolbarAllowedItemIdentifiers 248 249 def toolbar_itemForItemIdentifier_willBeInsertedIntoToolbar_(self, 250 toolbar, 251 itemIdentifier, flag): 252 """ 253 Delegate method fired when the toolbar is about to insert an 254 item into the toolbar. Item is identified by itemIdentifier. 255 256 Effectively makes a copy of the cached reference instance of 257 the toolbar item identified by itemIdentifier. 258 """ 259 newItem = NSToolbarItem.alloc().initWithItemIdentifier_(itemIdentifier) 260 item = self._toolbarItems[itemIdentifier] 261 262 newItem.setLabel_( item.label() ) 263 newItem.setPaletteLabel_( item.paletteLabel() ) 264 if item.view(): 265 newItem.setView_( item.view() ) 266 else: 267 newItem.setImage_( item.image() ) 268 269 newItem.setToolTip_( item.toolTip() ) 270 newItem.setTarget_( item.target() ) 271 newItem.setAction_( item.action() ) 272 newItem.setMenuFormRepresentation_( item.menuFormRepresentation() ) 273 274 if newItem.view(): 275 newItem.setMinSize_( item.minSize() ) 276 newItem.setMaxSize_( item.maxSize() ) 277 278 return newItem 279 280 def setStatusTextFieldMessage_(self, aMessage): 281 """ 282 Sets the contents of the statusTextField to aMessage and 283 forces the fileld's contents to be redisplayed. 284 """ 285 if not aMessage: 286 aMessage = "Displaying information about %d methods." % len(self._methods) 287 # All UI calls should be directed to the main thread 288 self.statusTextField.performSelectorOnMainThread_withObject_waitUntilDone_( 289 "setStringValue:", aMessage, 0) 290 291 def reloadData(self): 292 """Tell the main thread to update the table view.""" 293 self.methodsTable.performSelectorOnMainThread_withObject_waitUntilDone_( 294 "reloadData", None, 0) 295 296 def startWorking(self): 297 """Signal the UI there's work goin on.""" 298 if not self._working: 299 self.progressIndicator.startAnimation_(self) 300 self._working += 1 301 302 def stopWorking(self): 303 """Signal the UI that the work is done.""" 304 self._working -= 1 305 if not self._working: 306 self.progressIndicator.performSelectorOnMainThread_withObject_waitUntilDone_( 307 "stopAnimation:", self, 0) 308 309 @objc.IBAction 310 def reloadVisibleData_(self, sender): 311 """ 312 Reloads the list of methods and their signatures from the 313 XML-RPC server specified in the urlTextField. Displays 314 appropriate error messages, if necessary. 315 """ 316 if self._working: 317 # don't start a new job while there's an unfinished one 318 return 319 url = self.urlTextField.stringValue() 320 self._methods = [] 321 self._methodSignatures = {} 322 self._methodDescriptions = {} 323 324 if not url: 325 self.window().setTitle_("Untitled.") 326 self.setStatusTextFieldMessage_("No URL specified.") 327 return 328 329 self.window().setTitle_(url) 330 NSUserDefaults.standardUserDefaults().setObject_forKey_(url, "LastURL") 331 332 self.setStatusTextFieldMessage_("Retrieving method list...") 333 self.startWorking() 334 self._workerThread.scheduleWork(self.getMethods, url) 335 336 def getMethods(self, url): 337 self._server = xmlrpclib.ServerProxy(url) 338 pool = NSAutoreleasePool.alloc().init() # use an extra pool to get rid of intermediates 339 try: 340 self._methods = self._server.listMethods() 341 self._methodPrefix = "" 342 except: 343 try: 344 self._methods = self._server.system.listMethods() 345 self._methodPrefix = "system." 346 except: 347 self._server = None 348 self._methodPrefix = None 349 self.setStatusTextFieldMessage_("Server failed to respond to listMethods query. " 350 "See below for more information.") 351 352 exceptionType, exceptionValue, exceptionTraceback = sys.exc_info() 353 self.methodDescriptionTextView.performSelectorOnMainThread_withObject_waitUntilDone_( 354 "setString:", 355 "Exception information\n\nType: %s\n\nValue: %s\n\nTraceback:\n\n %s\n" % 356 (exceptionType, exceptionValue, "\n".join(traceback.format_tb(exceptionTraceback))), 357 0) 358 self.stopWorking() 359 return 360 361 del pool 362 if self._windowIsClosing: 363 return 364 365 self._methods.sort(lambda x, y: cmp(x, y)) 366 self.reloadData() 367 self.setStatusTextFieldMessage_("Retrieving information about %d methods." % len(self._methods)) 368 369 index = 0 370 for aMethod in self._methods: 371 if self._windowIsClosing: 372 return 373 pool = NSAutoreleasePool.alloc().init() # use an extra pool to get rid of intermediates 374 index = index + 1 375 if not (index % 5): 376 self.reloadData() 377 self.setStatusTextFieldMessage_("Retrieving signature for method %s (%d of %d)." % (aMethod , index, len(self._methods))) 378 del pool 379 methodSignature = getattr(self._server, self._methodPrefix + "methodSignature")(aMethod) 380 signatures = None 381 if isinstance(methodSignature, str): continue 382 if not len(methodSignature): 383 continue 384 385 for aSignature in methodSignature: 386 if (type(aSignature) == types.ListType) and (len(aSignature) > 0): 387 signature = "%s %s(%s)" % (aSignature[0], aMethod, string.join(aSignature[1:], ", ")) 388 else: 389 signature = aSignature 390 if signatures: 391 signatures = signatures + ", " + signature 392 else: 393 signatures = signature 394 self._methodSignatures[aMethod] = signatures 395 self.setStatusTextFieldMessage_(None) 396 self.reloadData() 397 self.stopWorking() 398 399 def tableViewSelectionDidChange_(self, sender): 400 """ 401 When the user selects a remote method, this method displays 402 the documentation for that method as returned by the XML-RPC 403 server. If the method's documentation has been previously 404 queried, the documentation will be retrieved from a cache. 405 """ 406 selectedRow = self.methodsTable.selectedRow() 407 selectedMethod = self._methods[selectedRow] 408 409 if not self._methodDescriptions.has_key(selectedMethod): 410 self._methodDescriptions[selectedMethod] = "<description is being retrieved>" 411 self.startWorking() 412 def work(): 413 self.setStatusTextFieldMessage_("Retrieving signature for method %s..." % selectedMethod) 414 methodDescription = getattr(self._server, self._methodPrefix + "methodHelp")(selectedMethod) 415 if not methodDescription: 416 methodDescription = "No description available." 417 self._methodDescriptions[selectedMethod] = methodDescription 418 if selectedRow == self.methodsTable.selectedRow(): 419 self.setStatusTextFieldMessage_(None) 420 self.methodDescriptionTextView.setString_(methodDescription) 421 self.stopWorking() 422 self._workerThread.scheduleWork(work) 423 else: 424 self.setStatusTextFieldMessage_(None) 425 methodDescription = self._methodDescriptions[selectedMethod] 426 self.methodDescriptionTextView.setString_(methodDescription) 427 428 def numberOfRowsInTableView_(self, aTableView): 429 """ 430 Returns the number of methods found on the server. 431 """ 432 return len(self._methods) 433 434 def tableView_objectValueForTableColumn_row_(self, aTableView, aTableColumn, rowIndex): 435 """ 436 Returns either the raw method name or the method signature, 437 depending on if a signature had been found on the server. 438 """ 439 aMethod = self._methods[rowIndex] 440 if self._methodSignatures.has_key(aMethod): 441 return self._methodSignatures[aMethod] 442 else: 443 return aMethod 444 445 def tableView_shouldEditTableColumn_row_(self, aTableView, aTableColumn, rowIndex): 446 # don't allow editing of any cells 447 return 0 448