Files
NetNewsWire/Mac/MainWindow/Sidebar/PasteboardWebFeed.swift
Tyler aabb149307 Address 3580 by using only the items in the DraggingInfo to handle the drag and no view properties associated with a single window.
Remove cached `draggedNodes` optimization and just always use the data on the dragging pasteboard because it works both within the same window and across two windows.

Add support for WebFeedPasteboardWriter to include a parent containerName in the pasteboard dictionary written for a WebFeed object (which doesn't know about it's parent) because we need this for drag and drop to be able to perform a move operation.

Fix issue with dragged feeds not knowing their parent folder and delete deleting the wrong item by telling WebFeedPasteboardWriter what the container of items is.  Remove code that is now unused now that draggedNodes is removed and consolidate code that was expanded to handle both node and pasteboard items since we are just using pasteboard items now.
2025-04-27 22:01:58 -07:00

225 lines
6.7 KiB
Swift

//
// PasteboardFeed.swift
// NetNewsWire
//
// Created by Brent Simmons on 9/20/18.
// Copyright © 2018 Ranchero Software. All rights reserved.
//
import AppKit
import Articles
import Account
import RSCore
typealias PasteboardWebFeedDictionary = [String: String]
struct PasteboardWebFeed: Hashable {
fileprivate struct Key {
static let url = "URL"
static let homePageURL = "homePageURL"
static let name = "name"
// Internal
static let accountID = "accountID"
static let accountType = "accountType"
static let webFeedID = "webFeedID"
static let editedName = "editedName"
static let containerName = "containerName"
}
let url: String
let webFeedID: String?
let homePageURL: String?
let name: String?
let editedName: String?
let accountID: String?
let accountType: AccountType?
let containerName: String?
let isLocalFeed: Bool
init(url: String, webFeedID: String?, homePageURL: String?, name: String?, editedName: String?, accountID: String?, accountType: AccountType?, containerName: String? = nil) {
self.url = url.normalizedURL
self.webFeedID = webFeedID
self.homePageURL = homePageURL?.normalizedURL
self.name = name
self.editedName = editedName
self.accountID = accountID
self.accountType = accountType
self.containerName = containerName
self.isLocalFeed = accountID != nil
}
// MARK: - Reading
init?(dictionary: PasteboardWebFeedDictionary) {
guard let url = dictionary[Key.url] else {
return nil
}
let homePageURL = dictionary[Key.homePageURL]
let name = dictionary[Key.name]
let accountID = dictionary[Key.accountID]
let webFeedID = dictionary[Key.webFeedID]
let editedName = dictionary[Key.editedName]
var accountType: AccountType? = nil
if let accountTypeString = dictionary[Key.accountType], let accountTypeInt = Int(accountTypeString) {
accountType = AccountType(rawValue: accountTypeInt)
}
let containerName = dictionary[Key.containerName]
self.init(url: url, webFeedID: webFeedID, homePageURL: homePageURL, name: name, editedName: editedName, accountID: accountID, accountType: accountType, containerName: containerName)
}
init?(pasteboardItem: NSPasteboardItem) {
var pasteboardType: NSPasteboard.PasteboardType?
if pasteboardItem.types.contains(WebFeedPasteboardWriter.webFeedUTIInternalType) {
pasteboardType = WebFeedPasteboardWriter.webFeedUTIInternalType
}
else if pasteboardItem.types.contains(WebFeedPasteboardWriter.webFeedUTIType) {
pasteboardType = WebFeedPasteboardWriter.webFeedUTIType
}
if let foundType = pasteboardType {
if let feedDictionary = pasteboardItem.propertyList(forType: foundType) as? PasteboardWebFeedDictionary {
self.init(dictionary: feedDictionary)
return
}
return nil
}
// Check for URL or a string that may be a URL.
if pasteboardItem.types.contains(.URL) {
pasteboardType = .URL
}
else if pasteboardItem.types.contains(.string) {
pasteboardType = .string
}
if let foundType = pasteboardType {
if let possibleURLString = pasteboardItem.string(forType: foundType) {
if possibleURLString.mayBeURL {
self.init(url: possibleURLString, webFeedID: nil, homePageURL: nil, name: nil, editedName: nil, accountID: nil, accountType: nil)
return
}
}
}
return nil
}
static func pasteboardFeeds(with pasteboard: NSPasteboard) -> Set<PasteboardWebFeed>? {
guard let items = pasteboard.pasteboardItems else {
return nil
}
let webFeeds = items.compactMap { PasteboardWebFeed(pasteboardItem: $0) }
return webFeeds.isEmpty ? nil : Set(webFeeds)
}
// MARK: - Writing
func exportDictionary() -> PasteboardWebFeedDictionary {
var d = PasteboardWebFeedDictionary()
d[Key.url] = url
d[Key.homePageURL] = homePageURL ?? ""
if let nameForDisplay = editedName ?? name {
d[Key.name] = nameForDisplay
}
return d
}
func internalDictionary() -> PasteboardWebFeedDictionary {
var d = PasteboardWebFeedDictionary()
d[PasteboardWebFeed.Key.webFeedID] = webFeedID
d[PasteboardWebFeed.Key.url] = url
if let homePageURL = homePageURL {
d[PasteboardWebFeed.Key.homePageURL] = homePageURL
}
if let name = name {
d[PasteboardWebFeed.Key.name] = name
}
if let editedName = editedName {
d[PasteboardWebFeed.Key.editedName] = editedName
}
if let accountID = accountID {
d[PasteboardWebFeed.Key.accountID] = accountID
}
if let accountType = accountType {
d[PasteboardWebFeed.Key.accountType] = String(accountType.rawValue)
}
if let containerName = containerName {
d[PasteboardWebFeed.Key.containerName] = containerName
}
return d
}
}
extension WebFeed: @retroactive PasteboardWriterOwner {
public var pasteboardWriter: NSPasteboardWriting {
return WebFeedPasteboardWriter(webFeed: self)
}
}
@objc final class WebFeedPasteboardWriter: NSObject, NSPasteboardWriting {
private let webFeed: WebFeed
static let webFeedUTI = "com.ranchero.webFeed"
static let webFeedUTIType = NSPasteboard.PasteboardType(rawValue: webFeedUTI)
static let webFeedUTIInternal = "com.ranchero.NetNewsWire-Evergreen.internal.webFeed"
static let webFeedUTIInternalType = NSPasteboard.PasteboardType(rawValue: webFeedUTIInternal)
var containerID: ContainerIdentifier? = nil
init(webFeed: WebFeed) {
self.webFeed = webFeed
}
// MARK: - NSPasteboardWriting
func writableTypes(for pasteboard: NSPasteboard) -> [NSPasteboard.PasteboardType] {
return [WebFeedPasteboardWriter.webFeedUTIType, .URL, .string, WebFeedPasteboardWriter.webFeedUTIInternalType]
}
func pasteboardPropertyList(forType type: NSPasteboard.PasteboardType) -> Any? {
let plist: Any?
switch type {
case .string:
plist = webFeed.nameForDisplay
case .URL:
plist = webFeed.url
case WebFeedPasteboardWriter.webFeedUTIType:
plist = exportDictionary
case WebFeedPasteboardWriter.webFeedUTIInternalType:
plist = internalDictionary
default:
plist = nil
}
return plist
}
}
private extension WebFeedPasteboardWriter {
var pasteboardFeed: PasteboardWebFeed {
return PasteboardWebFeed(url: webFeed.url, webFeedID: webFeed.webFeedID, homePageURL: webFeed.homePageURL, name: webFeed.name, editedName: webFeed.editedName, accountID: webFeed.account?.accountID, accountType: webFeed.account?.type)
}
var exportDictionary: PasteboardWebFeedDictionary {
return pasteboardFeed.exportDictionary()
}
var internalDictionary: PasteboardWebFeedDictionary {
var dictionary = pasteboardFeed.internalDictionary()
if dictionary[PasteboardWebFeed.Key.containerName] == nil,
case let .folder(accountID, folderName) = containerID {
assert(accountID == dictionary[PasteboardWebFeed.Key.accountID], "unexpected: container account doesn't match account of contained item")
dictionary[PasteboardWebFeed.Key.containerName] = folderName
}
return dictionary
}
}