TLayer
======

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

A PyObjC Example without documentation

.. rst-class:: tabber

Sources
-------

.. rst-class:: tabbertab

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

.. sourcecode:: python

    import Cocoa
    import objc
    import TLayerDemo
    
    
    class AppDelegate(Cocoa.NSObject):
        shadowDemo = objc.ivar()
    
        def applicationDidFinishLaunching_(self, notification):
            self.showTLayerDemoWindow_(self)
    
        @objc.IBAction
        def showTLayerDemoWindow_(self, sender):
            if self.shadowDemo is None:
                self.shadowDemo = TLayerDemo.TLayerDemo.alloc().init()
    
            self.shadowDemo.window().orderFront_(self)
    
        def applicationShouldTerminateAfterLastWindowClosed_(self, app):
            return True

.. rst-class:: tabbertab

Circle.py
.........

.. sourcecode:: python

    import math
    
    import Cocoa
    import objc
    import Quartz  # noqa: F401
    
    
    class Circle(Cocoa.NSObject):
        radius = objc.ivar(type=objc._C_FLT)
        center = objc.ivar(type=Cocoa.NSPoint.__typestr__)
        color = objc.ivar()
    
        def bounds(self):
            return Cocoa.NSMakeRect(
                self.center.x - self.radius,
                self.center.y - self.radius,
                2 * self.radius,
                2 * self.radius,
            )
    
        def draw(self):
            context = Cocoa.NSGraphicsContext.currentContext().graphicsPort()
    
            self.color.set()
            Cocoa.CGContextSetGrayStrokeColor(context, 0, 1)
            Cocoa.CGContextSetLineWidth(context, 1.5)
    
            Cocoa.CGContextSaveGState(context)
    
            Cocoa.CGContextTranslateCTM(context, self.center.x, self.center.y)
            Cocoa.CGContextScaleCTM(context, self.radius, self.radius)
            Cocoa.CGContextMoveToPoint(context, 1, 0)
            Cocoa.CGContextAddArc(context, 0, 0, 1, 0, 2 * math.pi, False)
            Cocoa.CGContextClosePath(context)
    
            Cocoa.CGContextRestoreGState(context)
            Cocoa.CGContextDrawPath(context, Cocoa.kCGPathFill)

.. rst-class:: tabbertab

Extras.py
.........

.. sourcecode:: python

    import random
    
    import Cocoa
    import objc
    
    
    class NSColor(objc.Category(Cocoa.NSColor)):
        @classmethod
        def randomColor(self):
            return Cocoa.NSColor.colorWithCalibratedRed_green_blue_alpha_(
                random.uniform(0, 1), random.uniform(0, 1), random.uniform(0, 1), 1
            )
    
    
    def makeRandomPointInRect(rect):
        return Cocoa.NSPoint(
            x=random.uniform(Cocoa.NSMinX(rect), Cocoa.NSMaxX(rect)),
            y=random.uniform(Cocoa.NSMinY(rect), Cocoa.NSMaxY(rect)),
        )

.. rst-class:: tabbertab

ShadowOffsetView.py
...................

.. sourcecode:: python

    import math
    
    import Cocoa
    import objc
    import Quartz
    
    ShadowOffsetChanged = "ShadowOffsetChanged"
    
    
    class ShadowOffsetView(Cocoa.NSView):
        _offset = objc.ivar(type=Quartz.CGSize.__typestr__)
        _scale = objc.ivar(type=objc._C_FLT)
    
        def scale(self):
            return self._scale
    
        def setScale_(self, scale):
            self._scale = scale
    
        def offset(self):
            return Quartz.CGSizeMake(
                self._offset.width * self._scale, self._offset.height * self._scale
            )
    
        def setOffset_(self, offset):
            offset = Quartz.CGSizeMake(
                offset.width / self._scale, offset.height / self._scale
            )
            if self._offset != offset:
                self._offset = offset
                self.setNeedsDisplay_(True)
    
        def isOpaque(self):
            return False
    
        def setOffsetFromPoint_(self, point):
            bounds = self.bounds()
            offset = Quartz.CGSize(
                width=(point.x - Cocoa.NSMidX(bounds)) / (Cocoa.NSWidth(bounds) / 2),
                height=(point.y - Cocoa.NSMidY(bounds)) / (Cocoa.NSHeight(bounds) / 2),
            )
            radius = math.sqrt(offset.width * offset.width + offset.height * offset.height)
            if radius > 1:
                offset.width /= radius
                offset.height /= radius
    
            if self._offset != offset:
                self._offset = offset
                self.setNeedsDisplay_(True)
                Cocoa.NSNotificationCenter.defaultCenter().postNotificationName_object_(
                    ShadowOffsetChanged, self
                )
    
        def mouseDown_(self, event):
            point = self.convertPoint_fromView_(event.locationInWindow(), None)
            self.setOffsetFromPoint_(point)
    
        def mouseDragged_(self, event):
            point = self.convertPoint_fromView_(event.locationInWindow(), None)
            self.setOffsetFromPoint_(point)
    
        def drawRect_(self, rect):
            bounds = self.bounds()
            x = Cocoa.NSMinX(bounds)
            y = Cocoa.NSMinY(bounds)
            w = Cocoa.NSWidth(bounds)
            h = Cocoa.NSHeight(bounds)
            r = min(w / 2, h / 2)
    
            context = Cocoa.NSGraphicsContext.currentContext().graphicsPort()
    
            Quartz.CGContextTranslateCTM(context, x + w / 2, y + h / 2)
    
            Quartz.CGContextAddArc(context, 0, 0, r, 0, math.pi, True)
            Quartz.CGContextClip(context)
    
            Quartz.CGContextSetGrayFillColor(context, 0.910, 1)
            Quartz.CGContextFillRect(context, Quartz.CGRectMake(-w / 2, -h / 2, w, h))
    
            Quartz.CGContextAddArc(context, 0, 0, r, 0, 2 * math.pi, True)
            Quartz.CGContextSetGrayStrokeColor(context, 0.616, 1)
            Quartz.CGContextStrokePath(context)
    
            Quartz.CGContextAddArc(context, 0, -2, r, 0, 2 * math.pi, True)
            Quartz.CGContextSetGrayStrokeColor(context, 0.784, 1)
            Quartz.CGContextStrokePath(context)
    
            Quartz.CGContextMoveToPoint(context, 0, 0)
            Quartz.CGContextAddLineToPoint(
                context, r * self._offset.width, r * self._offset.height
            )
    
            Quartz.CGContextSetLineWidth(context, 2)
            Quartz.CGContextSetGrayStrokeColor(context, 0.33, 1)
            Quartz.CGContextStrokePath(context)

.. rst-class:: tabbertab

TLayerDemo.py
.............

.. sourcecode:: python

    import Cocoa
    import objc
    import Quartz
    import ShadowOffsetView
    from objc import super, nil  # noqa: A004
    
    
    class TLayerDemo(Cocoa.NSObject):
        colorWell = objc.IBOutlet()
        shadowOffsetView = objc.IBOutlet()
        shadowRadiusSlider = objc.IBOutlet()
        tlayerView = objc.IBOutlet()
        transparencyLayerButton = objc.IBOutlet()
    
        @classmethod
        def initialize(self):
            Cocoa.NSColorPanel.sharedColorPanel().setShowsAlpha_(True)
    
        def init(self):
            self = super().init()
            if self is None:
                return None
    
            if not Cocoa.NSBundle.loadNibNamed_owner_("TLayerDemo", self):
                Cocoa.NSLog("Failed to load TLayerDemo.nib")
                return nil
    
            self.shadowOffsetView.setScale_(40)
            self.shadowOffsetView.setOffset_(Quartz.CGSizeMake(-30, -30))
            self.tlayerView.setShadowOffset_(Quartz.CGSizeMake(-30, -30))
    
            self.shadowRadiusChanged_(self.shadowRadiusSlider)
    
            # Better to do this as a subclass of NSControl....
            Cocoa.NSNotificationCenter.defaultCenter().addObserver_selector_name_object_(
                self, b"shadowOffsetChanged:", ShadowOffsetView.ShadowOffsetChanged, None
            )
            return self
    
        def dealloc(self):
            Cocoa.NSNotificationCenter.defaultCenter().removeObserver_(self)
            super().dealloc()
    
        def window(self):
            return self.tlayerView.window()
    
        @objc.IBAction
        def shadowRadiusChanged_(self, sender):
            self.tlayerView.setShadowRadius_(self.shadowRadiusSlider.floatValue())
    
        @objc.IBAction
        def toggleTransparencyLayers_(self, sender):
            self.tlayerView.setUsesTransparencyLayers_(self.transparencyLayerButton.state())
    
        def shadowOffsetChanged_(self, notification):
            offset = notification.object().offset()
            self.tlayerView.setShadowOffset_(offset)

.. rst-class:: tabbertab

TLayerView.py
.............

.. sourcecode:: python

    import Cocoa
    import objc
    import Quartz
    from Circle import Circle
    from Extras import makeRandomPointInRect
    from objc import super  # noqa: A004
    
    gCircleCount = 3
    
    
    class NSEvent(objc.Category(Cocoa.NSEvent)):
        def locationInView_(self, view):
            return view.convertPoint_fromView_(self.locationInWindow(), None)
    
    
    class TLayerView(Cocoa.NSView):
        circles = objc.ivar()
        shadowRadius = objc.ivar(type=objc._C_FLT)
        shadowOffset = objc.ivar(type=Quartz.CGSize.__typestr__)
        useTLayer = objc.ivar(type=objc._C_BOOL)
    
        def initWithFrame_(self, frame):
            circleRadius = 100
            colors = [(0.5, 0.0, 0.5, 1), (1.0, 0.7, 0.0, 1), (0.0, 0.5, 0.0, 1)]
    
            self = super().initWithFrame_(frame)
            if self is None:
                return None
    
            self.useTLayer = False
            self.circles = []
    
            for c in colors:
                color = Cocoa.NSColor.colorWithCalibratedRed_green_blue_alpha_(*c)
                circle = Circle.alloc().init()
                circle.color = color
                circle.radius = circleRadius
                circle.center = makeRandomPointInRect(self.bounds())
                self.circles.append(circle)
    
            self.registerForDraggedTypes_([Cocoa.NSColorPboardType])
            self.setNeedsDisplay_(True)
            return self
    
        def setShadowRadius_(self, radius):
            if radius != self.shadowRadius:
                self.shadowRadius = radius
                self.setNeedsDisplay_(True)
    
        def setShadowOffset_(self, offset):
            if self.shadowOffset != offset:
                self.shadowOffset = offset
                self.setNeedsDisplay_(True)
    
        def setUsesTransparencyLayers_(self, state):
            if self.useTLayer != state:
                self.useTLayer = state
                self.setNeedsDisplay_(True)
    
        def isOpaque(self):
            return True
    
        def acceptsFirstMouse_(self, event):
            return True
    
        def boundsForCircle_(self, circle):
            dx = 2 * abs(self.shadowOffset.width) + 2 * self.shadowRadius
            dy = 2 * abs(self.shadowOffset.height) + 2 * self.shadowRadius
            return Cocoa.NSInsetRect(circle.bounds(), -dx, -dy)
    
        def dragCircleAtIndex_withEvent_(self, index, event):
            circle = self.circles[index]
            del self.circles[index]
            self.circles.append(circle)
    
            self.setNeedsDisplayInRect_(self.boundsForCircle_(circle))
    
            mask = Cocoa.NSLeftMouseDraggedMask | Cocoa.NSLeftMouseUpMask
    
            start = event.locationInView_(self)
    
            while 1:
                event = self.window().nextEventMatchingMask_(mask)
                if event.type() == Cocoa.NSLeftMouseUp:
                    break
    
                self.setNeedsDisplayInRect_(self.boundsForCircle_(circle))
    
                center = circle.center
                point = event.locationInView_(self)
                center.x += point.x - start.x
                center.y += point.y - start.y
                circle.center = center
    
                self.setNeedsDisplayInRect_(self.boundsForCircle_(circle))
    
                start = point
    
        def indexOfCircleAtPoint_(self, point):
            for idx, circle in reversed(list(enumerate(self.circles))):
                center = circle.center
                radius = circle.radius
                dx = point.x - center.x
                dy = point.y - center.y
                if dx * dx + dy * dy < radius * radius:
                    return idx
            return -1
    
        def mouseDown_(self, event):
            point = event.locationInView_(self)
            index = self.indexOfCircleAtPoint_(point)
            if index >= 0:
                self.dragCircleAtIndex_withEvent_(index, event)
    
        def setFrame_(self, frame):
            super().setFrame_(frame)
            self.setNeedsDisplay_(True)
    
        def drawRect_(self, rect):
            context = Cocoa.NSGraphicsContext.currentContext().graphicsPort()
    
            Quartz.CGContextSetRGBFillColor(context, 0.7, 0.7, 0.9, 1)
            Quartz.CGContextFillRect(context, rect)
    
            Quartz.CGContextSetShadow(context, self.shadowOffset, self.shadowRadius)
    
            if self.useTLayer:
                Quartz.CGContextBeginTransparencyLayer(context, None)
    
            for circle in self.circles:
                bounds = self.boundsForCircle_(circle)
                if Cocoa.NSIntersectsRect(bounds, rect):
                    circle.draw()
    
            if self.useTLayer:
                Quartz.CGContextEndTransparencyLayer(context)
    
        def draggingEntered_(self, sender):
            # Since we have only registered for NSColorPboardType drags, this is
            # actually unneeded. If you were to register for any other drag types,
            # though, this code would be necessary.
    
            if (sender.draggingSourceOperationMask() & Cocoa.NSDragOperationGeneric) != 0:
                pasteboard = sender.draggingPasteboard()
                if pasteboard.types().containsObject_(Cocoa.NSColorPboardType):
                    return Cocoa.NSDragOperationGeneric
    
            return Cocoa.NSDragOperationNone
    
        def performDragOperation_(self, sender):
            point = self.convertPoint_fromView_(sender.draggingLocation(), None)
            index = self.indexOfCircleAtPoint_(point)
    
            if index >= 0:
                # The current drag location is inside the bounds of a circle so we
                # accept the drop and move on to concludeDragOperation:.
                return True
    
            return False
    
        def concludeDragOperation_(self, sender):
            color = Cocoa.NSColor.colorFromPasteboard_(sender.draggingPasteboard())
            point = self.convertPoint_fromView_(sender.draggingLocation(), None)
            index = self.indexOfCircleAtPoint_(point)
    
            if index >= 0:
                circle = self.circles[index]
                circle.color = color
                self.setNeedsDisplayInRect_(self.boundsForCircle_(circle))

.. rst-class:: tabbertab

main.py
.......

.. sourcecode:: python

    import AppDelegate  # noqa: F401
    import Circle  # noqa: F401
    import Extras  # noqa: F401
    import ShadowOffsetView  # noqa: F401
    import TLayerDemo  # noqa: F401
    import TLayerView  # 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="TLayer",
        app=["main.py"],
        data_files=["English.lproj"],
        setup_requires=["py2app", "pyobjc-framework-Cocoa", "pyobjc-framework-Quartz"],
    )

