ABPresence
==========

* :download:`Download example <PyObjCExample-ABPresence.zip>`

A PyObjC Example without documentation

.. rst-class:: tabber

Sources
-------

.. rst-class:: tabbertab

ABPersonDisplayNameAdditions.py
...............................

.. sourcecode:: python

    import AddressBook
    import Cocoa
    import objc
    
    
    class ABPerson(objc.Category(AddressBook.ABPerson)):
        # Pull first and last name, organization, and record flags
        # If the entry is a company, display the organization name instead
        def displayName(self):
            firstName = self.valueForProperty_(AddressBook.kABFirstNameProperty)
            lastName = self.valueForProperty_(AddressBook.kABLastNameProperty)
            companyName = self.valueForProperty_(AddressBook.kABOrganizationProperty)
            flags = self.valueForProperty_(AddressBook.kABPersonFlags)
            if flags is None:
                flags = 0
    
            if (flags & AddressBook.kABShowAsMask) == AddressBook.kABShowAsCompany:
                if len(companyName):
                    return companyName
    
            lastNameFirst = (
                flags & AddressBook.kABNameOrderingMask
            ) == AddressBook.kABLastNameFirst
            hasFirstName = firstName is not None
            hasLastName = lastName is not None
    
            if hasLastName and hasFirstName:
                if lastNameFirst:
                    return Cocoa.NSString.stringWithString_(f"{lastName} {firstName}")
                else:
                    return Cocoa.NSString.stringWithString_(f"{firstName} {lastName}")
    
            if hasLastName:
                return lastName
    
            return firstName
    
        def compareDisplayNames_(self, person):
            return self.displayName().localizedCaseInsensitiveCompare_(person.displayName())

.. rst-class:: tabbertab

PeopleDataSource.py
...................

.. sourcecode:: python

    import AddressBook
    import Cocoa
    import InstantMessage
    import objc
    from ServiceWatcher import kAddressBookPersonStatusChanged, kStatusImagesChanged
    
    
    class PeopleDataSource(Cocoa.NSObject):
        _abPeople = objc.ivar()
        _imPersonStatus = objc.ivar()  # Parallel array to abPeople
        _table = objc.IBOutlet()
    
        def awakeFromNib(self):
            self._imPersonStatus = Cocoa.NSMutableArray.alloc().init()
    
            # We don't need to query the status of everyone, we will wait for
            # notifications of their status to arrive, so we just set them all up
            # as offline.
            self.setupABPeople()
    
            Cocoa.NSNotificationCenter.defaultCenter().addObserver_selector_name_object_(
                self,
                b"abDatabaseChangedExternallyNotification:",
                AddressBook.kABDatabaseChangedExternallyNotification,
                None,
            )
    
            Cocoa.NSNotificationCenter.defaultCenter().addObserver_selector_name_object_(
                self,
                b"addressBookPersonStatusChanged:",
                kAddressBookPersonStatusChanged,
                None,
            )
    
            Cocoa.NSNotificationCenter.defaultCenter().addObserver_selector_name_object_(
                self, b"statusImagesChanged:", kStatusImagesChanged, None
            )
    
        # This dumps all the status information and rebuilds the array against
        # the current _abPeople
        # Fairly expensive, so this is only done when necessary
        def rebuildStatusInformation(self):
            # Now scan through all the people, adding their status to the status
            # cache array
            for i, person in enumerate(self._abPeople):
                # Let's assume they're offline to start
                bestStatus = InstantMessage.IMPersonStatusOffline
    
                for service in InstantMessage.IMService.allServices():
                    screenNames = service.screenNamesForPerson_(person)
    
                    for screenName in screenNames:
                        dictionary = service.infoForScreenName_(screenName)
                        status = dictionary.get(InstantMessage.IMPersonStatusKey)
                        if status is not None:
                            thisStatus = status
                            if (
                                InstantMessage.IMComparePersonStatus(bestStatus, thisStatus)
                                != Cocoa.NSOrderedAscending
                            ):
                                bestStatus = thisStatus
    
                self._imPersonStatus[i] = bestStatus
    
            self._table.reloadData()
    
        # Rebuild status information for a given person, much faster than a full
        # rebuild
        def rebuildStatusInformationForPerson_(self, forPerson):
            for i, person in enumerate(self._abPeople):
                if person is forPerson:
                    bestStatus = InstantMessage.IMPersonStatusOffline
    
                    # Scan through all the services, taking the 'best' status we
                    # can find
                    for service in InstantMessage.IMService.allServices():
                        screenNames = service.screenNamesForPerson_(person)
    
                        # Ask for the status on each of their screen names
                        for screenName in screenNames:
                            dictionary = service.infoForScreenName_(screenName)
                            status = dictionary.get(InstantMessage.IMPersonStatusKey)
                            if status is not None:
                                thisStatus = status
                                if (
                                    InstantMessage.IMComparePersonStatus(
                                        bestStatus, thisStatus
                                    )
                                    != Cocoa.NSOrderedAscending
                                ):
                                    bestStatus = thisStatus
    
                    self._imPersonStatus[i] = bestStatus
                    self._table.reloadData()
                    break
    
        # Sets up all our internal data
        def setupABPeople(self):
            # Keep around a copy of all the people in the AB now
            self._abPeople = (
                AddressBook.ABAddressBook.sharedAddressBook().people().mutableCopy()
            )
    
            # Sort them by display name
            self._abPeople.sortUsingSelector_("compareDisplayNames:")
    
            # Assume everyone is offline.
            self._imPersonStatus.removeAllObjects()
            offlineNumber = InstantMessage.IMPersonStatusOffline
            for _ in range(len(self._abPeople)):
                self._imPersonStatus.append(offlineNumber)
    
        # This will do a full flush of people in our AB Cache, along with
        # rebuilding their status */
        def reloadABPeople(self):
            self.setupABPeople()
    
            # Now recache all the status info, this will spawn a reload of the table
            self.rebuildStatusInformation()
    
        def numberOfRowsInTableView_(self, tableView):
            if self._abPeople is None:
                return 0
            return len(self._abPeople)
    
        def tableView_objectValueForTableColumn_row_(self, tableView, tableColumn, row):
            identifier = tableColumn.identifier()
            if identifier == "image":
                status = self._imPersonStatus[row]
                return Cocoa.NSImage.imageNamed_(
                    InstantMessage.IMService.imageNameForStatus_(status)
                )
    
            elif identifier == "name":
                return self._abPeople[row].displayName()
    
            return None
    
        # Posted from ServiceWatcher
        # The object of this notification is an ABPerson who's status has
        # Changed
        def addressBookPersonStatusChanged_(self, notification):
            self.rebuildStatusInformationForPerson_(notification.object())
    
        # Posted from ServiceWatcher
        # We should reload the tableview, because the user has changed the
        # status images that iChat is using.
        def statusImagesChanged_(self, notification):
            self._table.reloadData()
    
        # If the AB database changes, force a reload of everyone
        # We could look in the notification to catch differential updates, but
        # for now this is fine.
        def abDatabaseChangedExternallyNotification_(self, notification):
            self.reloadABPeople()

.. rst-class:: tabbertab

ServiceWatcher.py
.................

.. sourcecode:: python

    import AddressBook  # noqa: F401
    import Cocoa
    import InstantMessage
    
    kAddressBookPersonStatusChanged = "AddressBookPersonStatusChanged"
    kStatusImagesChanged = "StatusImagesChanged"
    
    
    class ServiceWatcher(Cocoa.NSObject):
        def startMonitoring(self):
            nCenter = InstantMessage.IMService.notificationCenter()
            if nCenter is None:
                return None
    
            nCenter.addObserver_selector_name_object_(
                self,
                b"imPersonStatusChangedNotification:",
                InstantMessage.IMPersonStatusChangedNotification,
                None,
            )
    
            nCenter.addObserver_selector_name_object_(
                self,
                b"imStatusImagesChangedAppearanceNotification:",
                InstantMessage.IMStatusImagesChangedAppearanceNotification,
                None,
            )
    
        def stopMonitoring(self):
            nCenter = InstantMessage.IMService.notificationCenter()
            nCenter.removeObserver_(self)
    
        def awakeFromNib(self):
            self.startMonitoring()
    
        # Received from IMService's custom notification center. Posted when a
        # different user (screenName) logs in, logs off, goes away,
        # and so on. This notification is for the IMService object. The user
        # information dictionary will always contain an
        # IMPersonScreenNameKey and an IMPersonStatusKey, and no others.
        def imPersonStatusChangedNotification_(self, notification):
            service = notification.object()
            userInfo = notification.userInfo()
            screenName = userInfo[InstantMessage.IMPersonScreenNameKey]
            abPersons = service.peopleWithScreenName_(screenName)
    
            center = Cocoa.NSNotificationCenter.defaultCenter()
            for person in abPersons:
                center.postNotificationName_object_(kAddressBookPersonStatusChanged, person)
    
        # Received from IMService's custom notification center. Posted when the
        # user changes their preferred images for displaying status.
        # This notification is relevant to no particular object. The user
        # information dictionary will not contain keys. Clients that display
        # status information graphically (using the green/yellow/red dots) should
        # call <tt>imageURLForStatus:</tt> to get the new image.
        # See "Class Methods" for IMService in this document.
        def imStatusImagesChangedAppearanceNotification_(self, notification):
            Cocoa.NSNotificationCenter.defaultCenter().postNotificationName_object_(
                kStatusImagesChanged, self
            )

.. rst-class:: tabbertab

main.py
.......

.. sourcecode:: python

    import ABPersonDisplayNameAdditions  # noqa: F401
    import PeopleDataSource  # noqa: F401
    import ServiceWatcher  # noqa: F401
    from PyObjCTools import AppHelper
    
    
    AppHelper.runEventLoop()

.. rst-class:: tabbertab

setup.py
........

.. sourcecode:: python

    """
    Script for building the example.
    
    Usage:
        python3 setup.py py2app
    """
    
    from setuptools import setup
    
    setup(
        name="PyABPresence",
        app=["main.py"],
        data_files=["English.lproj"],
        setup_requires=[
            "py2app",
            "pyobjc-framework-Cocoa",
            "pyobjc-framework-InstantMessage",
        ],
    )

