303 lines
9.3 KiB
Swift
303 lines
9.3 KiB
Swift
import Cocoa
|
|
import Darwin
|
|
|
|
extension NSAppearance {
|
|
func getThemeName() -> AppearanceThemePreference {
|
|
if #available(macOS 10.14, *) {
|
|
let appearance = NSApp.effectiveAppearance.name
|
|
if appearance == .darkAqua || appearance == .vibrantDark {
|
|
return .dark
|
|
}
|
|
}
|
|
return .light
|
|
}
|
|
}
|
|
|
|
extension NSColor {
|
|
// periphery:ignore
|
|
func toHex() -> String? {
|
|
guard let rgbColor = usingColorSpace(.deviceRGB) else {
|
|
return nil
|
|
}
|
|
|
|
let red = Int(rgbColor.redComponent * 255.0)
|
|
let green = Int(rgbColor.greenComponent * 255.0)
|
|
let blue = Int(rgbColor.blueComponent * 255.0)
|
|
|
|
return String(format: "#%02X%02X%02X", red, green, blue)
|
|
}
|
|
|
|
class var systemAccentColor: NSColor {
|
|
if #available(macOS 10.14, *) {
|
|
// dynamically adapts to changes in System Default; no need to listen to notifications
|
|
return NSColor.controlAccentColor
|
|
}
|
|
return NSColor.blue
|
|
}
|
|
|
|
class var tableBorderColor: NSColor {
|
|
// #4b4b4b
|
|
if NSAppearance.current.getThemeName() == .dark {
|
|
return NSColor(srgbRed: 75/255, green: 75/255, blue: 75/255, alpha: 0.8)
|
|
|
|
}
|
|
// #e5e5e5
|
|
return NSColor(srgbRed: 229/255, green: 229/255, blue: 229/255, alpha: 0.8)
|
|
}
|
|
|
|
class var tableBackgroundColor: NSColor {
|
|
// #2b2b2b
|
|
if NSAppearance.current.getThemeName() == .dark {
|
|
return NSColor(srgbRed: 43/255, green: 43/255, blue: 43/255, alpha: 0.8)
|
|
|
|
}
|
|
// #f2f2f2
|
|
return NSColor(srgbRed: 242/255, green: 242/255, blue: 242/255, alpha: 0.8)
|
|
}
|
|
|
|
class var tableSeparatorColor: NSColor {
|
|
// #353535
|
|
if NSAppearance.current.getThemeName() == .dark {
|
|
return NSColor(srgbRed: 53/255, green: 53/255, blue: 53/255, alpha: 0.8)
|
|
|
|
}
|
|
// #e7e7e7
|
|
return NSColor(srgbRed: 231/255, green: 231/255, blue: 231/255, alpha: 0.8)
|
|
}
|
|
|
|
class var tableHoverColor: NSColor {
|
|
// #363636
|
|
if NSAppearance.current.getThemeName() == .dark {
|
|
return NSColor(srgbRed: 54/255, green: 54/255, blue: 54/255, alpha: 0.8)
|
|
|
|
}
|
|
// #ebebeb
|
|
return NSColor(srgbRed: 235/255, green: 235/255, blue: 235/255, alpha: 0.8)
|
|
}
|
|
}
|
|
|
|
extension NSView {
|
|
// constrain size to fittingSize
|
|
func fit() {
|
|
addOrUpdateConstraint(widthAnchor, fittingSize.width)
|
|
addOrUpdateConstraint(heightAnchor, fittingSize.height)
|
|
}
|
|
|
|
// constrain size to provided width and height
|
|
func fit(_ width: CGFloat, _ height: CGFloat) {
|
|
addOrUpdateConstraint(widthAnchor, width)
|
|
addOrUpdateConstraint(heightAnchor, height)
|
|
}
|
|
|
|
func addOrUpdateConstraint(_ anchor: NSLayoutDimension, _ constant: CGFloat) {
|
|
if let constraint = (constraints.first { $0.firstAnchor == anchor && $0.secondAnchor == nil }) {
|
|
constraint.constant = constant
|
|
} else {
|
|
anchor.constraint(equalToConstant: constant).isActive = true
|
|
}
|
|
}
|
|
|
|
func centerFrameInParent(x: Bool = false, y: Bool = false) {
|
|
let selfSize = (self is NSTextField) ? (self as! NSTextField).cell!.cellSize : frame.size
|
|
let superviewSize = (superview! is NSTextField) ? (superview! as! NSTextField).cell!.cellSize : superview!.frame.size
|
|
if (x) {
|
|
frame.origin.x = ((superviewSize.width - selfSize.width) / 2).rounded()
|
|
}
|
|
if (y) {
|
|
let diff = superviewSize.height - selfSize.height
|
|
// if there is no perfect centering, we biais top, as it's more aesthetic for ThumbnailView.label
|
|
let diffWithBiasTop = diff.truncatingRemainder(dividingBy: 2) == 0 ? diff : diff - 1
|
|
frame.origin.y = (diffWithBiasTop / 2).rounded()
|
|
}
|
|
}
|
|
|
|
func setSubviews(_ views: [NSView]) {
|
|
for view in views {
|
|
normalizeSubview(view)
|
|
}
|
|
subviews = views
|
|
}
|
|
|
|
func addSubviews(_ views: [NSView]) {
|
|
for view in views {
|
|
normalizeSubview(view)
|
|
}
|
|
subviews = subviews + views
|
|
}
|
|
|
|
func setSubviewAbove(_ view: NSView) {
|
|
normalizeSubview(view)
|
|
addSubview(view, positioned: .above, relativeTo: nil)
|
|
}
|
|
|
|
private func normalizeSubview(_ view: NSView) {
|
|
view.translatesAutoresizingMaskIntoConstraints = false
|
|
}
|
|
}
|
|
|
|
extension Collection {
|
|
subscript(safe index: Index) -> Element? {
|
|
return indices.contains(index) ? self[index] : nil
|
|
}
|
|
}
|
|
|
|
// allow using a closure for NSControl action, instead of selector
|
|
class SelectorWrapper<T> {
|
|
let selector: Selector
|
|
let closure: (T) -> Void
|
|
|
|
init(withClosure closure: @escaping (T) -> Void) {
|
|
self.selector = #selector(callClosure)
|
|
self.closure = closure
|
|
}
|
|
|
|
@objc
|
|
private func callClosure(sender: AnyObject) {
|
|
closure(sender as! T)
|
|
}
|
|
}
|
|
|
|
fileprivate var handle: Int = 0
|
|
|
|
typealias ActionClosure = (NSControl) -> Void
|
|
|
|
extension NSControl {
|
|
var onAction: ActionClosure? {
|
|
get {
|
|
return nil
|
|
}
|
|
set {
|
|
if let newValue = newValue {
|
|
let selectorWrapper = SelectorWrapper<NSControl>(withClosure: newValue)
|
|
objc_setAssociatedObject(self, &handle, selectorWrapper, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
|
action = selectorWrapper.selector
|
|
target = selectorWrapper
|
|
} else {
|
|
action = nil
|
|
target = nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// only assign if different; useful for performance
|
|
func assignIfDifferent<T: Equatable>(_ a: UnsafeMutablePointer<T>, _ b: T) {
|
|
if a.pointee != b {
|
|
a.pointee = b
|
|
}
|
|
}
|
|
|
|
extension DispatchQoS {
|
|
func toQualityOfService() -> QualityOfService {
|
|
switch self {
|
|
case .userInteractive: return .userInteractive
|
|
case .userInitiated: return .userInitiated
|
|
case .utility: return .utility
|
|
case .background: return .background
|
|
default: return .default
|
|
}
|
|
}
|
|
}
|
|
|
|
extension NSImage {
|
|
// NSImage(named) caches/reuses NSImage objects; we force separate instances of images by using copy()
|
|
static func initCopy(_ name: String) -> NSImage {
|
|
return NSImage(named: name)!.copy() as! NSImage
|
|
}
|
|
|
|
static func initTemplateCopy(_ name: String) -> NSImage {
|
|
let image = initCopy(name)
|
|
image.isTemplate = true
|
|
return image
|
|
}
|
|
|
|
// copy and resize an image using high quality interpolation
|
|
static func initResizedCopy(_ name: String, _ width: CGFloat, _ height: CGFloat) -> NSImage {
|
|
let original = initCopy(name)
|
|
let img = NSImage(size: CGSize(width: width, height: height))
|
|
img.lockFocus()
|
|
NSGraphicsContext.current?.imageInterpolation = .high
|
|
original.draw(in: NSMakeRect(0, 0, width, height), from: NSMakeRect(0, 0, original.size.width, original.size.height), operation: .copy, fraction: 1)
|
|
img.unlockFocus()
|
|
return img
|
|
}
|
|
|
|
static func fromCgImage(_ cgImage: CGImage) -> NSImage {
|
|
return NSImage(cgImage: cgImage, size: NSSize(width: cgImage.width, height: cgImage.height))
|
|
}
|
|
}
|
|
|
|
extension pid_t {
|
|
func isZombie() -> Bool {
|
|
var kinfo = kinfo_proc()
|
|
var size = MemoryLayout<kinfo_proc>.stride
|
|
var mib: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, self]
|
|
sysctl(&mib, u_int(mib.count), &kinfo, &size, nil, 0)
|
|
_ = withUnsafePointer(to: &kinfo.kp_proc.p_comm) {
|
|
String(cString: UnsafeRawPointer($0).assumingMemoryBound(to: CChar.self))
|
|
}
|
|
return kinfo.kp_proc.p_stat == SZOMB
|
|
}
|
|
}
|
|
|
|
extension String {
|
|
// convert a FourCharCode into a String
|
|
init(_ fourCharCode: FourCharCode) { // or `OSType`, or `UInt32`
|
|
self = NSFileTypeForHFSTypeCode(fourCharCode).trimmingCharacters(in: CharacterSet(charactersIn: "'"))
|
|
}
|
|
}
|
|
|
|
extension Int {
|
|
func compare(_ otherNumber: Int) -> ComparisonResult {
|
|
return (self as NSNumber).compare(otherNumber as NSNumber)
|
|
}
|
|
}
|
|
|
|
extension Optional where Wrapped == String {
|
|
func localizedStandardCompare(_ string: String?) -> ComparisonResult {
|
|
return (self ?? "").localizedStandardCompare(string ?? "")
|
|
}
|
|
}
|
|
|
|
extension NSWindow {
|
|
func hideAppIfLastWindowIsClosed() {
|
|
if (!NSApp.windows.contains { $0.isVisible && $0.className != "NSStatusBarWindow" && $0.windowNumber != windowNumber}) {
|
|
App.shared.hide(nil)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension CaseIterable where Self: Equatable {
|
|
var index: Int {
|
|
return Self.allCases.distance(from: Self.allCases.startIndex, to: Self.allCases.firstIndex(of: self)!)
|
|
}
|
|
var indexAsString: String {
|
|
return String(describing: self.index)
|
|
}
|
|
}
|
|
|
|
class ModifierFlags {
|
|
static var current: NSEvent.ModifierFlags {
|
|
return NSEvent.modifierFlags
|
|
}
|
|
}
|
|
|
|
extension NSPoint {
|
|
static func += (lhs: inout NSPoint, rhs: NSPoint) {
|
|
lhs.x += rhs.x
|
|
lhs.y += rhs.y
|
|
}
|
|
|
|
static func + (lhs: NSPoint, rhs: NSPoint) -> NSPoint {
|
|
return NSPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y)
|
|
}
|
|
|
|
static func - (lhs: NSPoint, rhs: NSPoint) -> NSPoint {
|
|
return NSPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y)
|
|
}
|
|
|
|
static func / (lhs: NSPoint, rhs: Int) -> NSPoint {
|
|
return NSPoint(x: lhs.x / Double(rhs), y: lhs.y / Double(rhs))
|
|
}
|
|
}
|