(Swift) 拖放 NSOutlineview 时出现奇怪的反馈
(Swift) Strange feedback when drag&drop NSOutlineview
首先,对不起我的英语,我会尽力说清楚。(根据@Chip Jarred 的建议编辑,我做了一些更改以简化我的问题)
我成功实现了NSOutlineview的拖拽方式,使用.gap风格(我就是想用这个风格!):
outlineView.draggingDestinationFeedbackStyle = .gap
然后出现了问题,用gif描述会更简单:
https://i.stack.imgur.com/0avhJ.gif
您可以看到拖放可以 运行 部分正确。但问题是:当我将一个节点拖到列表的底部时,在 Node4 下面一点,它会被拖到列表的顶部。
我试图修复它,所以我在 validatDrop{} 中插入了一个“打印”函数:
func outlineView(_ outlineView: NSOutlineView,
validateDrop info: NSDraggingInfo,
proposedItem item: Any?,
proposedChildIndex index: Int) -> NSDragOperation {
print("item:\(item),index:\(index)")
if index < 0 && item == nil{
return []
}else{
outlineView.draggingDestinationFeedbackStyle = .gap
return .move
}
}
终端告诉我,当我将一个节点放到列表顶部或列表底部时,它返回相同的索引:
item:nil,index:0
https://i.stack.imgur.com/7pI7s.gif
如果我删除 .gap 样式:
func outlineView(_ outlineView: NSOutlineView,
validateDrop info: NSDraggingInfo,
proposedItem item: Any?,
proposedChildIndex index: Int) -> NSDragOperation {
print("item:\(item),index:\(index)")
if index < 0 && item == nil{
return []
}else{
// outlineView.draggingDestinationFeedbackStyle = .gap //ignore this line
return .move
}
}
https://i.stack.imgur.com/a8MMe.gif
一切都变得正常了。所以可以推断这可能不是我的“移动”方法的问题。
再次抱歉英语,如果有任何帮助,我将不胜感激。
这是我的代码的重要部分:
outlineView.registerForDraggedTypes([.string])
...
func outlineView(_ outlineView: NSOutlineView,
heightOfRowByItem item: Any) -> CGFloat {
return 15
}
...
extension SceneCatalogView{
func outlineView(_ outlineView: NSOutlineView,
pasteboardWriterForItem item: Any) -> NSPasteboardWriting? {
let sourceNode = outlineView.row(forItem: item)
return "\(sourceNode)" as NSString
}
func outlineView(_ outlineView: NSOutlineView,
validateDrop info: NSDraggingInfo,
proposedItem item: Any?,
proposedChildIndex index: Int) -> NSDragOperation {
if index < 0 && item == nil{
return []
}else{
outlineView.draggingDestinationFeedbackStyle = .gap
return .move
}
}
func outlineView(_ outlineView: NSOutlineView,
acceptDrop info: NSDraggingInfo,
item: Any?,
childIndex index: Int) -> Bool {
let pasteboard = info.draggingPasteboard
let sourceNode = Int(pasteboard.string(forType: .string)!)!
let source = outlineView.item(atRow: sourceNode) as? Catalog
let content = source?.content
let targetNode = outlineView.row(forItem: item)
moveNode(sourceNode, targetNode, index) // Not finished
outlineView.reloadData() // Not finished
return true
}
}
经过一番研究后,我发现多份关于使用 .gap
拖动方式时 NSTableView
错误的报告。 NSOutlineView
似乎继承了该错误。无论如何,我找到了解决方法。
问题是,当您拖动到最后一个顶级项目下方时,传递给 outlineView(_:acceptDrop:item:childIndex)
的 item
和 childIndex
始终是 nil
和 0
,这与拖动到列表顶部时得到的值完全相同。我能找到区分这两种情况的唯一方法是使用 NSDraggingInfo
中的 draggingLocation
与第一项的单元格框架进行比较,并使用它来翻译 index
.
func translateIndexForGapBug(
_ outlineView: NSOutlineView,
item: Any?,
index: Int,
for info: NSDraggingInfo) -> Int
{
guard outlineView.draggingDestinationFeedbackStyle == .gap,
items.count > 0,
item == nil,
index == 0
else { return index }
let point = outlineView.convert(info.draggingLocation, from: nil)
let firstCellFrame = outlineView.frameOfCell(atColumn: 0, row: 0)
return outlineView.isFlipped
? (point.y < firstCellFrame.maxY ? index : items.count)
: (point.y >= firstCellFrame.minY ? index : items.count)
}
我在outlineView(_:acceptDrop:item:childIndex)
中调用它:
func outlineView(
_ outlineView: NSOutlineView,
acceptDrop info: NSDraggingInfo,
item: Any?,
childIndex index: Int) -> Bool
{
assert(item == nil || item is Item)
trace("item = \(String(describing: item)), index = \(index)")
guard let sourceTitle = info.draggingPasteboard.string(forType: .string),
let source = parentAndChildIndex(forItemTitled: sourceTitle)
else { return false }
let debuggedIndex = translateIndexForGapBug(
outlineView,
item: item,
index: index,
for: info
)
moveItem(from: source, to: (item as? Item, debuggedIndex))
outlineView.reloadData()
return true
}
由于其他拖动方式似乎也有效,我只在设置为 .gap
时才这样做,所以为了测试,我的 outlineView(_:validateDrop:proposedItem:proposedChildIndex:)
看起来像这样:
func outlineView(
_ outlineView: NSOutlineView,
validateDrop info: NSDraggingInfo,
proposedItem item: Any?,
proposedChildIndex index: Int) -> NSDragOperation
{
trace("item = \(String(describing: item)), index = \(index)")
guard info.draggingSource as? NSOutlineView === outlineView else {
return []
}
outlineView.draggingDestinationFeedbackStyle = .gap
if item == nil, index < 0 {
return []
}
return .move
}
但是,与其每次都将其设置为 .gap
,不如在大纲视图中设置数据源时只设置一次。
我对Item
的定义应该等同于你的Catalog
。
//------------------------------
class Item: CustomStringConvertible
{
var description: String { title }
var title: String
var children: [Item] = []
//------------------------------
init(_ title: String) { self.title = title }
convenience init(_ id: Int) { self.init("Item \(id)") }
//------------------------------
func addChild() {
children.append(Item("\(title).\(children.count + 1)"))
}
//------------------------------
func parentAndChildIndex(forChildTitled title: String) -> (Item?, Int)?
{
for i in children.indices
{
let child = children[i]
if child.title == title { return (self, i) }
if let found = child.parentAndChildIndex(forChildTitled: title){
return found
}
}
return nil
}
}
这是我的数据源的完整实现:
//------------------------------
@objc class OVDataSource: NSObject, NSOutlineViewDataSource
{
//------------------------------
// Just creating some items programmatically for testing
var items: [Item] =
{
trace()
let items = (1...4).map { Item([=14=]) }
items[2].addChild()
items[2].addChild()
return items
}()
//------------------------------
func outlineView(
_ outlineView: NSOutlineView,
pasteboardWriterForItem item: Any) -> NSPasteboardWriting?
{
trace()
guard let item = item as? Item else { return nil }
return item.title as NSString
}
//------------------------------
func outlineView(
_ outlineView: NSOutlineView,
numberOfChildrenOfItem item: Any?) -> Int
{
trace()
if let item = item {
return (item as? Item)?.children.count ?? 0
}
return items.count
}
//------------------------------
func outlineView(
_ outlineView: NSOutlineView,
child index: Int,
ofItem item: Any?) -> Any
{
trace()
if let item = item as? Item {
return item.children[index]
}
return items[index]
}
//------------------------------
func outlineView(
_ outlineView: NSOutlineView,
isItemExpandable item: Any) -> Bool
{
trace()
if let item = item as? Item {
return item.children.count > 0
}
return false
}
//------------------------------
func outlineView(
_ outlineView: NSOutlineView,
validateDrop info: NSDraggingInfo,
proposedItem item: Any?,
proposedChildIndex index: Int) -> NSDragOperation
{
trace("item = \(String(describing: item)), index = \(index)")
guard info.draggingSource as? NSOutlineView === outlineView else {
return []
}
outlineView.draggingDestinationFeedbackStyle = .gap
if item == nil, index < 0 {
return []
}
return .move
}
//------------------------------
func outlineView(
_ outlineView: NSOutlineView,
acceptDrop info: NSDraggingInfo,
item: Any?,
childIndex index: Int) -> Bool
{
assert(item == nil || item is Item)
trace("item = \(String(describing: item)), index = \(index)")
guard let sourceTitle = info.draggingPasteboard.string(forType: .string),
let source = parentAndChildIndex(forItemTitled: sourceTitle)
else { return false }
let debuggedIndex = translateIndexForGapBug(
outlineView,
item: item,
index: index,
for: info
)
moveItem(from: source, to: (item as? Item, debuggedIndex))
outlineView.reloadData()
return true
}
//------------------------------
func translateIndexForGapBug(
_ outlineView: NSOutlineView,
item: Any?,
index: Int,
for info: NSDraggingInfo) -> Int
{
guard outlineView.draggingDestinationFeedbackStyle == .gap,
items.count > 0,
item == nil,
index == 0
else { return index }
let point = outlineView.convert(info.draggingLocation, from: nil)
let firstCellFrame = outlineView.frameOfCell(atColumn: 0, row: 0)
return outlineView.isFlipped
? (point.y < firstCellFrame.maxY ? index : items.count)
: (point.y >= firstCellFrame.minY ? index : items.count)
}
//------------------------------
func parentAndChildIndex(forItemTitled title: String) -> (parent: Item?, index: Int)?
{
trace("Finding parent and child for item: \"\(title)\"")
for i in items.indices
{
let item = items[i]
if item.title == title { return (nil, i) }
if let found = item.parentAndChildIndex(forChildTitled: title) {
return found
}
}
return nil
}
//------------------------------
func moveItem(
from src: (parent: Item?, index: Int),
to dst: (parent: Item?, index: Int))
{
trace("src = \(src), dst = \(dst)")
let item: Item = src.parent?.children[src.index]
?? items[src.index]
if src.parent === dst.parent // Moving item in same level?
{
if let commonParent = src.parent
{
moveItem(
item,
from: src.index,
to: dst.index,
in: &commonParent.children
)
return
}
moveItem(item, from: src.index, to: dst.index, in: &items)
return
}
// Moving between levels
if let srcParent = src.parent {
srcParent.children.remove(at: src.index)
}
else { items.remove(at: src.index) }
if let dstParent = dst.parent {
insertItem(item, into: &dstParent.children, at: dst.index)
}
else { insertItem(item, into: &items, at: dst.index) }
}
//------------------------------
// Move an item within the same level
func moveItem(
_ item: Item,
from srcIndex: Int,
to dstIndex: Int,
in items: inout [Item])
{
if srcIndex < dstIndex
{
insertItem(item, into: &items, at: dstIndex)
items.remove(at: srcIndex)
return
}
items.remove(at: srcIndex)
insertItem(item, into: &items, at: dstIndex)
}
func insertItem(_ item: Item, into items: inout [Item], at index: Int)
{
if index < 0
{
items.append(item)
return
}
items.insert(item, at: index)
}
}
trace()
调用仅用于调试。要么删除它们,要么实施它:
func trace(
_ message: @autoclosure () -> String = "",
function: StaticString = #function,
line: UInt = #line)
{
#if DEBUG
print("\(function):\(line): \(message())")
#endif
}
首先,对不起我的英语,我会尽力说清楚。(根据@Chip Jarred 的建议编辑,我做了一些更改以简化我的问题)
我成功实现了NSOutlineview的拖拽方式,使用.gap风格(我就是想用这个风格!):
outlineView.draggingDestinationFeedbackStyle = .gap
然后出现了问题,用gif描述会更简单:
https://i.stack.imgur.com/0avhJ.gif
您可以看到拖放可以 运行 部分正确。但问题是:当我将一个节点拖到列表的底部时,在 Node4 下面一点,它会被拖到列表的顶部。
我试图修复它,所以我在 validatDrop{} 中插入了一个“打印”函数:
func outlineView(_ outlineView: NSOutlineView,
validateDrop info: NSDraggingInfo,
proposedItem item: Any?,
proposedChildIndex index: Int) -> NSDragOperation {
print("item:\(item),index:\(index)")
if index < 0 && item == nil{
return []
}else{
outlineView.draggingDestinationFeedbackStyle = .gap
return .move
}
}
终端告诉我,当我将一个节点放到列表顶部或列表底部时,它返回相同的索引:
item:nil,index:0
https://i.stack.imgur.com/7pI7s.gif
如果我删除 .gap 样式:
func outlineView(_ outlineView: NSOutlineView,
validateDrop info: NSDraggingInfo,
proposedItem item: Any?,
proposedChildIndex index: Int) -> NSDragOperation {
print("item:\(item),index:\(index)")
if index < 0 && item == nil{
return []
}else{
// outlineView.draggingDestinationFeedbackStyle = .gap //ignore this line
return .move
}
}
https://i.stack.imgur.com/a8MMe.gif
一切都变得正常了。所以可以推断这可能不是我的“移动”方法的问题。
再次抱歉英语,如果有任何帮助,我将不胜感激。
这是我的代码的重要部分:
outlineView.registerForDraggedTypes([.string])
...
func outlineView(_ outlineView: NSOutlineView,
heightOfRowByItem item: Any) -> CGFloat {
return 15
}
...
extension SceneCatalogView{
func outlineView(_ outlineView: NSOutlineView,
pasteboardWriterForItem item: Any) -> NSPasteboardWriting? {
let sourceNode = outlineView.row(forItem: item)
return "\(sourceNode)" as NSString
}
func outlineView(_ outlineView: NSOutlineView,
validateDrop info: NSDraggingInfo,
proposedItem item: Any?,
proposedChildIndex index: Int) -> NSDragOperation {
if index < 0 && item == nil{
return []
}else{
outlineView.draggingDestinationFeedbackStyle = .gap
return .move
}
}
func outlineView(_ outlineView: NSOutlineView,
acceptDrop info: NSDraggingInfo,
item: Any?,
childIndex index: Int) -> Bool {
let pasteboard = info.draggingPasteboard
let sourceNode = Int(pasteboard.string(forType: .string)!)!
let source = outlineView.item(atRow: sourceNode) as? Catalog
let content = source?.content
let targetNode = outlineView.row(forItem: item)
moveNode(sourceNode, targetNode, index) // Not finished
outlineView.reloadData() // Not finished
return true
}
}
经过一番研究后,我发现多份关于使用 .gap
拖动方式时 NSTableView
错误的报告。 NSOutlineView
似乎继承了该错误。无论如何,我找到了解决方法。
问题是,当您拖动到最后一个顶级项目下方时,传递给 outlineView(_:acceptDrop:item:childIndex)
的 item
和 childIndex
始终是 nil
和 0
,这与拖动到列表顶部时得到的值完全相同。我能找到区分这两种情况的唯一方法是使用 NSDraggingInfo
中的 draggingLocation
与第一项的单元格框架进行比较,并使用它来翻译 index
.
func translateIndexForGapBug(
_ outlineView: NSOutlineView,
item: Any?,
index: Int,
for info: NSDraggingInfo) -> Int
{
guard outlineView.draggingDestinationFeedbackStyle == .gap,
items.count > 0,
item == nil,
index == 0
else { return index }
let point = outlineView.convert(info.draggingLocation, from: nil)
let firstCellFrame = outlineView.frameOfCell(atColumn: 0, row: 0)
return outlineView.isFlipped
? (point.y < firstCellFrame.maxY ? index : items.count)
: (point.y >= firstCellFrame.minY ? index : items.count)
}
我在outlineView(_:acceptDrop:item:childIndex)
中调用它:
func outlineView(
_ outlineView: NSOutlineView,
acceptDrop info: NSDraggingInfo,
item: Any?,
childIndex index: Int) -> Bool
{
assert(item == nil || item is Item)
trace("item = \(String(describing: item)), index = \(index)")
guard let sourceTitle = info.draggingPasteboard.string(forType: .string),
let source = parentAndChildIndex(forItemTitled: sourceTitle)
else { return false }
let debuggedIndex = translateIndexForGapBug(
outlineView,
item: item,
index: index,
for: info
)
moveItem(from: source, to: (item as? Item, debuggedIndex))
outlineView.reloadData()
return true
}
由于其他拖动方式似乎也有效,我只在设置为 .gap
时才这样做,所以为了测试,我的 outlineView(_:validateDrop:proposedItem:proposedChildIndex:)
看起来像这样:
func outlineView(
_ outlineView: NSOutlineView,
validateDrop info: NSDraggingInfo,
proposedItem item: Any?,
proposedChildIndex index: Int) -> NSDragOperation
{
trace("item = \(String(describing: item)), index = \(index)")
guard info.draggingSource as? NSOutlineView === outlineView else {
return []
}
outlineView.draggingDestinationFeedbackStyle = .gap
if item == nil, index < 0 {
return []
}
return .move
}
但是,与其每次都将其设置为 .gap
,不如在大纲视图中设置数据源时只设置一次。
我对Item
的定义应该等同于你的Catalog
。
//------------------------------
class Item: CustomStringConvertible
{
var description: String { title }
var title: String
var children: [Item] = []
//------------------------------
init(_ title: String) { self.title = title }
convenience init(_ id: Int) { self.init("Item \(id)") }
//------------------------------
func addChild() {
children.append(Item("\(title).\(children.count + 1)"))
}
//------------------------------
func parentAndChildIndex(forChildTitled title: String) -> (Item?, Int)?
{
for i in children.indices
{
let child = children[i]
if child.title == title { return (self, i) }
if let found = child.parentAndChildIndex(forChildTitled: title){
return found
}
}
return nil
}
}
这是我的数据源的完整实现:
//------------------------------
@objc class OVDataSource: NSObject, NSOutlineViewDataSource
{
//------------------------------
// Just creating some items programmatically for testing
var items: [Item] =
{
trace()
let items = (1...4).map { Item([=14=]) }
items[2].addChild()
items[2].addChild()
return items
}()
//------------------------------
func outlineView(
_ outlineView: NSOutlineView,
pasteboardWriterForItem item: Any) -> NSPasteboardWriting?
{
trace()
guard let item = item as? Item else { return nil }
return item.title as NSString
}
//------------------------------
func outlineView(
_ outlineView: NSOutlineView,
numberOfChildrenOfItem item: Any?) -> Int
{
trace()
if let item = item {
return (item as? Item)?.children.count ?? 0
}
return items.count
}
//------------------------------
func outlineView(
_ outlineView: NSOutlineView,
child index: Int,
ofItem item: Any?) -> Any
{
trace()
if let item = item as? Item {
return item.children[index]
}
return items[index]
}
//------------------------------
func outlineView(
_ outlineView: NSOutlineView,
isItemExpandable item: Any) -> Bool
{
trace()
if let item = item as? Item {
return item.children.count > 0
}
return false
}
//------------------------------
func outlineView(
_ outlineView: NSOutlineView,
validateDrop info: NSDraggingInfo,
proposedItem item: Any?,
proposedChildIndex index: Int) -> NSDragOperation
{
trace("item = \(String(describing: item)), index = \(index)")
guard info.draggingSource as? NSOutlineView === outlineView else {
return []
}
outlineView.draggingDestinationFeedbackStyle = .gap
if item == nil, index < 0 {
return []
}
return .move
}
//------------------------------
func outlineView(
_ outlineView: NSOutlineView,
acceptDrop info: NSDraggingInfo,
item: Any?,
childIndex index: Int) -> Bool
{
assert(item == nil || item is Item)
trace("item = \(String(describing: item)), index = \(index)")
guard let sourceTitle = info.draggingPasteboard.string(forType: .string),
let source = parentAndChildIndex(forItemTitled: sourceTitle)
else { return false }
let debuggedIndex = translateIndexForGapBug(
outlineView,
item: item,
index: index,
for: info
)
moveItem(from: source, to: (item as? Item, debuggedIndex))
outlineView.reloadData()
return true
}
//------------------------------
func translateIndexForGapBug(
_ outlineView: NSOutlineView,
item: Any?,
index: Int,
for info: NSDraggingInfo) -> Int
{
guard outlineView.draggingDestinationFeedbackStyle == .gap,
items.count > 0,
item == nil,
index == 0
else { return index }
let point = outlineView.convert(info.draggingLocation, from: nil)
let firstCellFrame = outlineView.frameOfCell(atColumn: 0, row: 0)
return outlineView.isFlipped
? (point.y < firstCellFrame.maxY ? index : items.count)
: (point.y >= firstCellFrame.minY ? index : items.count)
}
//------------------------------
func parentAndChildIndex(forItemTitled title: String) -> (parent: Item?, index: Int)?
{
trace("Finding parent and child for item: \"\(title)\"")
for i in items.indices
{
let item = items[i]
if item.title == title { return (nil, i) }
if let found = item.parentAndChildIndex(forChildTitled: title) {
return found
}
}
return nil
}
//------------------------------
func moveItem(
from src: (parent: Item?, index: Int),
to dst: (parent: Item?, index: Int))
{
trace("src = \(src), dst = \(dst)")
let item: Item = src.parent?.children[src.index]
?? items[src.index]
if src.parent === dst.parent // Moving item in same level?
{
if let commonParent = src.parent
{
moveItem(
item,
from: src.index,
to: dst.index,
in: &commonParent.children
)
return
}
moveItem(item, from: src.index, to: dst.index, in: &items)
return
}
// Moving between levels
if let srcParent = src.parent {
srcParent.children.remove(at: src.index)
}
else { items.remove(at: src.index) }
if let dstParent = dst.parent {
insertItem(item, into: &dstParent.children, at: dst.index)
}
else { insertItem(item, into: &items, at: dst.index) }
}
//------------------------------
// Move an item within the same level
func moveItem(
_ item: Item,
from srcIndex: Int,
to dstIndex: Int,
in items: inout [Item])
{
if srcIndex < dstIndex
{
insertItem(item, into: &items, at: dstIndex)
items.remove(at: srcIndex)
return
}
items.remove(at: srcIndex)
insertItem(item, into: &items, at: dstIndex)
}
func insertItem(_ item: Item, into items: inout [Item], at index: Int)
{
if index < 0
{
items.append(item)
return
}
items.insert(item, at: index)
}
}
trace()
调用仅用于调试。要么删除它们,要么实施它:
func trace(
_ message: @autoclosure () -> String = "",
function: StaticString = #function,
line: UInt = #line)
{
#if DEBUG
print("\(function):\(line): \(message())")
#endif
}