ImageBrowser
============

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

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
      exemple 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.


.. rst-class:: tabber

Sources
-------

.. rst-class:: tabbertab

ImageBrowserController.py
.........................

.. sourcecode:: python

    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

.. rst-class:: tabbertab

main.py
.......

.. sourcecode:: python

    import ImageBrowserController  # noqa: F401
    from PyObjCTools import AppHelper
    import objc
    
    objc.setVerbose(True)
    
    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="ImageBrowser",
        app=["main.py"],
        data_files=["English.lproj"],
        setup_requires=["py2app", "pyobjc-framework-Cocoa", "pyobjc-framework-Quartz"],
    )

