# ImageBrowser

- `Download example`

This sample demonstrates the ImageKit `ImageBrowser` in a basic Cocoa application. It uses IB to create a window an `ImageBrowser` and a zoom slider.

This sample should present a reasonably complete correctly formed Cocoa application which can be used as a starting point for using the `ImageBrowser` in a Cocoa applications.

Usual steps to use the ImageKit image browser in your application:

1. setup your nib file

   Add a custom view and set its class to IKImageBrowserView. Connect an IBOutlet from your controller to your image browser view and connect the `IKImageBrowserView`’s `_datasource` `IBOutlet` to your controller (if you want your controller to be the data source)

2. Don’t forget to import the Quartz package:

       from Quartz import *

3. Create your own data source representation (here using a `NSMutableArray`):

       class MyController (NSWindowController):
           _myImages = objc.ivar()
           _myImageView = objc.IBOutlet()

4. implement the required methods of the informal data source protocol (`IKImageBrowserDataSource`):

       def numberOfItemsInImageBrowser_(self, browser):
               return len(self._images)

       def imageBrowser_itemAtIndex_(self, aBrowser, index):
               return self._images[index]

5. The returned data source object must implement the 3 required methods from the `IKImageBrowserItemProtocol` informal protocol:

   > def imageUID(self): pass def mageRepresentationType(self): pass def imageRepresentation(self): pass

   - the id returned by `imageUID` MUST be different for each item displayed in the image-view. Moreover, the image browser build it’s own internal cache according to this UID. the `imageUID` can be for example the absolute path of an image existing on the filesystem or another UID based on your own data structures.

   - `imageRepresentationType` return one of the following string constant depending of the client’s choice of representation:

         IKImageBrowserPathRepresentationType
         IKImageBrowserNSImageRepresentationType
         IKImageBrowserCGImageRepresentationType
         IKImageBrowserNSDataRepresentationType
         IKImageBrowserNSBitmapImageRepresentationType
         IKImageBrowserQTMovieRepresentationType

     (see IKImageBrowserView.h for complete list)

   - `imageRepresentation` return an object depending of the representation type:

     - a `NSString` for `IKImageBrowserPathRepresentationType`

     - a `NSImage` for `IKImageBrowserNSImageRepresentationType`

     - a `CGImageRef` for `IKImageBrowserCGImageRepresentationType`

     - …

   Here is a sample code of a simple implementation of a data source item:

       class myItemObject (NSObject, IKImageBrowserItem):
         _path = objc.ivar()

         def mageRepresentationType(self):
             return IKImageBrowserPathRepresentationType;

         def mageRepresentation(self):
             return self._path

         def imageUID(self):
             return self._path

6. Now to see your data displayed in your instance of the image browser view, you have to tell the browser to read your data using your `IBOutlet` connected to the browser and invoke `reloadData` on it:

       self._myImageView.reloadData()

   Call `reloadData` each time you want the image browser to reflect changes of your data source.

That’s all for a very basic use. Then you may need to add a scroller or a scrollview and a slider to your interface to let the user to scroll and zoom to browser his images.

## Sources

### ImageBrowserController.py

    import os

    import Cocoa
    import LaunchServices
    import objc
    import Quartz

    #       openFiles
    #
    #       A simple C function that opens NSOpenPanel and returns an array of file paths.
    #       It uses uniform type identifiers (UTIs) for proper filtering of image files.
    # -------------------------------------------------------------------------
    def openFiles():
        # Get a list of extensions to filter in our NSOpenPanel.
        panel = Cocoa.NSOpenPanel.openPanel()

        # The user can choose a folder; images in the folder are added recursively.
        panel.setCanChooseDirectories_(True)
        panel.setCanChooseFiles_(True)
        panel.setAllowsMultipleSelection_(True)

        if (
            panel.runModalForTypes_(Cocoa.NSImage.imageUnfilteredTypes())
            == Cocoa.NSOKButton
        ):
            return panel.filenames()

        return []

    # ==============================================================================
    # This is the data source object.
    class myImageObject(Cocoa.NSObject):
        _path = objc.ivar()

        # -------------------------------------------------------------------------
        #   setPath:path
        #
        #   The data source object is just a file path representation
        # -------------------------------------------------------------------------
        def setPath_(self, inPath):
            self._path = inPath

        # The required methods of the IKImageBrowserItem protocol.

        # -------------------------------------------------------------------------
        #   imageRepresentationType:
        #
        #   Set up the image browser to use a path representation.
        # -------------------------------------------------------------------------
        def imageRepresentationType(self):
            return Quartz.IKImageBrowserPathRepresentationType

        # -------------------------------------------------------------------------
        #   imageRepresentation:
        #
        #   Give the path representation to the image browser.
        # -------------------------------------------------------------------------
        def imageRepresentation(self):
            return self._path

        # -------------------------------------------------------------------------
        #   imageUID:
        #
        #   Use the absolute file path as the identifier.
        # -------------------------------------------------------------------------
        def imageUID(self):
            return self._path

    class ImageBrowserController(Cocoa.NSWindowController):
        imageBrowser = objc.IBOutlet()

        images = objc.ivar()
        importedImages = objc.ivar()

        # -------------------------------------------------------------------------
        #   awakeFromNib:
        # -------------------------------------------------------------------------
        def awakeFromNib(self):
            # Create two arrays : The first is for the data source representation.
            # The second one contains temporary imported images  for thread safeness.
            self.images = Cocoa.NSMutableArray.alloc().init()
            self.importedImages = Cocoa.NSMutableArray.alloc().init()

            # Allow reordering, animations and set the dragging destination
            # delegate.
            self.imageBrowser.setAllowsReordering_(True)
            self.imageBrowser.setAnimates_(True)
            self.imageBrowser.setDraggingDestinationDelegate_(self)

        # -------------------------------------------------------------------------
        #   updateDatasource:
        #
        #   This is the entry point for reloading image browser data and
        #   triggering setNeedsDisplay.
        # -------------------------------------------------------------------------
        def updateDatasource(self):
            # Update the datasource, add recently imported items.
            self.images.extend(self.importedImages)

            # Empty the temporary array.
            del self.importedImages[:]

            # Reload the image browser, which triggers setNeedsDisplay.
            self.imageBrowser.reloadData()

        # -------------------------------------------------------------------------
        #   isImageFile:filePath
        #
        #   This utility method indicates if the file located at 'filePath' is
        #   an image file based on the UTI. It relies on the ImageIO framework for
        #   the supported type identifiers.
        #
        # -------------------------------------------------------------------------
        def isImageFile_(self, filePath):
            isImageFile = False
            uti = None

            url = Cocoa.CFURLCreateWithFileSystemPath(
                None, filePath, Cocoa.kCFURLPOSIXPathStyle, False
            )

            res, info = LaunchServices.LSCopyItemInfoForURL(
                url,
                LaunchServices.kLSRequestExtension | LaunchServices.kLSRequestTypeCreator,
                None,
            )
            if res == 0:
                # Obtain the UTI using the file information.

                # If there is a file extension, get the UTI.
                if info[3] is not None:
                    uti = LaunchServices.UTTypeCreatePreferredIdentifierForTag(
                        LaunchServices.kUTTagClassFilenameExtension,
                        info[3],
                        LaunchServices.kUTTypeData,
                    )

                # No UTI yet
                if uti is None:
                    # If there is an OSType, get the UTI.
                    typeString = LaunchServices.UTCreateStringForOSType(info.filetype)
                    if typeString is not None:
                        uti = LaunchServices.UTTypeCreatePreferredIdentifierForTag(
                            LaunchServices.kUTTagClassOSType,
                            typeString,
                            LaunchServices.kUTTypeData,
                        )

                # Verify that this is a file that the ImageIO framework supports.
                if uti is not None:
                    supportedTypes = Quartz.CGImageSourceCopyTypeIdentifiers()

                    for item in supportedTypes:
                        if LaunchServices.UTTypeConformsTo(uti, item):
                            isImageFile = True
                            break

            return isImageFile

        # -------------------------------------------------------------------------
        #   addAnImageWithPath:path
        # -------------------------------------------------------------------------
        def addAnImageWithPath_(self, path):
            addObject = False

            fileAttribs = (
                Cocoa.NSFileManager.defaultManager().fileAttributesAtPath_traverseLink_(
                    path, True
                )
            )
            if fileAttribs is not None:
                # Check for packages.
                if Cocoa.NSFileTypeDirectory == fileAttribs[Cocoa.NSFileType]:
                    if not Cocoa.NSWorkspace.sharedWorkspace().isFilePackageAtPath_(path):
                        addObject = True  # If it is a file, it's OK to add.

                else:
                    addObject = True  # It is a file, so it's OK to add.

            if addObject and self.isImageFile_(path):
                # Add a path to the temporary images array.
                p = myImageObject.alloc().init()
                p.setPath_(path)
                self.importedImages.append(p)

        # -------------------------------------------------------------------------
        #   addImagesWithPath:path:recursive
        # -------------------------------------------------------------------------
        def addImagesWithPath_recursive_(self, path, recursive):
            if os.path.isdir(path):
                content = os.listdir(path)
                # Parse the directory content.
                for fn in content:
                    if recursive:
                        self.addImagesWithPath_recursive_(os.path.join(path, fn), True)
                    else:
                        self.addAnImageWithPath_(os.path.join(path, fn))

            else:
                self.addAnImageWithPath_(path)

        # -------------------------------------------------------------------------
        #   addImagesWithPaths:paths
        #
        #   Performed in an independent thread, parse all paths in "paths" and
        #   add these paths in the temporary images array.
        # -------------------------------------------------------------------------
        def addImagesWithPaths_(self, paths):
            pool = Cocoa.NSAutoreleasePool.alloc().init()

            for path in paths:
                isdir = os.path.isdir(path)
                self.addImagesWithPath_recursive_(path, isdir)

            # Update the data source in the main thread.
            self.performSelectorOnMainThread_withObject_waitUntilDone_(
                b"updateDatasource", None, True
            )

            del pool

        # pragma mark -
        # pragma mark actions

        # -------------------------------------------------------------------------
        #   addImageButtonClicked:sender
        #
        #   The user clicked the Add button.d
        # -------------------------------------------------------------------------
        @objc.IBAction
        def addImageButtonClicked_(self, sender):
            path = openFiles()
            if path:
                # launch import in an independent thread
                Cocoa.NSThread.detachNewThreadSelector_toTarget_withObject_(
                    b"addImagesWithPaths:", self, path
                )

        # -------------------------------------------------------------------------
        #   addImageButtonClicked:sender
        #
        #   Action called when the zoom slider changes.
        # -------------------------------------------------------------------------
        @objc.IBAction
        def zoomSliderDidChange_(self, sender):
            # update the zoom value to scale images
            self.imageBrowser.setZoomValue_(sender.floatValue())

            # redisplay
            self.imageBrowser.setNeedsDisplay_(True)

        # Implement the image browser  data source protocol .
        # The data source representation is a simple mutable array.

        # -------------------------------------------------------------------------
        #   numberOfItemsInImageBrowser:view
        # -------------------------------------------------------------------------
        def numberOfItemsInImageBrowser_(self, view):
            # The item count to display is the datadsource item count.
            return len(self.images)

        # -------------------------------------------------------------------------
        #   imageBrowser:view:index:
        # -------------------------------------------------------------------------
        def imageBrowser_itemAtIndex_(self, view, index):
            return self.images[index]

        # Implement some optional methods of the image browser  datasource protocol
        # to allow for removing and reodering items.

        # -------------------------------------------------------------------------
        #   removeItemsAtIndexes:
        #
        #   The user wants to delete images, so remove these entries from the data source.
        # -------------------------------------------------------------------------
        def imageBrowser_removeItemsAtIndexes_(self, view, indexes):
            self.images.removeObjectsAtIndexes_(indexes)

        # -------------------------------------------------------------------------
        #   moveItemsAtIndexes:
        #
        #   The user wants to reorder images, update the datadsource and the browser
        #   will reflect our changes.
        # -------------------------------------------------------------------------
        def imageBrowser_moveItemsAtIndexes_toIndex_(
            self, browser, indexes, destinationIndex
        ):
            temporaryArray = []

            # First remove items from the data source and keep them in a
            # temporary array.
            for index in sorted(indexes, reverse=True):
                if index < destinationIndex:
                    destinationIndex -= 1

                obj = self.images[index]
                temporaryArray.append(obj)
                del self.images[index]

            # Then insert the removed items at the appropriate location.
            for item in temporaryArray:
                self.images.insertObject_atIndex_(item, destinationIndex)

            return True

        # -------------------------------------------------------------------------
        #   draggingEntered:sender
        # -------------------------------------------------------------------------
        def draggingEntered_(self, sender):
            return Cocoa.NSDragOperationCopy

        # -------------------------------------------------------------------------
        #   draggingUpdated:sender
        # -------------------------------------------------------------------------
        def draggingUpdated_(self, sender):
            return Cocoa.NSDragOperationCopy

        # -------------------------------------------------------------------------
        #   performDragOperation:sender
        # -------------------------------------------------------------------------
        def performDragOperation_(self, sender):
            pasteboard = sender.draggingPasteboard()

            # Look for paths on the pasteboard.
            data = None
            if Cocoa.NSFilenamesPboardType in pasteboard.types():
                data = pasteboard.dataForType_(Cocoa.NSFilenamesPboardType)

            if data is not None:
                # Retrieve  paths.
                (
                    filenames,
                    plformat,
                    errorDescription,
                ) = Cocoa.NSPropertyListSerialization.propertyListFromData_mutabilityOption_format_errorDescription_(  # noqa: B950
                    data, Cocoa.kCFPropertyListImmutable, None, None
                )

                # Add paths to the data source.
                for fn in filenames:
                    self.addAnImageWithPath_(fn)

                # Make the image browser reload the data source.
                self.updateDatasource()

            # Accept the drag operation.
            return True

### main.py

    import ImageBrowserController  # noqa: F401
    from PyObjCTools import AppHelper
    import objc

    objc.setVerbose(True)

    AppHelper.runEventLoop()

### setup.py

    """
    Script for building the example.

    Usage:
        python3 setup.py py2app
    """
    from setuptools import setup

    setup(
        name="ImageBrowser",
        app=["main.py"],
        data_files=["English.lproj"],
        setup_requires=["py2app", "pyobjc-framework-Cocoa", "pyobjc-framework-Quartz"],
    )