Move local modules into a folder named Modules.

This commit is contained in:
Brent Simmons
2024-07-06 21:07:05 -07:00
parent 14bcef0f9a
commit d50b5818ac
491 changed files with 76 additions and 52 deletions

View File

@@ -0,0 +1,37 @@
//
// FourCharCode.swift
// RSCore
//
// Created by Olof Hellman on 1/7/18.
// Copyright © 2018 Olof Hellman. All rights reserved.
//
import Foundation
public extension String {
/// Converts a string to a `FourCharCode`.
///
/// `FourCharCode` values like `OSType`, `DescType` or `AEKeyword` are really just
/// 4-byte values commonly represented as values like `'odoc'` where each byte is
/// represented as its ASCII character. This property turns a Swift string into
/// its `FourCharCode` equivalent, as Swift doesn't recognize `FourCharCode` types
/// natively just yet. With this extension, one can use `"odoc".fourCharCode`
/// where one would really want to use `'odoc'`.
var fourCharCode: FourCharCode {
precondition(count == 4)
var sum: UInt32 = 0
for scalar in self.unicodeScalars {
sum = (sum * 256) + scalar.value
}
return sum
}
}
public extension Int {
var fourCharCode: FourCharCode {
return UInt32(self)
}
}

View File

@@ -0,0 +1,153 @@
//
// Keyboard.swift
// RSCore
//
// Created by Brent Simmons on 12/19/17.
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
//
#if os(macOS)
import AppKit
private extension String {
var keyboardIntegerValue: Int? {
if isEmpty {
return nil
}
let utf16String = utf16
let startIndex = utf16String.startIndex
if startIndex == utf16String.endIndex {
return nil
}
return Int(utf16String[startIndex])
}
}
@MainActor public struct KeyboardShortcut: Hashable {
public let key: KeyboardKey
public let actionString: String
public init?(dictionary: [String: Any]) {
guard let key = KeyboardKey(dictionary: dictionary) else {
return nil
}
guard let actionString = dictionary["action"] as? String else {
return nil
}
self.key = key
self.actionString = actionString
}
public func perform(with view: NSView) {
let action = NSSelectorFromString(actionString)
NSApplication.shared.sendAction(action, to: nil, from: view)
}
public static func findMatchingShortcut(in shortcuts: Set<KeyboardShortcut>, key: KeyboardKey) -> KeyboardShortcut? {
for shortcut in shortcuts {
if shortcut.key == key {
return shortcut
}
}
return nil
}
// MARK: - Hashable
nonisolated public func hash(into hasher: inout Hasher) {
hasher.combine(key)
}
}
public struct KeyboardKey: Hashable, Sendable {
public let shiftKeyDown: Bool
public let optionKeyDown: Bool
public let commandKeyDown: Bool
public let controlKeyDown: Bool
public let integerValue: Int // unmodified character as Int
init(integerValue: Int, shiftKeyDown: Bool, optionKeyDown: Bool, commandKeyDown: Bool, controlKeyDown: Bool) {
self.integerValue = integerValue
self.shiftKeyDown = shiftKeyDown
self.optionKeyDown = optionKeyDown
self.commandKeyDown = commandKeyDown
self.controlKeyDown = controlKeyDown
}
static let deleteKeyCode = 127
public init(with event: NSEvent) {
let flags = event.modifierFlags
let shiftKeyDown = flags.contains(.shift)
let optionKeyDown = flags.contains(.option)
let commandKeyDown = flags.contains(.command)
let controlKeyDown = flags.contains(.control)
let integerValue = event.charactersIgnoringModifiers?.keyboardIntegerValue ?? 0
self.init(integerValue: integerValue, shiftKeyDown: shiftKeyDown, optionKeyDown: optionKeyDown, commandKeyDown: commandKeyDown, controlKeyDown: controlKeyDown)
}
public init?(dictionary: [String: Any]) {
guard let s = dictionary["key"] as? String else {
return nil
}
var integerValue = 0
switch(s) {
case "[space]":
integerValue = " ".keyboardIntegerValue!
case "[uparrow]":
integerValue = NSUpArrowFunctionKey
case "[downarrow]":
integerValue = NSDownArrowFunctionKey
case "[leftarrow]":
integerValue = NSLeftArrowFunctionKey
case "[rightarrow]":
integerValue = NSRightArrowFunctionKey
case "[return]":
integerValue = NSCarriageReturnCharacter
case "[enter]":
integerValue = NSEnterCharacter
case "[delete]":
integerValue = KeyboardKey.deleteKeyCode
case "[deletefunction]":
integerValue = NSDeleteFunctionKey
case "[tab]":
integerValue = NSTabCharacter
default:
guard let unwrappedIntegerValue = s.keyboardIntegerValue else {
return nil
}
integerValue = unwrappedIntegerValue
}
let shiftKeyDown = dictionary["shiftModifier"] as? Bool ?? false
let optionKeyDown = dictionary["optionModifier"] as? Bool ?? false
let commandKeyDown = dictionary["commandModifier"] as? Bool ?? false
let controlKeyDown = dictionary["controlModifier"] as? Bool ?? false
self.init(integerValue: integerValue, shiftKeyDown: shiftKeyDown, optionKeyDown: optionKeyDown, commandKeyDown: commandKeyDown, controlKeyDown: controlKeyDown)
}
// MARK: - Hashable
public func hash(into hasher: inout Hasher) {
hasher.combine(integerValue)
}
}
#endif

View File

@@ -0,0 +1,18 @@
//
// KeyboardDelegateProtocol.swift
// NetNewsWire
//
// Created by Brent Simmons on 10/11/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
//
#if os(macOS)
import AppKit
//let keypadEnter: unichar = 3
@objc public protocol KeyboardDelegate: AnyObject {
// Return true if handled.
@MainActor func keydown(_: NSEvent, in view: NSView) -> Bool
}
#endif

View File

@@ -0,0 +1,18 @@
//
// NSAppearance+RSCore.swift
// RSCore
//
// Created by Daniel Jalkut on 8/28/18.
// Copyright © 2018 Ranchero Software, LLC. All rights reserved.
//
#if os(macOS)
import AppKit
extension NSAppearance {
public var isDarkMode: Bool {
bestMatch(from: [.darkAqua, .aqua]) == .darkAqua
}
}
#endif

View File

@@ -0,0 +1,30 @@
//
// NSAppleEventDescriptor+RSCore.swift
// RSCore
//
// Created by Nate Weaver on 2020-01-02.
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
//
#if os(macOS)
import AppKit
public extension NSAppleEventDescriptor {
/// An NSAppleEventDescriptor describing a running application.
///
/// - Parameter runningApplication: A running application to associate with the descriptor.
///
/// - Returns: An instance of `NSAppleEventDescriptor` that refers to the running application,
/// or `nil` if the running application has no process ID.
convenience init?(runningApplication: NSRunningApplication) {
let pid = runningApplication.processIdentifier
if pid == -1 {
return nil
}
self.init(processIdentifier: pid)
}
}
#endif

View File

@@ -0,0 +1,29 @@
//
// NSImage+RSCore.swift
// RSCore
//
// Created by Brent Simmons on 12/16/17.
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
//
#if os(macOS)
import AppKit
public extension NSImage {
func tinted(with color: NSColor) -> NSImage {
let image = self.copy() as! NSImage
image.lockFocus()
color.set()
let rect = NSRect(x: 0, y: 0, width: image.size.width, height: image.size.height)
rect.fill(using: .sourceAtop)
image.unlockFocus()
image.isTemplate = false
return image
}
}
#endif

View File

@@ -0,0 +1,31 @@
//
// NSMenu+Extensions.swift
// RSCore
//
// Created by Brent Simmons on 2/9/18.
// Copyright © 2018 Ranchero Software, LLC. All rights reserved.
//
#if os(macOS)
import AppKit
public extension NSMenu {
func takeItems(from menu: NSMenu) {
// The passed-in menu gets all its items removed.
let items = menu.items
menu.removeAllItems()
for menuItem in items {
addItem(menuItem)
}
}
/// Add a separator if there are multiple menu items and the last one is not a separator.
func addSeparatorIfNeeded() {
if items.count > 0 && !items.last!.isSeparatorItem {
addItem(NSMenuItem.separator())
}
}
}
#endif

View File

@@ -0,0 +1,168 @@
//
// NSOutlineView+Extensions.swift
// RSCore
//
// Created by Brent Simmons on 9/6/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
//
#if os(macOS)
import AppKit
public extension NSOutlineView {
var selectedItems: [AnyObject] {
if selectionIsEmpty {
return [AnyObject]()
}
return selectedRowIndexes.compactMap { (oneIndex) -> AnyObject? in
return item(atRow: oneIndex) as AnyObject
}
}
var firstSelectedRow: Int? {
if selectionIsEmpty {
return nil
}
return selectedRowIndexes.first
}
var lastSelectedRow: Int? {
if selectionIsEmpty {
return nil
}
return selectedRowIndexes.last
}
@IBAction func selectPreviousRow(_ sender: Any?) {
guard var row = firstSelectedRow else {
return
}
if row < 1 {
return
}
while true {
row -= 1
if row < 0 {
return
}
if canSelect(row) {
selectRowAndScrollToVisible(row)
return
}
}
}
@IBAction func selectNextRow(_ sender: Any?) {
// If no selectedRow, end up at first selectable row.
var row = lastSelectedRow ?? -1
while true {
row += 1
if let _ = item(atRow: row) {
if canSelect(row) {
selectRowAndScrollToVisible(row)
return
}
}
else {
return // if there are no more items, were out of rows
}
}
}
@IBAction func collapseSelectedRows(_ sender: Any?) {
for item in selectedItems {
if isExpandable(item) && isItemExpanded(item) {
animator().collapseItem(item)
}
}
}
@IBAction func expandSelectedRows(_ sender: Any?) {
for item in selectedItems {
if isExpandable(item) && !isItemExpanded(item) {
animator().expandItem(item)
}
}
}
@IBAction func expandAll(_ sender: Any?) {
expandAllChildren(of: nil)
}
@IBAction func collapseAllExceptForGroupItems(_ sender: Any?) {
collapseAllChildren(of: nil, exceptForGroupItems: true)
}
func expandAllChildren(of item: Any?) {
guard let childItems = children(of: item) else {
return
}
for child in childItems {
if !isItemExpanded(child) && isExpandable(child) {
animator().expandItem(child, expandChildren: true)
}
expandAllChildren(of: child)
}
}
func collapseAllChildren(of item: Any?, exceptForGroupItems: Bool) {
guard let childItems = children(of: item) else {
return
}
for child in childItems {
collapseAllChildren(of: child, exceptForGroupItems: exceptForGroupItems)
if exceptForGroupItems && isGroupItem(child) {
continue
}
if isItemExpanded(child) {
animator().collapseItem(child, collapseChildren: true)
}
}
}
func children(of item: Any?) -> [Any]? {
var children = [Any]()
for indexOfItem in 0..<numberOfChildren(ofItem: item) {
if let child = child(indexOfItem, ofItem: item) {
children.append(child)
}
}
return children.isEmpty ? nil : children
}
func isGroupItem(_ item: Any) -> Bool {
return delegate?.outlineView?(self, isGroupItem: item) ?? false
}
func canSelect(_ row: Int) -> Bool {
guard let item = item(atRow: row) else {
return false
}
return canSelectItem(item)
}
func canSelectItem(_ item: Any) -> Bool {
let isSelectable = delegate?.outlineView?(self, shouldSelectItem: item) ?? true
return isSelectable
}
}
#endif

View File

@@ -0,0 +1,63 @@
//
// NSPasteboard+RSCore.swift
// RSCore
//
// Created by Brent Simmons on 2/11/18.
// Copyright © 2018 Ranchero Software, LLC. All rights reserved.
//
#if os(macOS)
import AppKit
public extension NSPasteboard {
@MainActor func copyObjects(_ objects: [Any]) {
guard let writers = writersFor(objects) else {
return
}
clearContents()
writeObjects(writers)
}
func canCopyAtLeastOneObject(_ objects: [Any]) -> Bool {
for object in objects {
if object is PasteboardWriterOwner {
return true
}
}
return false
}
}
public extension NSPasteboard {
static func urlString(from pasteboard: NSPasteboard) -> String? {
return pasteboard.urlString
}
private var urlString: String? {
guard let type = self.availableType(from: [.string]) else {
return nil
}
guard let str = self.string(forType: type), !str.isEmpty else {
return nil
}
return str.mayBeURL ? str : nil
}
}
private extension NSPasteboard {
@MainActor func writersFor(_ objects: [Any]) -> [NSPasteboardWriting]? {
let writers = objects.compactMap { ($0 as? PasteboardWriterOwner)?.pasteboardWriter }
return writers.isEmpty ? nil : writers
}
}
#endif

View File

@@ -0,0 +1,31 @@
//
// NSResponder-Extensions.swift
// RSCore
//
// Created by Brent Simmons on 10/10/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
//
#if os(macOS)
import AppKit
public extension NSResponder {
func hasAncestor(_ ancestor: NSResponder) -> Bool {
var nomad: NSResponder = self
while(true) {
if nomad === ancestor {
return true
}
if let _ = nomad.nextResponder {
nomad = nomad.nextResponder!
}
else {
break
}
}
return false
}
}
#endif

View File

@@ -0,0 +1,108 @@
//
// NSTableView+Extensions.swift
// RSCore
//
// Created by Brent Simmons on 9/6/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
//
#if os(macOS)
import AppKit
public extension NSTableView {
var selectionIsEmpty: Bool {
return selectedRowIndexes.startIndex == selectedRowIndexes.endIndex
}
func indexesOfAvailableRowsPassingTest(_ test: (Int) -> Bool) -> IndexSet? {
// Checks visible and in-flight rows.
var indexes = IndexSet()
enumerateAvailableRowViews { (_, row) in
if test(row) {
indexes.insert(row)
}
}
return indexes.isEmpty ? nil : indexes
}
func indexesOfAvailableRows() -> IndexSet? {
var indexes = IndexSet()
enumerateAvailableRowViews { indexes.insert($1) }
return indexes.isEmpty ? nil : indexes
}
func scrollTo(row: Int, extraHeight: Int = 150) {
guard let scrollView = self.enclosingScrollView else {
return
}
let documentVisibleRect = scrollView.documentVisibleRect
let r = rect(ofRow: row)
if NSContainsRect(documentVisibleRect, r) {
return
}
let rMidY = NSMidY(r)
var scrollPoint = NSZeroPoint;
scrollPoint.y = floor(rMidY - (documentVisibleRect.size.height / 2.0)) + CGFloat(extraHeight)
scrollPoint.y = max(scrollPoint.y, 0)
let maxScrollPointY = frame.size.height - documentVisibleRect.size.height
scrollPoint.y = min(maxScrollPointY, scrollPoint.y)
let clipView = scrollView.contentView
let rClipView = NSMakeRect(scrollPoint.x, scrollPoint.y, NSWidth(clipView.bounds), NSHeight(clipView.bounds))
clipView.animator().bounds = rClipView
}
func scrollToRowIfNotVisible(_ row: Int) {
if let followingRow = rowView(atRow: row, makeIfNecessary: false) {
if !(visibleRowViews()?.contains(followingRow) ?? false) {
scrollTo(row: row, extraHeight: 0)
}
} else {
scrollTo(row: row, extraHeight: 0)
}
}
func visibleRowViews() -> [NSTableRowView]? {
guard let scrollView = self.enclosingScrollView, numberOfRows > 0 else {
return nil
}
let range = rows(in: scrollView.documentVisibleRect)
let ixMax = numberOfRows - 1
let ixStart = min(range.location, ixMax)
let ixEnd = min(((range.location + range.length) - 1), ixMax)
var visibleRows = [NSTableRowView]()
for ixRow in ixStart...ixEnd {
if let oneRowView = rowView(atRow: ixRow, makeIfNecessary: false) {
visibleRows += [oneRowView]
}
}
return visibleRows.isEmpty ? nil : visibleRows
}
func selectRow(_ row: Int) {
self.selectRowIndexes(IndexSet(integer: row), byExtendingSelection: false)
}
func selectRowAndScrollToVisible(_ row: Int) {
self.selectRow(row)
self.scrollRowToVisible(row)
}
}
#endif

View File

@@ -0,0 +1,17 @@
//
// NSToolbar+RSCore.swift
// RSCore
//
// Created by Brent Simmons on 2/17/18.
// Copyright © 2018 Ranchero Software, LLC. All rights reserved.
//
#if os(macOS)
import AppKit
public extension NSToolbar {
func existingItem(withIdentifier identifier: NSToolbarItem.Identifier) -> NSToolbarItem? {
return items.first(where: {$0.itemIdentifier == identifier})
}
}
#endif

View File

@@ -0,0 +1,53 @@
//
// NSView+Extensions.swift
// RSCore
//
// Created by Maurice Parker on 11/12/19.
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
//
#if os(macOS)
import AppKit
import FoundationExtras
public extension NSView {
func asImage() -> NSImage {
let rep = bitmapImageRepForCachingDisplay(in: bounds)!
cacheDisplay(in: bounds, to: rep)
let img = NSImage(size: bounds.size)
img.addRepresentation(rep)
return img
}
func constraintsToMakeSubViewFullSize(_ subview: NSView) -> [NSLayoutConstraint] {
let leadingConstraint = NSLayoutConstraint(item: subview, attribute: .leading, relatedBy: .equal, toItem: self.safeAreaLayoutGuide, attribute: .leading, multiplier: 1.0, constant: 0.0)
let trailingConstraint = NSLayoutConstraint(item: subview, attribute: .trailing, relatedBy: .equal, toItem: self.safeAreaLayoutGuide, attribute: .trailing, multiplier: 1.0, constant: 0.0)
let topConstraint = NSLayoutConstraint(item: subview, attribute: .top, relatedBy: .equal, toItem: self.safeAreaLayoutGuide, attribute: .top, multiplier: 1.0, constant: 0.0)
let bottomConstraint = NSLayoutConstraint(item: subview, attribute: .bottom, relatedBy: .equal, toItem: self.safeAreaLayoutGuide, attribute: .bottom, multiplier: 1.0, constant: 0.0)
return [leadingConstraint, trailingConstraint, topConstraint, bottomConstraint]
}
/// Keeps a subview at same size as receiver.
///
/// - Parameter subview: The subview to constrain. Must be a descendant of `self`.
func addFullSizeConstraints(forSubview subview: NSView) {
NSLayoutConstraint.activate([
subview.leadingAnchor.constraint(equalTo: leadingAnchor),
subview.trailingAnchor.constraint(equalTo: trailingAnchor),
subview.topAnchor.constraint(equalTo: topAnchor),
subview.bottomAnchor.constraint(equalTo: bottomAnchor)
])
}
/// Sets the view's frame if it's different from the current frame.
///
/// - Parameter rect: The new frame.
func setFrame(ifNotEqualTo rect: NSRect) {
if self.frame != rect {
self.frame = rect
}
}
}
#endif

View File

@@ -0,0 +1,95 @@
//
// NSWindow-Extensions.swift
// RSCore
//
// Created by Brent Simmons on 10/10/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
//
#if os(macOS)
import AppKit
public extension NSWindow {
var isDisplayingSheet: Bool {
return attachedSheet != nil
}
func makeFirstResponderUnlessDescendantIsFirstResponder(_ responder: NSResponder) {
if let fr = firstResponder, fr.hasAncestor(responder) {
return
}
makeFirstResponder(responder)
}
func setPointAndSizeAdjustingForScreen(point: NSPoint, size: NSSize, minimumSize: NSSize) {
// point.y specifices from the *top* of the screen, even though screen coordinates work from the bottom up. This is for convenience.
// The eventual size may be smaller than requested, since the screen may be small, but not smaller than minimumSize.
guard let screenFrame = screen?.visibleFrame else {
return
}
let paddingFromScreenEdge: CGFloat = 8.0
let x = point.x
let y = screenFrame.maxY - point.y
var width = size.width
var height = size.height
if x + width > screenFrame.maxX {
width = max((screenFrame.maxX - x) - paddingFromScreenEdge, minimumSize.width)
}
if y - height < 0.0 {
height = max((screenFrame.maxY - point.y) - paddingFromScreenEdge, minimumSize.height)
}
let frame = NSRect(x: x, y: y, width: width, height: height)
setFrame(frame, display: true)
setFrameTopLeftPoint(frame.origin)
}
var flippedOrigin: NSPoint? {
// Screen coordinates start at lower-left.
// With this we can use upper-left, like sane people.
get {
guard let screenFrame = screen?.frame else {
return nil
}
let flippedPoint = NSPoint(x: frame.origin.x, y: screenFrame.maxY - frame.maxY)
return flippedPoint
}
set {
guard let screenFrame = screen?.frame else {
return
}
var point = newValue!
point.y = screenFrame.maxY - point.y
setFrameTopLeftPoint(point)
}
}
func setFlippedOriginAdjustingForScreen(_ point: NSPoint) {
guard let screenFrame = screen?.frame else {
return
}
let paddingFromEdge: CGFloat = 8.0
var unflippedPoint = point
unflippedPoint.y = (screenFrame.maxY - point.y) - frame.height
if unflippedPoint.y < 0 {
unflippedPoint.y = paddingFromEdge
}
if unflippedPoint.x < 0 {
unflippedPoint.x = paddingFromEdge
}
setFrameOrigin(unflippedPoint)
}
}
#endif

View File

@@ -0,0 +1,23 @@
//
// NSWindowController+RSCore.swift
// RSCore
//
// Created by Brent Simmons on 2/17/18.
// Copyright © 2018 Ranchero Software, LLC. All rights reserved.
//
#if os(macOS)
import AppKit
public extension NSWindowController {
var isDisplayingSheet: Bool {
return window?.isDisplayingSheet ?? false
}
var isOpen: Bool {
return isWindowLoaded && window!.isVisible
}
}
#endif

View File

@@ -0,0 +1,15 @@
//
// PasteboardWriterOwner.swift
// RSCore
//
// Created by Brent Simmons on 2/11/18.
// Copyright © 2018 Ranchero Software, LLC. All rights reserved.
//
#if os(macOS)
import AppKit
public protocol PasteboardWriterOwner {
@MainActor var pasteboardWriter: NSPasteboardWriting { get }
}
#endif

View File

@@ -0,0 +1,65 @@
//
// RSToolbarItem.swift
// RSCore
//
// Created by Brent Simmons on 10/16/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
//
#if os(macOS)
import AppKit
public class RSToolbarItem: NSToolbarItem {
override public func validate() {
guard let view = view, let _ = view.window else {
isEnabled = false
return
}
isEnabled = isValidAsUserInterfaceItem()
}
}
private extension RSToolbarItem {
func isValidAsUserInterfaceItem() -> Bool {
// Use NSValidatedUserInterfaceItem protocol rather than calling validateToolbarItem:.
if let target = target as? NSResponder {
return validateWithResponder(target) ?? false
}
var responder = view?.window?.firstResponder
if responder == nil {
return false
}
while(true) {
if let validated = validateWithResponder(responder!) {
return validated
}
responder = responder?.nextResponder
if responder == nil {
break
}
}
if let appDelegate = NSApplication.shared.delegate {
if let validated = validateWithResponder(appDelegate) {
return validated
}
}
return false
}
func validateWithResponder(_ responder: NSObjectProtocol) -> Bool? {
guard responder.responds(to: action), let target = responder as? NSUserInterfaceValidations else {
return nil
}
return target.validateUserInterfaceItem(self)
}
}
#endif

View File

@@ -0,0 +1,47 @@
//
// URLPasteboardWriter.swift
// RSCore
//
// Created by Brent Simmons on 1/28/18.
// Copyright © 2018 Ranchero Software, LLC. All rights reserved.
//
#if os(macOS)
import AppKit
// Takes a string, not a URL, but writes it as a URL (when possible) and as a String.
@objc public final class URLPasteboardWriter: NSObject, NSPasteboardWriting {
let urlString: String
public init(urlString: String) {
self.urlString = urlString
}
public class func write(urlString: String, to pasteboard: NSPasteboard) {
pasteboard.clearContents()
let writer = URLPasteboardWriter(urlString: urlString)
pasteboard.writeObjects([writer])
}
// MARK: - NSPasteboardWriting
public func writableTypes(for pasteboard: NSPasteboard) -> [NSPasteboard.PasteboardType] {
if let _ = URL(string: urlString) {
return [.URL, .string]
}
return [.string]
}
public func pasteboardPropertyList(forType type: NSPasteboard.PasteboardType) -> Any? {
guard type == .string || type == .URL else {
return nil
}
return urlString
}
}
#endif

View File

@@ -0,0 +1,148 @@
//
// UserApp.swift
// RSCore
//
// Created by Brent Simmons on 1/14/18.
// Copyright © 2018 Ranchero Software, LLC. All rights reserved.
//
#if os(macOS)
import AppKit
/// Represents an app (the type of app mostly found in /Applications.)
///
/// The app may or may not be running. It may or may not exist.
public final class UserApp {
public let bundleID: String
public var existsOnDisk = false
public var isRunning: Bool {
updateStatus()
if let runningApplication = runningApplication {
return !runningApplication.isTerminated
}
return false
}
private var icon: NSImage? = nil
private var path: String? = nil
private var runningApplication: NSRunningApplication? = nil
public init(bundleID: String) {
self.bundleID = bundleID
updateStatus()
}
public func updateStatus() {
if let runningApplication = runningApplication, runningApplication.isTerminated {
self.runningApplication = nil
}
let runningApplications = NSRunningApplication.runningApplications(withBundleIdentifier: bundleID)
for app in runningApplications {
if let runningApplication = runningApplication {
if app == runningApplication {
break
}
}
else {
if !app.isTerminated {
runningApplication = app
break
}
}
}
if let runningApplication = runningApplication {
existsOnDisk = true
icon = runningApplication.icon
if let bundleURL = runningApplication.bundleURL {
path = bundleURL.path
}
else {
path = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleID)?.path
}
if icon == nil, let path {
icon = NSWorkspace.shared.icon(forFile: path)
}
return
}
path = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleID)?.path
if icon == nil, let path {
icon = NSWorkspace.shared.icon(forFile: path)
existsOnDisk = true
}
else {
existsOnDisk = false
icon = nil
}
}
public func launchIfNeeded() async -> Bool {
// Return true if already running.
// Return true if not running and successfully gets launched.
updateStatus()
if isRunning {
return true
}
guard existsOnDisk, let path = path else {
return false
}
let url = URL(fileURLWithPath: path)
do {
let configuration = NSWorkspace.OpenConfiguration()
configuration.promptsUserIfNeeded = true
let app = try await NSWorkspace.shared.openApplication(at: url, configuration: configuration)
runningApplication = app
if app.isFinishedLaunching {
return true
}
try? await Task.sleep(for: .seconds(1)) // Give the app time to launch. This is ugly.
if app.isFinishedLaunching {
return true
}
try? await Task.sleep(for: .seconds(1)) // Give it some *more* time.
return true
} catch {
return false
}
}
public func bringToFront() -> Bool {
// Activates the app, ignoring other apps.
// Does not automatically launch the app first.
updateStatus()
return runningApplication?.activate() ?? false
}
public func targetDescriptor() -> NSAppleEventDescriptor? {
// Requires that the app has previously been launched.
updateStatus()
guard let runningApplication = runningApplication, !runningApplication.isTerminated else {
return nil
}
return NSAppleEventDescriptor(runningApplication: runningApplication)
}
}
#endif