TLayer

A simple demo of Transparency Layers

Sources

AppDelegate.py

import objc
import Cocoa
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

Circle.py

import Cocoa
import Quartz

import objc
import math

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)

Extras.py

import Cocoa
import objc
import random

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

ShadowOffsetView.py

import Cocoa
import Quartz
import objc

import math

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)

TLayerDemo.py

import Cocoa
import Quartz
import objc
from objc import super

import ShadowOffsetView


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(TLayerDemo, self).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(TLayerDemo, self).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)

TLayerView.py

import Cocoa
import Quartz
import objc
from objc import super

from Extras import makeRandomPointInRect
from Circle import Circle

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(TLayerView, self).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(TLayerView, self).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))

main.py

from PyObjCTools import AppHelper

import objc; objc.setVerbose(True)
import AppDelegate
import Circle
import Extras
import ShadowOffsetView
import TLayerDemo
import TLayerView


AppHelper.runEventLoop()

setup.py

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

Resources