• Home
  • History
  • Annotate
  • Line#
  • Navigate
  • Raw
  • Download
  • only in /macosx-10.10/pyobjc-45/2.6/pyobjc/pyobjc-framework-Cocoa/Examples/Twisted/WebServicesTool/
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