Compare commits

..
1 Commits
Author SHA1 Message Date
TZGyn 7e9cc5b269 Drive capture selection through a CGEventTap
Panel event delivery is unreliable on macOS 26 (clicks pass through even
with the app active and the panel key). The selection flow is now driven
by a CGEventTap that sees and consumes mouse events system-wide: hover
highlights windows, clicks capture them (rounded), drags marquee-select,
Esc cancels. Requires Accessibility permission, prompted on first use;
falls back to panel events when unavailable.

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4-flash-0731
2026-09-09 12:08:57 +08:00
4 changed files with 265 additions and 189 deletions
+7
View File
@@ -90,6 +90,13 @@ enum PermissionManager {
static func request() { static func request() {
CGRequestScreenCaptureAccess() CGRequestScreenCaptureAccess()
} }
static var isAccessibilityGranted: Bool { AXIsProcessTrusted() }
static func requestAccessibility() {
let opts = [kAXTrustedCheckOptionPrompt.takeUnretainedValue(): true] as CFDictionary
_ = AXIsProcessTrustedWithOptions(opts)
}
} }
enum LoginItem { enum LoginItem {
+222 -12
View File
@@ -1,27 +1,47 @@
import AppKit import AppKit
import CoreGraphics import CoreGraphics
import ScreenCaptureKit import ScreenCaptureKit
import ApplicationServices
final class CaptureController: NSObject { final class CaptureController: NSObject {
static let shared = CaptureController() static let shared = CaptureController()
private var overlays: [NSWindow] = [] private var overlays: [OverlayWindow] = []
private(set) var isSelecting = false private(set) var isSelecting = false
private var previousApp: NSRunningApplication? private var previousApp: NSRunningApplication?
private var overlayIDs: [Int] = [] private var overlayIDs: [Int] = []
// Selection state (driven by the event tap; AppKit global coordinates)
private var marqueeStart: NSPoint?
private(set) var marqueeRect: NSRect?
private(set) var hoverFrame: NSRect?
private var trackedWindows: [(id: CGWindowID, frame: NSRect)] = []
private var lastWindowScan = Date.distantPast
private var eventTap: CFMachPort?
/// Session event log for debugging (dumped via Copy Diagnostics). /// Session event log for debugging (dumped via Copy Diagnostics).
/// Also appended to /tmp/kapture-debug.log so it survives relaunches.
static var sessionLog: [String] = [] static var sessionLog: [String] = []
static func log(_ line: String) { static func log(_ line: String) {
sessionLog.append(line) let stamped = String(format: "%@ %@", DateFormatter.localizedString(from: Date(), dateStyle: .none, timeStyle: .medium), line)
if sessionLog.count > 100 { sessionLog.removeFirst() } sessionLog.append(stamped)
if sessionLog.count > 200 { sessionLog.removeFirst() }
NSLog("[Kapture] \(line)") NSLog("[Kapture] \(line)")
let path = "/tmp/kapture-debug.log"
if let handle = FileHandle(forWritingAtPath: path) {
handle.seekToEndOfFile()
handle.write((stamped + "\n").data(using: .utf8)!)
handle.closeFile()
} else {
FileManager.default.createFile(atPath: path, contents: (stamped + "\n").data(using: .utf8))
}
} }
/// Diagnostic snapshot for "Copy Diagnostics" menu item. /// Diagnostic snapshot for "Copy Diagnostics" menu item.
func diagnostics() -> String { func diagnostics() -> String {
var lines: [String] = [] var lines: [String] = []
lines.append("Kapture diagnostics — version \(Bundle.main.infoDictionary?["CFBundleShortVersionString"] ?? "?")") lines.append("Kapture diagnostics — version \(Bundle.main.infoDictionary?["CFBundleShortVersionString"] ?? "?")")
lines.append("accessibility=\(AXIsProcessTrusted()) eventTap=\(eventTap != nil)")
lines.append("--- session log (newest last) ---") lines.append("--- session log (newest last) ---")
lines.append(contentsOf: Self.sessionLog) lines.append(contentsOf: Self.sessionLog)
lines.append("--- screens ---") lines.append("--- screens ---")
@@ -53,6 +73,8 @@ final class CaptureController: NSObject {
return lines.joined(separator: "\n") return lines.joined(separator: "\n")
} }
// MARK: - Session lifecycle
func begin() { func begin() {
guard !isSelecting else { return } guard !isSelecting else { return }
guard PermissionManager.isGranted else { guard PermissionManager.isGranted else {
@@ -61,25 +83,22 @@ final class CaptureController: NSObject {
} }
isSelecting = true isSelecting = true
previousApp = NSWorkspace.shared.frontmostApplication previousApp = NSWorkspace.shared.frontmostApplication
// Activation is ASYNC: isActive only flips on a later runloop tick // Activation is async on macOS 26; wait a tick so the app is active
// (verified on macOS 26). Show overlays only once active, otherwise // before the overlays appear (otherwise clicks pass through).
// the first click can pass through to the windows below.
NSApp.activate(ignoringOtherApps: true) NSApp.activate(ignoringOtherApps: true)
CaptureController.log("begin: activate issued, active=\(NSApp.isActive)") CaptureController.log("begin: activate issued, active=\(NSApp.isActive)")
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [weak self] in DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [weak self] in
guard let self, self.isSelecting else { return } guard let self, self.isSelecting else { return }
CaptureController.log("begin: active=\(NSApp.isActive) — showing overlays") CaptureController.log("begin: active=\(NSApp.isActive) — showing overlays")
self.overlays = NSScreen.screens.map { screen in self.overlays = NSScreen.screens.map { screen in
OverlayWindow( let panel = OverlayWindow(screen: screen, controller: self)
screen: screen, (panel.contentView as? OverlayView)?.controller = self
onComplete: { [weak self] rect in self?.finishSelection(rect) }, return panel
onCancel: { [weak self] in self?.cancelSelection() },
onWindowPick: { [weak self] rect in self?.finishWindowPick(rect) }
)
} }
self.overlays.forEach { $0.makeKeyAndOrderFront(nil) } self.overlays.forEach { $0.makeKeyAndOrderFront(nil) }
self.overlayIDs = self.overlays.map { $0.windowNumber } self.overlayIDs = self.overlays.map { $0.windowNumber }
CaptureController.log("begin: \(self.overlays.count) overlays, ids=\(self.overlayIDs), active=\(NSApp.isActive)") CaptureController.log("begin: \(self.overlays.count) overlays, ids=\(self.overlayIDs), active=\(NSApp.isActive)")
self.installEventTap()
} }
} }
@@ -113,8 +132,12 @@ final class CaptureController: NSObject {
} }
private func hideOverlays() { private func hideOverlays() {
removeEventTap()
overlays.forEach { $0.orderOut(nil) } overlays.forEach { $0.orderOut(nil) }
overlays = [] overlays = []
marqueeRect = nil
hoverFrame = nil
marqueeStart = nil
} }
private func restorePreviousApp() { private func restorePreviousApp() {
@@ -122,6 +145,193 @@ final class CaptureController: NSObject {
previousApp = nil previousApp = nil
} }
private func renderOverlays() {
for overlay in overlays {
(overlay.contentView as? OverlayView)?.needsDisplay = true
}
}
// MARK: - Event tap
private func installEventTap() {
guard eventTap == nil else { return }
guard AXIsProcessTrusted() else {
CaptureController.log("tap: accessibility not granted — falling back to panel events")
PermissionManager.requestAccessibility()
return
}
let mask: CGEventMask = (1 << CGEventType.mouseMoved.rawValue)
| (1 << CGEventType.leftMouseDown.rawValue)
| (1 << CGEventType.leftMouseDragged.rawValue)
| (1 << CGEventType.leftMouseUp.rawValue)
| (1 << CGEventType.keyDown.rawValue)
guard let tap = CGEvent.tapCreate(
tap: .cghidEventTap,
place: .headInsertEventTap,
options: .defaultTap,
eventsOfInterest: CGEventMask(mask),
callback: Self.tapCallback,
userInfo: Unmanaged.passUnretained(self).toOpaque()
) else {
CaptureController.log("tap: creation FAILED — falling back to panel events")
return
}
eventTap = tap
if let source = CFMachPortCreateRunLoopSource(nil, tap, 0) {
CFRunLoopAddSource(CFRunLoopGetMain(), source, .commonModes)
}
CGEvent.tapEnable(tap: tap, enable: true)
CaptureController.log("tap: installed")
}
private func removeEventTap() {
guard let tap = eventTap else { return }
CGEvent.tapEnable(tap: tap, enable: false)
if let source = CFMachPortCreateRunLoopSource(nil, tap, 0) {
CFRunLoopRemoveSource(CFRunLoopGetMain(), source, .commonModes)
}
CFMachPortInvalidate(tap)
eventTap = nil
CaptureController.log("tap: removed")
}
private static let tapCallback: CGEventTapCallBack = { _, type, event, refcon in
guard let refcon else { return Unmanaged.passUnretained(event) }
let controller = Unmanaged<CaptureController>.fromOpaque(refcon).takeUnretainedValue()
if let result = controller.handleTap(type, event) {
return Unmanaged.passUnretained(result)
}
return nil
}
/// Convert a CG global point (top-left origin) to AppKit global.
private func cgPointToAppKit(_ p: NSPoint) -> NSPoint {
guard let mainScreen = NSScreen.screens.first else { return p }
return NSPoint(x: p.x, y: mainScreen.frame.height - p.y)
}
/// Scan on-screen windows (AppKit global frames), front to back.
private func refreshTrackedWindows() {
guard let list = CGWindowListCopyWindowInfo(
[ .optionOnScreenOnly, .excludeDesktopElements ], kCGNullWindowID
) as? [[String: Any]] else { trackedWindows = []; return }
let ownIDs = Set(overlayIDs)
var found: [(id: CGWindowID, frame: NSRect)] = []
for info in list {
guard let cid = info[kCGWindowNumber as String] as? CGWindowID,
let bounds = info[kCGWindowBounds as String] as? [String: CGFloat],
let layer = info[kCGWindowLayer as String] as? Int,
layer == 0
else { continue }
if ownIDs.contains(Int(cid)) { continue }
let cgRect = NSRect(
x: bounds["X"] ?? 0,
y: bounds["Y"] ?? 0,
width: bounds["Width"] ?? 0,
height: bounds["Height"] ?? 0
)
guard cgRect.width >= 80, cgRect.height >= 60 else { continue }
let rect = Self.cgToAppKit(rect: cgRect)
// Skip windows filling an entire screen (fullscreen apps)
let coversWholeScreen = NSScreen.screens.contains { screen in
abs(screen.frame.width - rect.width) < 2 && abs(screen.frame.height - rect.height) < 2
}
guard !coversWholeScreen else { continue }
found.append((cid, rect))
}
trackedWindows = found
}
/// Convert a CGWindowList (top-left origin) frame into AppKit screen
/// coordinates (bottom-left origin).
private static func cgToAppKit(rect: NSRect) -> NSRect {
guard let mainScreen = NSScreen.screens.first else { return rect }
let y = mainScreen.frame.height - rect.origin.y - rect.height
return NSRect(x: rect.origin.x, y: y, width: rect.width, height: rect.height)
}
private func topmostWindowContaining(_ appKitPoint: NSPoint) -> NSRect? {
for (_, frame) in trackedWindows {
if frame.contains(appKitPoint) {
return frame
}
}
return nil
}
/// Returns the event to pass through, or nil to consume it.
func handleTap(_ type: CGEventType, _ event: CGEvent) -> CGEvent? {
guard isSelecting else { return event }
switch type {
case .tapDisabledByUserInput:
CGEvent.tapEnable(tap: eventTap!, enable: true)
return event
case .tapDisabledByTimeout:
CGEvent.tapEnable(tap: eventTap!, enable: true)
return event
case .keyDown:
if event.getIntegerValueField(.keyboardEventKeycode) == 53 {
CaptureController.log("tap: Esc — cancel")
cancelSelection()
return nil
}
return event
case .mouseMoved, .leftMouseDragged:
// Throttle window scans while moving
if Date().timeIntervalSince(lastWindowScan) > 0.15 {
lastWindowScan = Date()
refreshTrackedWindows()
}
let appKitPoint = cgPointToAppKit(NSPoint(x: event.location.x, y: event.location.y))
if type == .mouseMoved {
let frame = topmostWindowContaining(appKitPoint)
if frame != hoverFrame {
hoverFrame = frame
renderOverlays()
}
return event
}
// Drag: grow the marquee
guard let start = marqueeStart else { return nil }
let rect = NSRect(
x: min(start.x, appKitPoint.x),
y: min(start.y, appKitPoint.y),
width: abs(appKitPoint.x - start.x),
height: abs(appKitPoint.y - start.y)
)
marqueeRect = rect
hoverFrame = nil
renderOverlays()
return nil
case .leftMouseDown:
marqueeStart = cgPointToAppKit(NSPoint(x: event.location.x, y: event.location.y))
marqueeRect = nil
CaptureController.log("tap: mouseDown at \(marqueeStart.map { "\($0)" } ?? "?")")
return nil
case .leftMouseUp:
defer { marqueeStart = nil; marqueeRect = nil }
let appKitPoint = cgPointToAppKit(NSPoint(x: event.location.x, y: event.location.y))
let dragDistance = marqueeStart.map { hypot($0.x - appKitPoint.x, $0.y - appKitPoint.y) } ?? 0
if let rect = marqueeRect, dragDistance >= 4, rect.width >= 2, rect.height >= 2 {
CaptureController.log("tap: marquee complete \(rect)")
finishSelection(rect)
} else if let hover = hoverFrame {
CaptureController.log("tap: window pick \(hover)")
finishWindowPick(hover)
} else {
CaptureController.log("tap: click with no hover/marquee — cancel")
cancelSelection()
}
return nil
default:
return event
}
}
// MARK: - Capture
private func capture(_ rect: NSRect, excludingWindowIDs ids: [Int], roundedCorners: Bool = false) { private func capture(_ rect: NSRect, excludingWindowIDs ids: [Int], roundedCorners: Bool = false) {
ScreenCapture.shared.capture(rect, excludingWindowIDs: ids, roundedCorners: roundedCorners) { [weak self] image in ScreenCapture.shared.capture(rect, excludingWindowIDs: ids, roundedCorners: roundedCorners) { [weak self] image in
DispatchQueue.main.async { DispatchQueue.main.async {
+35 -176
View File
@@ -1,17 +1,20 @@
import AppKit import AppKit
/// Renders the capture overlay. All interaction state lives in
/// CaptureController (driven by the CGEventTap); this view only draws.
final class OverlayWindow: NSPanel { final class OverlayWindow: NSPanel {
init(screen: NSScreen, onComplete: @escaping (NSRect) -> Void, onCancel: @escaping () -> Void, onWindowPick: @escaping (NSRect) -> Void) { init(screen: NSScreen, controller: CaptureController) {
let content = OverlayView(onComplete: onComplete, onCancel: onCancel, onWindowPick: onWindowPick) let content = OverlayView()
super.init( super.init(
contentRect: screen.frame, contentRect: screen.frame,
styleMask: [.borderless, .nonactivatingPanel], styleMask: [.borderless],
backing: .buffered, backing: .buffered,
defer: false defer: false
) )
contentView = content contentView = content
content.controller = controller
level = .screenSaver level = .screenSaver
collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .stationary, .transient] collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .stationary]
backgroundColor = .clear backgroundColor = .clear
isOpaque = false isOpaque = false
hasShadow = false hasShadow = false
@@ -23,29 +26,7 @@ final class OverlayWindow: NSPanel {
} }
final class OverlayView: NSView { final class OverlayView: NSView {
private let onComplete: (NSRect) -> Void weak var controller: CaptureController?
private let onCancel: () -> Void
private let onWindowPick: (NSRect) -> Void
private var startPoint: NSPoint?
private(set) var selection: NSRect?
private var isDragging = false
private var dragDistance: CGFloat = 0
// Window detection (frames stored in AppKit global space)
private var trackedWindows: [(id: CGWindowID, frame: NSRect)] = []
private var hoveredWindowFrame: NSRect?
init(onComplete: @escaping (NSRect) -> Void, onCancel: @escaping () -> Void, onWindowPick: @escaping (NSRect) -> Void) {
self.onComplete = onComplete
self.onCancel = onCancel
self.onWindowPick = onWindowPick
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var acceptsFirstResponder: Bool { true } override var acceptsFirstResponder: Bool { true }
@@ -55,178 +36,56 @@ final class OverlayView: NSView {
addCursorRect(bounds, cursor: .crosshair) addCursorRect(bounds, cursor: .crosshair)
} }
// MARK: - Window detection /// The screen frame this overlay covers, in AppKit global space.
private var screenFrame: NSRect {
guard let window else { return .zero }
return window.convertToScreen(NSRect(origin: .zero, size: bounds.size))
}
/// Rebuild the list of capturable windows intersecting this screen. /// Convert an AppKit-global rect into this view's coordinates.
/// Called lazily on mouse moves; cheap enough for interactive hover. private func viewRect(_ global: NSRect) -> NSRect {
private func refreshTrackedWindows() { let sf = screenFrame
guard let window = window else { return } return NSRect(
let screenFrame = window.convertToScreen(NSRect(origin: .zero, size: bounds.size)) x: global.origin.x - sf.origin.x,
// Exclude this overlay's own window (the other overlays don't intersect y: global.origin.y - sf.origin.y,
// this screen, so the on-screen filter handles them). width: global.width,
let ownID = window.windowNumber height: global.height
guard let list = CGWindowListCopyWindowInfo(
[ .optionOnScreenOnly, .excludeDesktopElements ], kCGNullWindowID
) as? [[String: Any]] else { trackedWindows = []; return }
var found: [(CGWindowID, NSRect)] = []
for info in list {
guard let cid = info[kCGWindowNumber as String] as? CGWindowID,
let bounds = info[kCGWindowBounds as String] as? [String: CGFloat],
let layer = info[kCGWindowLayer as String] as? Int,
layer == 0
else { continue }
// Skip our own overlays
if Int(cid) == ownID { continue }
let cgRect = NSRect(
x: bounds["X"] ?? 0,
y: bounds["Y"] ?? 0,
width: bounds["Width"] ?? 0,
height: bounds["Height"] ?? 0
) )
guard cgRect.width >= 80, cgRect.height >= 60 else { continue }
// Convert to AppKit space FIRST — CG and AppKit Y axes disagree on
// multi-desktop layouts, so intersect tests must use one space.
let rect = Self.cgToAppKit(rect: cgRect)
// Skip windows filling the whole screen (fullscreen apps)
guard rect.width < screenFrame.width || rect.height < screenFrame.height else { continue }
// Only windows intersecting this screen
guard rect.intersects(screenFrame) else { continue }
found.append((cid, rect))
}
trackedWindows = found
}
private func windowFrame(at point: NSPoint) -> NSRect? {
guard let window = window else { return nil }
// convertToScreen gives AppKit-global coordinates; tracked frames are
// already in that space.
let screenPoint = window.convertToScreen(NSRect(origin: point, size: .zero)).origin
for (_, frame) in trackedWindows {
if frame.contains(screenPoint) {
return frame
}
}
return nil
}
/// Convert a CGWindowList (top-left origin, CG global space) frame into
/// AppKit screen coordinates (bottom-left origin) for capture.
private static func cgToAppKit(rect: NSRect) -> NSRect {
guard let mainScreen = NSScreen.screens.first else { return rect }
let y = mainScreen.frame.height - rect.origin.y - rect.height
return NSRect(x: rect.origin.x, y: y, width: rect.width, height: rect.height)
}
// MARK: - Events
override func mouseMoved(with event: NSEvent) {
guard !isDragging else { return }
refreshTrackedWindows()
let p = convert(event.locationInWindow, from: nil)
let frame = windowFrame(at: p)
if frame != hoveredWindowFrame {
hoveredWindowFrame = frame
needsDisplay = true
}
}
override func mouseDown(with event: NSEvent) {
let p = convert(event.locationInWindow, from: nil)
startPoint = p
dragDistance = 0
selection = NSRect(origin: p, size: .zero)
needsDisplay = true
}
override func mouseDragged(with event: NSEvent) {
guard let start = startPoint else { return }
let p = convert(event.locationInWindow, from: nil)
dragDistance = hypot(p.x - start.x, p.y - start.y)
// Only become a marquee after real movement; micro-jitter from a
// click must not cancel window-pick mode.
guard dragDistance >= 4 else { return }
isDragging = true
hoveredWindowFrame = nil
selection = NSRect(
x: min(start.x, p.x),
y: min(start.y, p.y),
width: abs(p.x - start.x),
height: abs(p.y - start.y)
)
needsDisplay = true
}
override func mouseUp(with event: NSEvent) {
defer {
isDragging = false
dragDistance = 0
}
if isDragging {
guard let sel = selection else { return }
if sel.width >= 2, sel.height >= 2 {
onComplete(window!.convertToScreen(sel))
} else {
onCancel()
}
return
}
// It was a click. If a window is hovered, capture it with rounded
// corners; otherwise treat as an empty marquee (cancel).
if let frame = hoveredWindowFrame {
onWindowPick(frame)
} else {
onCancel()
}
}
override func keyDown(with event: NSEvent) {
if event.keyCode == 53 {
onCancel()
} else {
super.keyDown(with: event)
}
} }
override func draw(_ dirtyRect: NSRect) { override func draw(_ dirtyRect: NSRect) {
NSColor.black.withAlphaComponent(0.35).setFill() NSColor.black.withAlphaComponent(0.35).setFill()
NSBezierPath(rect: bounds).fill() NSBezierPath(rect: bounds).fill()
// Highlight the hovered window (rounded, like the real window corners) guard let controller else { return }
if let frame = hoveredWindowFrame {
// Tracked frames are AppKit-global; view space = global - window origin. // Highlight the hovered window (rounded, like real window corners)
guard let window = window else { return } if let frame = controller.hoverFrame {
let screenFrame = window.convertToScreen(NSRect(origin: .zero, size: bounds.size)) let vf = viewRect(frame)
let viewFrame = NSRect(
x: frame.origin.x - screenFrame.origin.x,
y: frame.origin.y - screenFrame.origin.y,
width: frame.width,
height: frame.height
)
NSGraphicsContext.current?.saveGraphicsState() NSGraphicsContext.current?.saveGraphicsState()
NSGraphicsContext.current?.compositingOperation = .clear NSGraphicsContext.current?.compositingOperation = .clear
NSColor.black.setFill() NSColor.black.setFill()
NSBezierPath(roundedRect: viewFrame, xRadius: 10, yRadius: 10).fill() NSBezierPath(roundedRect: vf, xRadius: 10, yRadius: 10).fill()
NSGraphicsContext.current?.restoreGraphicsState() NSGraphicsContext.current?.restoreGraphicsState()
let border = NSBezierPath(roundedRect: viewFrame, xRadius: 10, yRadius: 10) let border = NSBezierPath(roundedRect: vf, xRadius: 10, yRadius: 10)
border.lineWidth = 2 border.lineWidth = 2
NSColor.controlAccentColor.setStroke() NSColor.controlAccentColor.setStroke()
border.stroke() border.stroke()
return return
} }
guard let sel = selection, sel.width > 0, sel.height > 0 else { return } // Marquee selection
guard let sel = controller.marqueeRect, sel.width > 0, sel.height > 0 else { return }
let vf = viewRect(sel)
NSGraphicsContext.current?.saveGraphicsState() NSGraphicsContext.current?.saveGraphicsState()
NSGraphicsContext.current?.compositingOperation = .clear NSGraphicsContext.current?.compositingOperation = .clear
NSColor.black.setFill() NSColor.black.setFill()
NSBezierPath(rect: sel).fill() NSBezierPath(rect: vf).fill()
NSGraphicsContext.current?.restoreGraphicsState() NSGraphicsContext.current?.restoreGraphicsState()
let border = NSBezierPath(rect: sel) let border = NSBezierPath(rect: vf)
border.lineWidth = 1.5 border.lineWidth = 1.5
NSColor.controlAccentColor.setStroke() NSColor.controlAccentColor.setStroke()
border.stroke() border.stroke()
@@ -238,8 +97,8 @@ final class OverlayView: NSView {
] ]
let labelSize = label.size(withAttributes: attrs) let labelSize = label.size(withAttributes: attrs)
let labelRect = NSRect( let labelRect = NSRect(
x: sel.maxX - labelSize.width - 8, x: vf.maxX - labelSize.width - 8,
y: sel.minY - labelSize.height - 10, y: vf.minY - labelSize.height - 10,
width: labelSize.width + 8, width: labelSize.width + 8,
height: labelSize.height + 6 height: labelSize.height + 6
) )
+2 -2
View File
@@ -108,9 +108,9 @@ cat > "$APP/Contents/Info.plist" <<'PLIST'
<key>CFBundleIconFile</key> <key>CFBundleIconFile</key>
<string>Icon</string> <string>Icon</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.6.5</string> <string>1.7.0</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>65</string> <string>70</string>
<key>LSUIElement</key> <key>LSUIElement</key>
<true/> <true/>
<key>NSHighResolutionCapable</key> <key>NSHighResolutionCapable</key>