PDFKitViewer
============

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

A simple PDF viewer application using the ``PDFKit`` framework.


.. rst-class:: tabber

Sources
-------

.. rst-class:: tabbertab

AppDelegate.py
..............

.. sourcecode:: python

    from Cocoa import NSObject
    
    
    class AppDelegate(NSObject):
        def applicationShouldOpenUntitledFile_(self, application):
            return False

.. rst-class:: tabbertab

MyPDFDocument.py
................

.. sourcecode:: python

    import Cocoa
    import objc
    import Quartz
    from objc import super  # noqa: A004
    
    
    class MyPDFDocument(Cocoa.NSDocument):
        _outline = objc.ivar()
        _searchResults = objc.ivar()
    
        _drawer = objc.IBOutlet()
        _noOutlineText = objc.IBOutlet()
        _outlineView = objc.IBOutlet()
        _pdfView = objc.IBOutlet()
        _searchProgress = objc.IBOutlet()
        _searchTable = objc.IBOutlet()
    
        def dealloc(self):
            Cocoa.NSNotificationCenter.defaultCenter().removeObserver_(self)
            self._searchResults = None
            super().dealloc()
    
        def windowNibName(self):
            return "MyDocument"
    
        def windowControllerDidLoadNib_(self, controller):
            super().windowControllerDidLoadNib_(controller)
    
            if self.fileName():
                pdfDoc = Quartz.PDFDocument.alloc().initWithURL_(
                    Cocoa.NSURL.fileURLWithPath_(self.fileName())
                )
                self._pdfView.setDocument_(pdfDoc)
    
            # Page changed notification.
            Cocoa.NSNotificationCenter.defaultCenter().addObserver_selector_name_object_(
                self, "pageChanged:", Quartz.PDFViewPageChangedNotification, self._pdfView
            )
    
            # Find notifications.
            center = Cocoa.NSNotificationCenter.defaultCenter()
            center.addObserver_selector_name_object_(
                self,
                "startFind:",
                Quartz.PDFDocumentDidBeginFindNotification,
                self._pdfView.document(),
            )
            center.addObserver_selector_name_object_(
                self,
                "findProgress:",
                Quartz.PDFDocumentDidEndPageFindNotification,
                self._pdfView.document(),
            )
            center.addObserver_selector_name_object_(
                self,
                "endFind:",
                Quartz.PDFDocumentDidEndFindNotification,
                self._pdfView.document(),
            )
    
            # Set self to be delegate (find).
            self._pdfView.document().setDelegate_(self)
    
            # Get outline.
            self._outline = self._pdfView.document().outlineRoot()
            if self._outline is not None:
                # Remove text that says, "No outline."
                self._noOutlineText.removeFromSuperview()
                self._noOutlineText = None
    
                # Force it to load up.
                self._outlineView.reloadData()
    
            else:
                # Remove outline view (leaving instead text that says,
                # "No outline.").
                self._outlineView.enclosingScrollView().removeFromSuperview()
                self._outlineView = None
    
            # Open drawer.
            self._drawer.open()
    
            # Size the window.
            windowSize = self._pdfView.rowSizeForPage_(self._pdfView.currentPage())
    
            if (self._pdfView.displayMode() & 0x01) and (
                self._pdfView.document().pageCount() > 1
            ):
                windowSize.width += Cocoa.NSScroller.scrollerWidth()
            controller.window().setContentSize_(windowSize)
    
        def dataRepresentationOfType_(self, aType):
            return None
    
        def loadDataRepresentation_ofType_(self, data, aType):
            return True
    
        @objc.IBAction
        def toggleDrawer_(self, sender):
            self._drawer.toggle_(self)
    
        @objc.IBAction
        def takeDestinationFromOutline_(self, sender):
            # Get the destination associated with the search result list.
            # Tell the PDFView to go there.
            self._pdfView.goToDestination_(
                sender.itemAtRow_(sender.selectedRow()).destination()
            )
    
        @objc.IBAction
        def displaySinglePage_(self, sender):
            # Display single page mode.
            if self._pdfView.displayMode() > Quartz.kPDFDisplaySinglePageContinuous:
                self._pdfView.setDisplayMode_(self._pdfView.displayMode() - 2)
    
        @objc.IBAction
        def displayTwoUp_(self, sender):
            #  Display two-up.
            if self._pdfView.displayMode() < Quartz.kPDFDisplayTwoUp:
                self._pdfView.setDisplayMode_(self._pdfView.displayMode() + 2)
    
        def pageChanged_(self, notification):
            # Skip out if there is no outline.
            if self.pdfView.document().outlineRoot() is None:
                return
    
            # What is the new page number (zero-based).
            newPageIndex = self._pdfView.document().indexForPage_(
                self._pdfView.currentPage()
            )
    
            # Walk outline view looking for best firstpage number match.
            newlySelectedRow = -1
            numRows = self._outlineView.numberOfRows()
            for i in range(numRows):
                outlineItem = self._outlineView.itemAtRow_(i)
    
                if (
                    self._pdfView.document().indexForPage_(outlineItem.destination().page())
                    == newPageIndex
                ):
                    newlySelectedRow = i
                    self._outlineView.selectRow_byExtendingSelection_(
                        newlySelectedRow, False
                    )
                    break
    
                elif (
                    self._pdfView.document().indexForPage_(outlineItem.destination().page())
                    > newPageIndex
                ):
                    newlySelectedRow = i - 1
                    self._outlineView.selectRow_byExtendingSelection_(
                        newlySelectedRow, False
                    )
                    break
    
            # Auto-scroll.
            if newlySelectedRow != -1:
                self._outlineView.scrollRowToVisible_(newlySelectedRow)
    
        def doFind_(self, sender):
            if self._pdfView.document().isFinding():
                self._pdfView.document().cancelFindString()
    
            # Lazily allocate _searchResults.
            if self._searchResults is None:
                self._searchResults = Cocoa.NSMutableArray.arrayWithCapacity_(10)
    
            self._pdfView.document().beginFindString_withOptions_(
                sender.stringValue(), Cocoa.NSCaseInsensitiveSearch
            )
    
        def startFind_(self, notification):
            # Empty arrays.
            self._searchResults.removeAllObjects()
            self._searchTable.reloadData()
            self._searchProgress.startAnimation_(self)
    
        def findProgress_(self, notification):
            pageIndex = notification.userInfo().objectForKey_(
                "PDFDocumentPageIndex"
            )  # .doubleValue()
            self._searchProgress.setDoubleValue_(
                pageIndex / self._pdfView.document().pageCount()
            )
    
        def didMatchString_(self, instance):
            # Add page label to our array.
            self._searchResults.addObject_(instance.copy())
            self._searchTable.reloadData()
    
        def endFind_(self, notification):
            self._searchProgress.stopAnimation_(self)
            self._searchProgress.setDoubleValue_(0)
    
        #  The table view is used to hold search results.  Column 1 lists the
        # page number for the search result,  column two the section in the PDF
        # (x-ref with the PDF outline) where the result appears.
    
        def numberOfRowsInTableView_(self, aTableView):
            if self._searchResults is None:
                return 0
            return self._searchResults.count()
    
        def tableView_objectValueForTableColumn_row_(self, aTableView, theColumn, rowIndex):
            if theColumn.identifier() == "page":
                return (
                    self._searchResults.objectAtIndex_(rowIndex)
                    .pages()
                    .objectAtIndex_(0)
                    .label()
                )
    
            elif theColumn.identifier() == "section":
                value = self._pdfView.document().outlineItemForSelection_(
                    self._searchResults.objectAtIndex_(rowIndex)
                )
    
                if value is None:
                    return None
    
                return value.label()
    
            else:
                return None
    
        def tableViewSelectionDidChange_(self, notification):
            # What was selected.  Skip out if the row has not changed.
            rowIndex = notification.object().selectedRow()
            if rowIndex >= 0:
                self._pdfView.setCurrentSelection_(
                    self._searchResults.objectAtIndex_(rowIndex)
                )
                self._pdfView.centerSelectionInVisibleArea_(self)
    
        # The outline view is for the PDF outline.  Not all PDF's have an outline.
        def outlineView_numberOfChildrenOfItem_(self, outlineView, item):
            if item is None:
                if self._outline is not None:
                    return self._outline.numberOfChildren()
                else:
                    return 0
    
            else:
                return item.numberOfChildren()
    
        def outlineView_child_ofItem_(self, outlineView, index, item):
            if item is None:
                if self._outline is not None:
                    return self._outline.childAtIndex_(index).retain()
                else:
                    return None
    
            else:
                return item.childAtIndex_(index).retain()
    
        def outlineView_isItemExpandable_(self, outlineView, item):
            if item is None:
                if self._outline:
                    return self._outline.numberOfChildren() > 0
    
                else:
                    return False
    
            else:
                return item.numberOfChildren() > 0
    
        def outlineView_objectValueForTableColumn_byItem_(
            self, outlineView, tableColumn, item
        ):
            return item.label()

.. rst-class:: tabbertab

main.py
.......

.. sourcecode:: python

    import AppDelegate  # noqa: F401
    import MyPDFDocument  # 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="PDFKitViewer",
        app=["main.py"],
        data_files=["English.lproj", "pdfkitviewer.icns"],
        options={"py2app": {"plist": "Info.plist"}},
        setup_requires=["py2app", "pyobjc-framework-Cocoa", "pyobjc-framework-Quartz"],
    )

