PDFKitViewer

A PyObjC Example without documentation

Sources

AppDelegate.py

from Cocoa import NSObject


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

MyPDFDocument.py

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()

main.py

import AppDelegate  # noqa: F401
import MyPDFDocument  # noqa: F401
from PyObjCTools import AppHelper

AppHelper.runEventLoop()

setup.py

"""
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"],
)