chipjarred / StackOverflowOutlineView

Answering a stack overflow question
0 stars 1 forks source link

Deeper discussion #1

Open tilseam opened 3 years ago

tilseam commented 3 years ago

Hi,Chip I'm the guy from stackoverflow, I've tested your code,I hope to explain my thoughts more clearly: I add one line in your code:

func outlineView(
        _ outlineView: NSOutlineView,
        validateDrop info: NSDraggingInfo,
        proposedItem item: Any?,
        proposedChildIndex index: Int) -> NSDragOperation
    {
        trace()
        if item == nil, index < 0 {
            return []
        }
        outlineView.draggingDestinationFeedbackStyle = .gap \\ ← I inserted here
        return .move
    }

It will be shown as below:

屏幕录制2021-04-10 下午7 19 55

And what confuses me most is that when dragging "Item4" down,it will be reorganized to the first row of the list. The same issue if I drag any other items.

chipjarred commented 3 years ago

I did some research. I have found several mentions of a suspected bug in NSTableView when using .gap dragging style. I suspect that NSOutlineView is inheriting that bug. The problem is that when using .gap the item and index passed to outlineView(_:acceptDrop:item:childIndex) is always nil, and 0, so of course it goes to the first location. I'm trying to figure out a work-around, but I think it will involve hit testing the dragged location, which is an NSPoint against subviews frames in window coordinates.

chipjarred commented 3 years ago

I believe I found a solution! Do a git pull and check it out. Let me know if it works as you expect for you.

Screenshot

I also updated my answer on StackOverflow

tilseam commented 3 years ago

Thx Chip! That works for me!

It is a brilliant idea to add a coordinate judgment, and it led me to think more deeply. I think we should make the interaction more intuitive, the coordinate judgment may be placed between validateDrop{} :

    func outlineView(_ outlineView: NSOutlineView,
                     validateDrop info: NSDraggingInfo,
                     proposedItem item: Any?,
                     proposedChildIndex index: Int) -> NSDragOperation {

        let point = outlineView.convert(info.draggingLocation, from: nil)
        let firstCellFrame = outlineView.frameOfCell(atColumn: 0, row: 0)
        let dragDown:Bool = outlineView.isFlipped // An indicator to determine whether it is dragged down
            ? (point.y < firstCellFrame.maxY ? false : true)
            : (point.y >= firstCellFrame.minY ? false : true)

        if (index < 0 && item == nil) ||
            (item == nil && index == 0 && dragDown){
            return []
        }else{
            outlineView.draggingDestinationFeedbackStyle = .gap
            return .move
        }
    }

Therefore, when we dragging down any items to the bottom, there won't be any incorrect animation feedback.

chipjarred commented 3 years ago

Yes, I noticed that the cell animation kept the last cell in a fixed location when dragging past the bottom cell so that the cell above it would animate on top of it. Does applying the draggingLocation in validateDrop fix the index for acceptDrop?

chipjarred commented 3 years ago

Actually animation strangeness I noticed was when moving a cell up.

I tried it with the changes to validateDrop. It no longer accepts drops past the last cell. It places the dragged item back into its original location. Is that the behavior you want?

tilseam commented 3 years ago

Actually animation strangeness I noticed was when moving a cell up.

I tried it with the changes to validateDrop. It no longer accepts drops past the last cell. It places the dragged item back into its original location. Is that the behavior you want?

I didn't find any problem in my program, the Finder's sidebar gives the same feedback, I would think this is natural. 屏幕录制2021-04-11 下午12 08 37

chipjarred commented 3 years ago

If I remove my fix to acceptDrop and add your changes to validateDrop, this is what I get:

Screen Recording 2021-04-10 at 11 17 00 PM

I get the same result if I restore my fix for acceptDrop and keep your changes to validateDrop. Notice how the items do not move to the end of the list. They go back to their original location.

I also notice that in my debugging output that validateDrop is getting an index of -2.

tilseam commented 3 years ago

屏幕录制2021-04-11 下午12 47 34 I think it is normal that the item to return to its original position after being dragged far from the list boundary(return index of -2), as Finder feedbacks in the same way. When you drag one item to to the bottom of the list, you can only drag a little bit below the last item in the list.

chipjarred commented 3 years ago

Whether it is more natural is a matter of taste. I'm trying to help you achieve the look and feel you want, not what I would prefer, but I will give you a different perspective to consider.

The .gap dragging style makes me feel like I'm trying shoot a bird in flight. Things are moving around while I'm trying to place the item where I want. It's not that big of a deal, but I do notice my frustration rises when views do that.

With the .regular style, the items stay where they are. My brain doesn't have to engage in dynamic targeting, and the insertion line very clearly says "If you drop now, this is where it will go." It's visually less flashy, but I think more effective UI design.

chipjarred commented 3 years ago

I think the Finder side bar is an NSTableView. If you look at the main Finder view that shows folder contents in list mode, it is hierarchical so must be an NSOutlineView. It does not use .gap. Perhaps we have discovered why.

Speaking as a user of applications, if I drag an item below the last item in a list, as long as I'm still in the list view, I expect it to go below the last item. The Finder side bar does that, except that you have different list views. When you drag too far down you leave your favorite items and move into the view for labels... that's a different view, so the drop is not valid.

Nonetheless, it is your app, so the decision is yours, not mine. If the change gives you the behavior you want then we have succeeded.

tilseam commented 3 years ago

Thank you sincerely! I've got what I want! This problem has entangled me for weeks, I am really not sure whether this is a "bug" or not, or just I set sth wrong.

Finder is a NSTableView. And I think there may be a reason why Apple left this "bug"——Apple may discourage to use .gap style with NSOutlinView.

chipjarred commented 3 years ago

In this case, it seems to be Apple's bug. I would guess that the only reason Apple has not fixed it is because not enough developers have complained, so it has not been a high priority. And if all the developers who do complain find a work-around, there is less pressure on Apple to fix it.

NSTableView and NSOutlineView are very old classes. They go way back to NeXTStep (which is where the NS prefix comes from). I think .gap was retrofitted into it sometime in the past 10 years.

tilseam commented 3 years ago

lol...As a Newbie who has been in contact with programming/swift for less than half a year, I dare not think that I will reach the place of programming language bugs. Besides, NSOutlineView should be a very basic component... Although during this period of time, I have suffered a lot from the legacy problems of OC, Swift 3 or various versions of the language.

tilseam commented 3 years ago

Before I started writing programs, I thought it has been an era of NO CODE !! And implementing some basic requirements should be as simple as building LEGOs...I really never thought about facing sth. like this...

chipjarred commented 3 years ago

It doesn't help that Swift is a moving target, and now there is SwiftUI to learn. I'm a little concerned that Swift is starting to enable too much "magic." In particular I'm thinking of result builders. SwiftUI would not exist without them, but the compiler has to generate a lot code for you that's hard to reason about. When it goes well, it results in a beautiful declarative style. But when you get it wrong, often the errors you get aren't even on the line of code causing the error, which makes it hard to figure out what you did wrong. And you can get unexpected behavior without a good indication of why.

I was just thinking about when I first noticed the .gap dragging style. It was some time after the iPhone came out. UITableView supports it, so my guess is that they added it to NSTableView to have an equivalent behavior on macOS. The problem is that NSOutlineView inherits from NSTableView and I don't think there is anything like NSOutlineView in UIKit. Hierarchical structures usually involve navigating to a new view on iOS.

chipjarred commented 3 years ago

Lol..People keep trying to come up with something like LEGO-style programming, but they always fall short of the goal. We do get some new libraries that make things easier. Imagine if there were no AppKit. You'd have to process events manually. In fact that's what we had to do back in classic macOS days (before OS X)... and do it in a lot less RAM with less disk space.