mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Move local modules into a folder named Modules.
This commit is contained in:
37
Modules/AppKitExtras/Sources/AppKitExtras/FourCharCode.swift
Normal file
37
Modules/AppKitExtras/Sources/AppKitExtras/FourCharCode.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
|
||||
153
Modules/AppKitExtras/Sources/AppKitExtras/Keyboard.swift
Normal file
153
Modules/AppKitExtras/Sources/AppKitExtras/Keyboard.swift
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
168
Modules/AppKitExtras/Sources/AppKitExtras/NSOutlineView+RSCore.swift
Executable file
168
Modules/AppKitExtras/Sources/AppKitExtras/NSOutlineView+RSCore.swift
Executable 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, we’re 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
|
||||
@@ -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
|
||||
31
Modules/AppKitExtras/Sources/AppKitExtras/NSResponder-Extensions.swift
Executable file
31
Modules/AppKitExtras/Sources/AppKitExtras/NSResponder-Extensions.swift
Executable 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
|
||||
108
Modules/AppKitExtras/Sources/AppKitExtras/NSTableView+RSCore.swift
Executable file
108
Modules/AppKitExtras/Sources/AppKitExtras/NSTableView+RSCore.swift
Executable 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
95
Modules/AppKitExtras/Sources/AppKitExtras/NSWindow-Extensions.swift
Executable file
95
Modules/AppKitExtras/Sources/AppKitExtras/NSWindow-Extensions.swift
Executable 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
65
Modules/AppKitExtras/Sources/AppKitExtras/RSToolbarItem.swift
Executable file
65
Modules/AppKitExtras/Sources/AppKitExtras/RSToolbarItem.swift
Executable 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
|
||||
@@ -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
|
||||
148
Modules/AppKitExtras/Sources/AppKitExtras/UserApp.swift
Normal file
148
Modules/AppKitExtras/Sources/AppKitExtras/UserApp.swift
Normal 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
|
||||
|
||||
Reference in New Issue
Block a user