Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,24 @@ configuration.zoomValues = [.xxl, .s, .l, .xs]
SCColorSampler.sample(configuration: configuration) { ... }
```

#### Zoom with Mouse Wheel Inverted
```swift
// For example:
configuration.zoomWheelInverse = true
```

#### Change Loupe Location to avoid sight blocking
```swift
// For example:
configuration.loupeShape = .rect
configuration.loupeFollowDistance = 10
configuration.loupeSize = .recOnly(256, 64)

//configuration.loupeShape = .circle
//configuration.loupeFollowDistance = 1
//configuration.loupeSize = .large
```

#### Show Color Description
<p float="left">
<img alt="Image showing showColorDescription off option" src="./readme_assets/off.png" width="100" height="125">
Expand Down
113 changes: 107 additions & 6 deletions Sources/ColorSampler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Combine
import Foundation
import ScreenCaptureKit
import struct SwiftUI.Binding
import Carbon.HIToolbox

internal class ColorSampler: NSObject {
// Properties
Expand All @@ -20,6 +21,8 @@ internal class ColorSampler: NSObject {

var onMouseMovedHandlerBlock: ((NSColor) -> Void)?
var selectionHandlerBlock: ((NSColor?) -> Void)?
var monitors: [Any?] = []
var isRunning: Bool = false;

// Functions
func sample(
Expand All @@ -40,14 +43,19 @@ internal class ColorSampler: NSObject {
return
}

// Make window a little bigger than use specified
let loupeSize = configuration.loupeSize.getSize()
let samplerWindowWidth = loupeSize.width + configuration.padding * 2
let samplerWindowHeight = loupeSize.height + configuration.padding * 2

var windowInit: (
contentRect: NSRect,
styleMask: NSWindow.StyleMask,
backing: NSWindow.BackingStoreType,
defer: Bool
) {
return (
NSRect.init(origin: .zero, size: configuration.loupeSize.getSize()),
NSRect.init(origin: .zero, size: CGSize(width: samplerWindowWidth, height: samplerWindowHeight)),
NSWindow.StyleMask.borderless,
NSWindow.BackingStoreType.buffered,
true
Expand Down Expand Up @@ -75,15 +83,18 @@ internal class ColorSampler: NSObject {
name: NSWindow.didResignKeyNotification,
object: self.colorSamplerWindow
)

NSApplication.shared.activate(ignoringOtherApps: true)
self.colorSamplerWindow?.makeKeyAndOrderFront(self)
addMouseMonitor()
// 这里有问题,激活放大镜后,其它程序都变灰色了,取色就不对了(已修复)
// NSApplication.shared.activate(ignoringOtherApps: false)
self.colorSamplerWindow?.orderFront(self) // 不能变成 key
self.colorSamplerWindow?.orderedIndex = 0

// prepare image for window's contentView in advance
self.colorSamplerWindow?.mouseMoved(with: NSEvent())

NSCursor.hide()
self.isRunning = true
if self.configuration?.loupeFollowMode == .center {
NSCursor.hide()
}
}

func reset() {
Expand All @@ -99,4 +110,94 @@ internal class ColorSampler: NSObject {
self.onMouseMovedHandlerBlock = nil
self.selectionHandlerBlock = nil
}

func colorSelected() {
self.isRunning = false
self.colorSamplerWindow?.finalizeColor()
self.removeMonitors()
self.reset()
}

func cancel() {
self.isRunning = false
self.colorSamplerWindow?.cancel()
self.removeMonitors()
self.reset()
}

func addMouseMonitor() {

// 假如鼠标移动过快导致窗口跟不上,需要此函数来找回监听。和键盘相关的全局事件需要辅助功能权限
// 目前已经将隐形窗口放大(SCColorSamplerConfiguration.padding),这种情况应该很少出现了,如果出现,就需要这里发挥作用
let global_mouseMoved = NSEvent.addGlobalMonitorForEvents(matching: .mouseMoved) { e in
self.colorSamplerWindow?.mouseMoved(with: e)
}
monitors.append(global_mouseMoved)

let local_mouseExited = NSEvent.addLocalMonitorForEvents(matching: .mouseExited) { e in
self.colorSamplerWindow?.mouseMoved(with: e)
return e
}
monitors.append(local_mouseExited)

// 该事件只监听除了自身以外的程序,用于在鼠标按下捕获颜色,和键盘相关的全局事件需要辅助功能权限
let global_mouse_down = NSEvent.addGlobalMonitorForEvents(matching: .leftMouseDown) { e in
guard self.isRunning else {
return
}
self.colorSelected()
}
monitors.append(global_mouse_down)

// 用于在按下ESC关闭取色窗口,回车取色,和键盘相关的全局事件需要辅助功能权限
let global_key = NSEvent.addGlobalMonitorForEvents(matching: .keyDown) { e in
guard self.isRunning else {
return
}
if e.keyCode == kVK_Escape {
self.cancel()
}
if e.keyCode == kVK_Return {
self.colorSelected()
}
}
monitors.append(global_key)
// 鼠标左键取色
let local_mouse = NSEvent.addLocalMonitorForEvents(matching: .leftMouseDown) { e in
guard self.isRunning else {
return e
}
self.colorSelected()
return e
}
monitors.append(local_mouse)

// 用于在按下ESC关闭取色窗口,回车取色
let local_key = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { e in
guard self.isRunning else {
return e
}
if e.keyCode == kVK_Escape {
self.cancel()
}
if e.keyCode == kVK_Return {
self.colorSelected()
}
return e
}
monitors.append(local_key)
}

func removeMonitors() {
for i in 0 ..< self.monitors.count {
if let m = self.monitors[i] {
do {
NSEvent.removeMonitor(m)
} catch {
}
}
}
// 防止出现 Thread 1: EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
self.monitors = []
}
}
81 changes: 60 additions & 21 deletions Sources/ColorSamplerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,34 @@ internal class ColorSamplerView: NSView {
var image: Binding<CGImage?>!
var loupeColor: Binding<NSColor>!

var quality: SCColorSamplerConfiguration.Quality!
var shape: SCColorSamplerConfiguration.LoupeShape!
var config: SCColorSamplerConfiguration!
var frameRect: NSRect!

init(
frame frameRect: NSRect,
zoom: Binding<SCColorSamplerConfiguration.ZoomValue?>,
image: Binding<CGImage?>,
loupeColor: Binding<NSColor>,
shape: SCColorSamplerConfiguration.LoupeShape,
quality: SCColorSamplerConfiguration.Quality
config: SCColorSamplerConfiguration
) {
self.zoom = zoom
self.image = image
self.quality = quality
self.loupeColor = loupeColor
self.shape = shape
self.config = config
self.frameRect = frameRect
super.init(
frame: frameRect
)
}

private func getUserViewFrame() -> NSRect {
let windowFrame = self.window!.frame
var size: CGSize = config.loupeSize.getSize()
let originOffset = config.loupeFollowMode == .noBlock ? config.padding : 0
var origin: CGPoint = .init(x: self.frameRect.origin.x + config.padding, y: self.frameRect.origin.y + originOffset)
return .init(origin: origin, size: size)
}

required init?(coder: NSCoder) {
super.init(coder: coder)
}
Expand All @@ -48,14 +55,40 @@ internal class ColorSamplerView: NSView {
// Weird ??
fatalError()
}

// Clear the drawing rect.
context.clear(self.bounds)

let rect = self.bounds

let width: CGFloat = rect.width
let height: CGFloat = rect.height
let quality = config.quality
let shape = config.loupeShape
let windowRect: NSRect = window!.frame

// User specified region
// 这个 rect 是放大镜的绘画区域,它的坐标系是相对于这个view本身,因此它的原点不是零点,而是(P, P), P=config.padding
let rect: NSRect = .init(origin: .init(x: config.padding, y: config.padding), size: config.loupeSize.getSize())

// 以下debug信息非常重要,保留
// print("window frame \(self.window!.frame.debugDescription)")
// print("view frame \(self.frame.debugDescription)")
// print("draw zone: \(rect.debugDescription)")
//
// // Invisible window for debug
// context.setLineWidth(4.0)
// context.setStrokeColor(CGColor(red: 255, green: 0, blue: 0, alpha: 1))
// var shape1: SCColorSamplerConfiguration.LoupeShape = .rect
// context.addPath(shape1.path(in: rect))
// context.strokePath()
//
// // Inviisible bounds window for debug
// context.setLineWidth(4.0)
// context.setStrokeColor(CGColor(red: 0, green: 255, blue: 0, alpha: 1))
// var shape2: SCColorSamplerConfiguration.LoupeShape = .rect
// context.addPath(shape2.path(in: self.bounds))
// context.strokePath()
//
// // Inviisible Out window for debug
// context.setLineWidth(4.0)
// context.setStrokeColor(CGColor(red: 0, green: 0, blue: 255, alpha: 1))
// var shape3: SCColorSamplerConfiguration.LoupeShape = .rect
// context.addPath(shape3.path(in: windowRect))
// context.strokePath()
// 以上debug信息非常重要,保留

// mask
let path = shape.path(in: rect)
Expand All @@ -68,15 +101,19 @@ internal class ColorSamplerView: NSView {
}

// draw image
let width: CGFloat = rect.width
let height: CGFloat = rect.height

context.setRenderingIntent(.relativeColorimetric)
context.interpolationQuality = .none
context.draw(image, in: rect)

// Get dimensions
let apertureSize: CGFloat = zoom.getApertureSize()

let x: CGFloat = (width / 2.0) - (apertureSize / 2.0)
let y: CGFloat = (height / 2.0) - (apertureSize / 2.0)
// 孔径位置
let x: CGFloat = (self.frameRect.width / 2.0) - (apertureSize / 2.0)
let y: CGFloat = (self.frameRect.height / 2.0) - (apertureSize / 2.0)

// Square pattern
let replicatorLayer = CAReplicatorLayer()
Expand All @@ -85,15 +122,15 @@ internal class ColorSamplerView: NSView {
let squareSize = zoom.getSquarePatternSize()
let squareDisplacement = zoom.getSquarePatternDisplacement()
square.borderWidth = 0.5
square.borderColor = .black.copy(alpha: 0.15)
square.borderColor = .black.copy(alpha: 0.05)
square.frame = CGRect(x: x - (squareSize * 25),
y: y - (squareSize * 25),
width: squareSize,
height: squareSize)

let instanceCount = 50
replicatorLayer.instanceCount = instanceCount
let instanceCount: Double = 50

replicatorLayer.instanceCount = Int(instanceCount)
replicatorLayer.instanceTransform = CATransform3DMakeTranslation(squareSize, squareDisplacement, 0)

replicatorLayer.addSublayer(square)
Expand All @@ -102,7 +139,7 @@ internal class ColorSamplerView: NSView {

outerReplicatorLayer.addSublayer(replicatorLayer)

outerReplicatorLayer.instanceCount = instanceCount
outerReplicatorLayer.instanceCount = Int(instanceCount)
outerReplicatorLayer.instanceTransform = CATransform3DMakeTranslation(squareDisplacement, squareSize, 0)

outerReplicatorLayer.render(in: context)
Expand All @@ -111,13 +148,15 @@ internal class ColorSamplerView: NSView {
let apertureRect = CGRect(x: x, y: y, width: apertureSize, height: apertureSize)
context.setLineWidth(zoom.getApertureLineWidth())
context.setStrokeColor(loupeColor.wrappedValue.cgColor)
// context.setStrokeColor(CGColor(red: 255, green: 0, blue: 0, alpha: 1))
context.setShouldAntialias(false)
context.stroke(apertureRect.insetBy(dx: zoom.getInsetAmount(), dy: zoom.getInsetAmount()))

// Stroke outer rectangle
context.setShouldAntialias(true)
context.setLineWidth(4.0)
context.setStrokeColor(loupeColor.wrappedValue.cgColor)
//context.setStrokeColor(CGColor(red: 0, green: 255, blue: 0, alpha: 1))
context.addPath(path)
context.strokePath()
}
Expand Down
Loading