Merge branch 'feature/fix-mac-deprecation-warnings'

This commit is contained in:
Brent Simmons
2025-04-25 09:38:12 -07:00
32 changed files with 163 additions and 87 deletions

View File

@@ -7,7 +7,7 @@
//
import AppKit
import WebKit
@preconcurrency import WebKit
import RSCore
import RSWeb
import Articles

View File

@@ -69,7 +69,7 @@ private extension NNW3ImportController {
panel.canChooseDirectories = false
panel.resolvesAliases = true
panel.directoryURL = NNW3ImportController.defaultFileURL
panel.allowedFileTypes = ["plist"]
panel.allowedContentTypes = [.propertyList]
panel.allowsOtherFileTypes = false
panel.accessoryView = accessoryViewController.view
panel.isAccessoryViewDisclosed = true

View File

@@ -8,6 +8,7 @@
import AppKit
import Account
import UniformTypeIdentifiers
class ExportOPMLWindowController: NSWindowController {
@@ -75,7 +76,7 @@ class ExportOPMLWindowController: NSWindowController {
func exportOPML(account: Account) {
let panel = NSSavePanel()
panel.allowedFileTypes = ["opml"]
panel.allowedContentTypes = [UTType.opml]
panel.allowsOtherFileTypes = false
panel.prompt = NSLocalizedString("Export OPML", comment: "Export OPML")
panel.title = NSLocalizedString("Export OPML", comment: "Export OPML")

View File

@@ -8,6 +8,7 @@
import AppKit
import Account
import UniformTypeIdentifiers
class ImportOPMLWindowController: NSWindowController {
@@ -85,7 +86,7 @@ class ImportOPMLWindowController: NSWindowController {
panel.allowsMultipleSelection = false
panel.canChooseDirectories = false
panel.resolvesAliases = true
panel.allowedFileTypes = ["opml", "xml"]
panel.allowedContentTypes = [UTType.opml, UTType.xml]
panel.allowsOtherFileTypes = false
panel.beginSheetModal(for: hostWindow!) { modalResult in

View File

@@ -84,7 +84,7 @@ struct PasteboardFolder: Hashable {
}
}
extension Folder: PasteboardWriterOwner {
extension Folder: @retroactive PasteboardWriterOwner {
public var pasteboardWriter: NSPasteboardWriting {
return FolderPasteboardWriter(folder: self)

View File

@@ -146,7 +146,7 @@ struct PasteboardWebFeed: Hashable {
}
}
extension WebFeed: PasteboardWriterOwner {
extension WebFeed: @retroactive PasteboardWriterOwner {
public var pasteboardWriter: NSPasteboardWriting {
return WebFeedPasteboardWriter(webFeed: self)

View File

@@ -10,7 +10,7 @@ import AppKit
import Articles
import RSCore
extension Article: PasteboardWriterOwner {
extension Article: @retroactive PasteboardWriterOwner {
public var pasteboardWriter: NSPasteboardWriting {
return ArticlePasteboardWriter(article: self)
}

View File

@@ -67,7 +67,7 @@ struct TimelineCellAppearance: Equatable {
}
}
extension NSEdgeInsets: Equatable {
extension NSEdgeInsets: @retroactive Equatable {
public static func ==(lhs: NSEdgeInsets, rhs: NSEdgeInsets) -> Bool {
return lhs.left == rhs.left && lhs.top == rhs.top && lhs.right == rhs.right && lhs.bottom == rhs.bottom

View File

@@ -0,0 +1,23 @@
//
// NSObject+NSSharingService_RSCore.h
// RSCore
//
// Created by Brent Simmons on 11/3/24.
//
@import AppKit;
@interface NSSharingService (NoDeprecationWarning)
// The only way to create custom UI — a Share menu, for instance —
// is to use the unfortunately deprecated
// +[NSSharingService sharingServicesForItems:].
// This cover method allows us to not generate a warning.
//
// We know its deprecated, and we dont want to be bugged
// about it every time we build. (If anyone from Apple
// is reading this — a replacement would be very welcome!)
+ (NSArray *)sharingServicesForItems_noDeprecationWarning:(NSArray *)items;
@end

View File

@@ -0,0 +1,22 @@
//
// NSSharingService+Extension.m
// RSCore
//
// Created by Brent Simmons on 11/3/24.
//
#import "NSSharingService+Extension.h"
@implementation NSSharingService (NoDeprecationWarning)
+ (NSArray *)sharingServicesForItems_noDeprecationWarning:(NSArray *)items {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
return [NSSharingService sharingServicesForItems:items];
#pragma clang diagnostic pop
}
@end

View File

@@ -200,7 +200,7 @@ private extension TimelineViewController {
let sortedArticles = articles.sortedByDate(.orderedAscending)
let items = sortedArticles.map { ArticlePasteboardWriter(article: $0) }
let standardServices = NSSharingService.sharingServices(forItems: items)
let standardServices = NSSharingService.sharingServices(forItems_noDeprecationWarning: items) as? [NSSharingService] ?? [NSSharingService]()
let customServices = SharingServicePickerDelegate.customSharingServices(for: items)
let services = standardServices + customServices
if services.isEmpty {

View File

@@ -7,3 +7,4 @@
//
#import "WKPreferencesPrivate.h"
#import "NSSharingService+Extension.h"

View File

@@ -222,7 +222,7 @@ struct AddAccountsView: View {
.padding(.top, 8)
HStack {
ForEach(0..<chunkedWebAccounts().count, content: { chunk in
ForEach(0..<chunkedWebAccounts().count, id: \.self, content: { chunk in
VStack {
Picker(selection: $selectedAccount, label: Text(""), content: {
ForEach(chunkedWebAccounts()[chunk], id: \.self, content: { account in

View File

@@ -10,6 +10,7 @@ import AppKit
import RSCore
import RSWeb
import UserNotifications
import UniformTypeIdentifiers
final class GeneralPreferencesViewController: NSViewController {
@@ -125,7 +126,7 @@ private extension GeneralPreferencesViewController {
let item = NSMenuItem(title: name, action: nil, keyEquivalent: "")
item.representedObject = browser.bundleIdentifier
let icon = browser.icon ?? NSWorkspace.shared.icon(forFileType: kUTTypeApplicationBundle as String)
let icon = browser.icon ?? NSWorkspace.shared.icon(for: UTType.applicationBundle)
icon.size = NSSize(width: 16.0, height: 16.0)
item.image = browser.icon
menu.addItem(item)

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1610"
LastUpgradeVersion = "1620"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1610"
LastUpgradeVersion = "1620"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@@ -10,7 +10,7 @@ import AppKit
//let keypadEnter: unichar = 3
@objc public protocol KeyboardDelegate: class {
@objc public protocol KeyboardDelegate: AnyObject {
// Return true if handled.
func keydown(_: NSEvent, in view: NSView) -> Bool

View File

@@ -128,8 +128,11 @@ public class RSAppMovementMonitor: NSObject {
func relaunchFromURL(_ appURL: URL) {
// Relaunching is best achieved by requesting that the system launch the app
// at the given URL with the "new instance" option to prevent it simply reactivating us.
let _ = try? NSWorkspace.shared.launchApplication(at: appURL, options: .newInstance, configuration: [:])
NSApp.terminate(self)
let configuration = NSWorkspace.OpenConfiguration()
configuration.createsNewApplicationInstance = true
NSWorkspace.shared.openApplication(at: appURL, configuration: configuration) { _, _ in
NSApp.terminate(self)
}
}
func defaultHandler() {

View File

@@ -63,7 +63,7 @@ public final class UserApp {
path = bundleURL.path
}
else {
path = NSWorkspace.shared.absolutePathForApplication(withBundleIdentifier: bundleID)
path = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleID)?.path
}
if icon == nil, let path = path {
icon = NSWorkspace.shared.icon(forFile: path)
@@ -71,7 +71,7 @@ public final class UserApp {
return
}
path = NSWorkspace.shared.absolutePathForApplication(withBundleIdentifier: bundleID)
path = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleID)?.path
if let path = path {
if icon == nil {
icon = NSWorkspace.shared.icon(forFile: path)
@@ -84,7 +84,7 @@ public final class UserApp {
}
}
public func launchIfNeeded() -> Bool {
public func launchIfNeeded() async -> Bool {
// Return true if already running.
// Return true if not running and successfully gets launched.
@@ -99,20 +99,29 @@ public final class UserApp {
}
let url = URL(fileURLWithPath: path)
if let app = try? NSWorkspace.shared.launchApplication(at: url, options: [.withErrorPresentation], configuration: [:]) {
runningApplication = app
if app.isFinishedLaunching {
return true
}
Thread.sleep(forTimeInterval: 1.0) // Give the app time to launch. This is ugly.
if app.isFinishedLaunching {
return true
}
Thread.sleep(forTimeInterval: 1.0) // Give it some *more* time.
return true
}
return false
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 {

View File

@@ -26,13 +26,13 @@ public enum CloudKitZoneError: LocalizedError {
}
}
public protocol CloudKitZoneDelegate: class {
public protocol CloudKitZoneDelegate: AnyObject {
func cloudKitDidModify(changed: [CKRecord], deleted: [CloudKitRecordKey], completion: @escaping (Result<Void, Error>) -> Void);
}
public typealias CloudKitRecordKey = (recordType: CKRecord.RecordType, recordID: CKRecord.ID)
public protocol CloudKitZone: class {
public protocol CloudKitZone: AnyObject {
static var qualityOfService: QualityOfService { get }

View File

@@ -15,7 +15,7 @@ import Foundation
/// When its canceled, it should do its best to stop
/// doing whatever its doing. However, it should not
/// leave data in an inconsistent state.
public protocol MainThreadOperation: class {
public protocol MainThreadOperation: AnyObject {
// These three properties are set by MainThreadOperationQueue. Dont set them.
var isCanceled: Bool { get set } // Check this at appropriate times in case the operation has been canceled.

View File

@@ -8,7 +8,7 @@
import Foundation
public protocol MainThreadOperationDelegate: class {
public protocol MainThreadOperationDelegate: AnyObject {
func operationDidComplete(_ operation: MainThreadOperation)
func cancelOperation(_ operation: MainThreadOperation)
func make(_ childOperation: MainThreadOperation, dependOn parentOperation: MainThreadOperation)

View File

@@ -8,7 +8,7 @@
import Foundation
public protocol UndoableCommand: class {
public protocol UndoableCommand: AnyObject {
var undoActionName: String { get }
var redoActionName: String { get }
@@ -39,7 +39,7 @@ extension UndoableCommand {
// Useful for view controllers.
public protocol UndoableCommandRunner: class {
public protocol UndoableCommandRunner: AnyObject {
var undoableCommands: [UndoableCommand] { get set }
var undoManager: UndoManager? { get }

View File

@@ -8,8 +8,8 @@
import Foundation
public protocol TreeControllerDelegate: class {
public protocol TreeControllerDelegate: AnyObject {
func treeController(treeController: TreeController, childNodesFor: Node) -> [Node]?
}

View File

@@ -20,8 +20,8 @@ let package = Package(
dependencies: [
"RSParser",
"RSCore"
]
//swiftSettings: [.unsafeFlags(["-warnings-as-errors"])]
],
swiftSettings: [.unsafeFlags(["-warnings-as-errors"])]
),
.testTarget(
name: "RSWebTests",

View File

@@ -8,6 +8,7 @@
#if os(macOS)
import AppKit
import UniformTypeIdentifiers
public class MacWebBrowser {
@@ -18,34 +19,28 @@ public class MacWebBrowser {
return false
}
if (inBackground) {
do {
try NSWorkspace.shared.open(preparedURL, options: [.withoutActivation], configuration: [:])
return true
}
catch {
return false
}
if inBackground {
let configuration = NSWorkspace.OpenConfiguration()
configuration.activates = false
NSWorkspace.shared.open(url, configuration: configuration, completionHandler: nil)
return true
}
return NSWorkspace.shared.open(preparedURL)
}
/// Returns an array of the browsers installed on the system, sorted by name.
///
/// "Browsers" are applications that can both handle `https` URLs, and display HTML documents.
public class func sortedBrowsers() -> [MacWebBrowser] {
guard let httpsIDs = LSCopyAllHandlersForURLScheme("https" as CFString)?.takeRetainedValue() as? [String] else {
return []
}
public static func sortedBrowsers() -> [MacWebBrowser] {
guard let htmlIDs = LSCopyAllRoleHandlersForContentType(kUTTypeHTML, .viewer)?.takeRetainedValue() as? [String] else {
return []
}
let httpsAppURLs = NSWorkspace.shared.urlsForApplications(toOpen: URL(string: "https://apple.com/")!)
let htmlAppURLs = NSWorkspace.shared.urlsForApplications(toOpen: UTType.html)
let browserAppURLs = Set(httpsAppURLs).intersection(Set(htmlAppURLs))
let browserIDs = Set(httpsIDs).intersection(Set(htmlIDs))
return browserIDs.compactMap { MacWebBrowser(bundleIdentifier: $0) }.sorted {
return browserAppURLs.compactMap { MacWebBrowser(url: $0) }.sorted {
if let leftName = $0.name, let rightName = $1.name {
return leftName < rightName
}
@@ -127,15 +122,25 @@ public class MacWebBrowser {
/// - url: The URL to open.
/// - inBackground: If `true`, attempt to load the URL without bringing the browser to the foreground.
@discardableResult public func openURL(_ url: URL, inBackground: Bool = false) -> Bool {
// TODO: make this function async.
guard let preparedURL = url.preparedForOpeningInBrowser() else {
return false
}
let options: NSWorkspace.LaunchOptions = inBackground ? [.withoutActivation] : []
Task { @MainActor in
return NSWorkspace.shared.open([preparedURL], withAppBundleIdentifier: self.bundleIdentifier, options: options, additionalEventParamDescriptor: nil, launchIdentifiers: nil)
let configuration = NSWorkspace.OpenConfiguration()
if inBackground {
configuration.activates = false
}
NSWorkspace.shared.open([preparedURL], withApplicationAt: self.url, configuration: configuration, completionHandler: nil)
}
return true
}
}
extension MacWebBrowser: CustomDebugStringConvertible {

View File

@@ -915,7 +915,7 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1240;
LastUpgradeCheck = 1610;
LastUpgradeCheck = 1620;
ORGANIZATIONNAME = "Ranchero Software";
TargetAttributes = {
176813F22564BB2C00D98635 = {
@@ -959,6 +959,7 @@
849C645F1ED37A5D003D8FC0 = {
CreatedOnToolsVersion = 8.2.1;
DevelopmentTeam = SHJK2V3AJG;
LastSwiftMigration = 1620;
ProvisioningStyle = Automatic;
SystemCapabilities = {
com.apple.HardenedRuntime = {

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1610"
LastUpgradeVersion = "1620"
wasCreatedForAppExtension = "YES"
version = "2.0">
<BuildAction

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1610"
LastUpgradeVersion = "1620"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1610"
LastUpgradeVersion = "1620"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"

View File

@@ -29,11 +29,17 @@ final class SendToMarsEditCommand: SendToCommand {
guard let article = (object as? ArticlePasteboardWriter)?.article else {
return
}
guard let app = appToUse(), app.launchIfNeeded(), app.bringToFront() else {
guard let app = appToUse() else {
return
}
send(article, to: app)
Task {
guard await app.launchIfNeeded(), app.bringToFront() else {
return
}
send(article, to: app)
}
}
}

View File

@@ -37,24 +37,27 @@ final class SendToMicroBlogCommand: SendToCommand {
guard let article = (object as? ArticlePasteboardWriter)?.article else {
return
}
guard microBlogApp.launchIfNeeded(), microBlogApp.bringToFront() else {
return
Task {
guard await microBlogApp.launchIfNeeded(), microBlogApp.bringToFront() else {
return
}
// TODO: get text from contentHTML or contentText if no title and no selectedText.
// TODO: consider selectedText.
let s = article.attributionString + article.linkString
let urlQueryDictionary = ["text": s]
guard let urlQueryString = urlQueryDictionary.urlQueryString else {
return
}
guard let url = URL(string: "microblog://post?" + urlQueryString) else {
return
}
NSWorkspace.shared.open(url)
}
// TODO: get text from contentHTML or contentText if no title and no selectedText.
// TODO: consider selectedText.
let s = article.attributionString + article.linkString
let urlQueryDictionary = ["text": s]
guard let urlQueryString = urlQueryDictionary.urlQueryString else {
return
}
guard let url = URL(string: "microblog://post?" + urlQueryString) else {
return
}
NSWorkspace.shared.open(url)
}
}