1

I have an custom collectionView:

import AppKit

final class InternalCollectionView: NSCollectionView {
    typealias KeyDownHandler = (_ event: NSEvent) -> Bool
    var keyDownHandler: KeyDownHandler? = nil
    
    // Do nothing on Cmd+A
    override func selectAll(_ sender: Any?) { }
}

also I have collectionView for SwiftUI with some controller used inside:

struct FBCollectionView<Content: View>: NSViewControllerRepresentable {
//here some implementation
}

public class NSCollectionController<Content: View>: NSViewController, NSCollectionViewDelegate, NSCollectionViewDataSource, QLPreviewPanelDataSource, QLPreviewPanelDelegate {
//here some implementation
}

I need to implement logic:

  • Items on drag must be drawn on their places, but not hidden [done]
  • The App must to be hidden on drag outside of the App

First of all I have tried to just hide the App on drag begins. For this I have implemented method of NSCollectionController :

public func collectionView(_ collectionView: NSCollectionView, draggingSession session: NSDraggingSession, willBeginAt screenPoint: NSPoint, forItemsAt indexPaths: Set<IndexPath>) {
    
    hideApp()
    
    preventHidingItemsDuringDrag(collectionView, indexPaths: indexPaths)
}

func hideApp() {
    DispatchQueue.main.async {
        NSApplication.shared.hide(self)
    }
    
    appShown = false
    automaticScroller.updStatus(appDisplayed: appShown)
}

but for some reason this works only on first drag(!) on each following drags app does not hide

I have tried to run this code in main thread, but didn't get any usable results

So question is:

  • How to hide app on drag outside of the app?
Andrew_STOP_RU_WAR_IN_UA
  • 9,318
  • 5
  • 65
  • 101
  • Do you want to hide the app or the main window? What do `preventHidingDuringDrag(collectionView, indexPaths: indexPaths)`, `appShown` and `automaticScroller.updStatus(appDisplayed: appShown)` do? – Willeke May 12 '23 at 04:56
  • @Willeke , `preventHidingDuringDrag()` = Items on drag must be drawn on their places, but not hidden ; Do you want to hide the app or the main window? - hide the app; `appShown` - just flag that checks that app is hidden even if it is active (menu line displayed on the top of screen); `automaticScroller` - is internal class that scrolls `NSCollectionView` to the top after 60 seconds app inactive – Andrew_STOP_RU_WAR_IN_UA May 12 '23 at 05:30
  • I tried to hide the app but it hides after I drop the item every time. Is this a SwftUI question? – Willeke May 12 '23 at 11:24
  • @Willeke no, it's closer to AppKit / old UI system question – Andrew_STOP_RU_WAR_IN_UA May 12 '23 at 15:15
  • I don't think you can hide the app during mouse tracking. Post a [mre] of the code that works only on first drag please. – Willeke May 16 '23 at 08:16
  • @Willeke it's possible because of I did this with custom draggable extension for swiftUI that I cannot use in this situation. If it was possible in swiftUI it's absolutely sure possible in AppKit. And absolutely sure I saw such behaviour in other macOS apps. If you need, I can find exact apps names :) – Andrew_STOP_RU_WAR_IN_UA May 16 '23 at 08:58
  • If you start a drag in AppKit then the mouse is tracked in a separate loop. The app returns to the runloop of the main thread after mouse-up. How does SwiftUI do this? – Willeke May 16 '23 at 09:43
  • Is hiding the window instead of the app an option? – Willeke May 16 '23 at 09:50
  • Yep, I see no difference between app and window hide in my case :) I have created repo with this CollectionView: https://github.com/ukushu/SwiftUITestProj – Andrew_STOP_RU_WAR_IN_UA May 16 '23 at 16:41

2 Answers2

1

You might consider using addLocalMonitorForEvents (I thought about addGlobalMonitorForEvents, but... as illustrated here, it would require for the app to have accessibility access)

However, as noted by the OP in the comments:

it's hide app only after release of mouse button. For some reason collectionView holds drawning of the window (in my case it is NSPanel). And hideApp() is called ONLY AFTER I drop mouse button (I see this in logs)

So instead, let's try another to monitor the dragging session status.

Reading "Supporting Table View Drag and Drop Through File Promises", I see:

When a drag starts, you adopt the NSPasteboardWriting protocol to write data to the NSPasteboard. When a drag occurs, you determine the valid drop target. When the drag ends, you read the drag data from the NSPasteboard."

Picking up on that:

import AppKit
import SwiftUI

public class NSCollectionController<Content: View>: NSViewController, NSCollectionViewDelegate, NSCollectionViewDataSource, QLPreviewPanelDataSource, QLPreviewPanelDelegate {
    
    // Flag to check whether the app is currently visible.
    static var appShown = true
    
    // A helper object for automatically scrolling the collection view.
    var automaticScroller: AutomaticScroller!

    // NSCollectionViewDelegate

    // This function is called when the user starts dragging an item.
    // We return our custom pasteboard writer, which also conforms to NSDraggingSource, for the dragged item.
    public func collectionView(_ collectionView: NSCollectionView, pasteboardWriterForItemAt indexPath: IndexPath) -> NSPasteboardWriting? {
        return MyPasteboardWriter()
    }

    // This function is called when a dragging session ends. At this point, we reset our appShown flag to true.
    public func collectionView(_ collectionView: NSCollectionView, draggingSession session: NSDraggingSession, endedAt screenPoint: NSPoint, dragOperation operation: NSDragOperation) {
        NSCollectionController.appShown = true
    }
    
    // A helper function to hide the app.
    static func hideApp() {
        DispatchQueue.main.async {
            NSApplication.shared.hide(nil)
        }
        appShown = false
        // Here you would call a function to update the automatic scroller.
        // automaticScroller.updStatus(appDisplayed: appShown)
    }

    // Our custom pasteboard writer. This class also implements NSDraggingSource to handle the dragging of the item.
    private class MyPasteboardWriter: NSObject, NSPasteboardWriting, NSDraggingSource {
        
        // NSPasteboardWriting
        
        // This function returns the types of data that this object can write to the pasteboard.
        func writableTypes(for pasteboard: NSPasteboard) -> [NSPasteboard.PasteboardType] {
            // You need to implement this method based on the data your items can represent.
            // For example, if your items can be represented as strings, you can return [.string].
        }

        // This function returns a property list that represents the data of this object for a specific type.
        func pasteboardPropertyList(forType type: NSPasteboard.PasteboardType) -> Any? {
            // You need to implement this method based on the data of your item for the given type.
            // For example, if your items can be represented as strings and type is .string, you can return the string representation of your item.
        }

        // NSDraggingSource

        // This function returns the allowed operations (like .copy, .move) when the dragging is outside the source application.
        func draggingSession(_ session: NSDraggingSession, sourceOperationMaskFor context: NSDraggingContext) -> NSDragOperation {
            return [.copy, .move]
        }

        // This function is called when the dragging image is moved.
        // Here we check if the mouse is outside the app window, and if so, we hide the app.
        func draggingSession(_ session: NSDraggingSession, movedTo screenPoint: NSPoint) {
            guard let window = NSApplication.shared.mainWindow, NSCollectionController.appShown else { return }
            let windowRectInScreenCoordinates = window.convertToScreen(window.frame)
            if !windowRectInScreenCoordinates.contains(screenPoint) {
                NSCollectionController.hideApp()
            }
        }

        // This function is called when the drag operation ends. There is no need to do anything here in this case.
        func draggingSession(_ session: NSDraggingSession, endedAt
        func draggingSession(_ session: NSDraggingSession, endedAt screenPoint: NSPoint, operation: NSDragOperation) {
            // You can add any cleanup operations here after a drag operation ends
        }
    }
}

The NSCollectionController class is a controller for an NSCollectionView. It handles many tasks, including acting as the delegate and data source for the collection view, and managing the drag-and-drop interactions.

In order to hide the entire application when a dragged item is moved outside the application window, the idea is to a custom class (MyPasteboardWriter) that conforms to both NSPasteboardWriting and NSDraggingSource protocols.
The NSPasteboardWriting protocol enables the class to provide data to the pasteboard (which is used during drag-and-drop operations), while NSDraggingSource allows it to react to drag-and-drop events.

In the NSDraggingSource protocol, the draggingSession(_:movedTo:) method is implemented to check the location of the dragged item. If the item is moved outside the application window, the application is hidden. This is done by using the NSApplication.shared.hide(nil) function.

The appShown static variable is used to keep track of whether the application is currently visible or not. It's important to prevent the application from attempting to hide multiple times in succession.

The draggingSession(_:sourceOperationMaskFor:) method is also implemented to specify the allowed operations (.copy, .move) when the dragging is outside the source application.

Finally, the collectionView(_:draggingSession:endedAt:dragOperation:) delegate method is used to reset the appShown flag back to true when a dragging session ends, indicating that the application can now be shown again.


movedTo function never called, so app cannot be hidden.

  • Make sure you properly set up the dragging session and the item you are dragging uses your custom MyPasteboardWriter as its pasteboard writer.

  • The class that adopts the NSDraggingSource protocol and implements the draggingSession(_:movedTo:) method must be the one used as the source object when initiating the dragging session.
    If you are using a different object as the source, the method won't be called.

VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
  • problem of the code that it's hide app only after release of mouse button. For some reason collectionView holds drawning of the window (in my case it is NSPanel). And `hideApp()` is called ONLY AFTER I drop mouse button (I see this in logs). Another problem that after such hiding app hides on each mouse move after I perform first drag. – Andrew_STOP_RU_WAR_IN_UA May 14 '23 at 17:03
  • @Andrew_STOP_RU_WAR_IN_UA OK I have rewritten the answer to try another approach based on `NSDraggingSource`. – VonC May 15 '23 at 13:13
  • Will check your answer in near 5-6 hrs, thanks a lot for your help – Andrew_STOP_RU_WAR_IN_UA May 15 '23 at 15:06
  • Checked your answer. Issues here: 1) `session.draggingSource = self` - there is no draggingSource; 2) `NSDraggingSource` require additiional method - `sourceOperationMaskFor`, I donnow how to implement it correctly; 3) `movedTo` never called. Please, check the video: https://drive.google.com/file/d/1rATC41TD139li2NsGIA8K2NsE39oX1tD/view?usp=share_link – Andrew_STOP_RU_WAR_IN_UA May 16 '23 at 07:19
  • as I think now - I need only method "sourceOperationMaskFor". I need to return there ".copy" and there is exist logic for check that drag inside the App or not. Please, check this: https://i.stack.imgur.com/P01sT.png ; But.... I do not understand why this method never called. – Andrew_STOP_RU_WAR_IN_UA May 16 '23 at 07:59
  • @Andrew_STOP_RU_WAR_IN_UA OK. I have revised the code, but I do not see a sourceOperationMaskFor method to implement in [`NSDraggingSource`](https://developer.apple.com/documentation/appkit/nsdraggingsource) – VonC May 16 '23 at 08:23
  • https://i.stack.imgur.com/s8T2y.png - xcode: Version 14.3 (14E222b) – Andrew_STOP_RU_WAR_IN_UA May 16 '23 at 08:55
  • its first in your link: https://developer.apple.com/documentation/appkit/nsdraggingsource/1416000-draggingsession – Andrew_STOP_RU_WAR_IN_UA May 16 '23 at 08:59
  • @Andrew_STOP_RU_WAR_IN_UA It is a draggingSession() method, which is implemented in my code: `return [.copy, .move]`. sourceOperationMaskFor is a parameter of the draggingSession() method. – VonC May 16 '23 at 09:05
  • Oh, I miss it in your code, sorry. But still - it's never called as you can see in the video =( https://drive.google.com/file/d/1rATC41TD139li2NsGIA8K2NsE39oX1tD/view there is called `willBeginAt ` and `endedAt`, no other functions called as you can see in buttom right corner of the video – Andrew_STOP_RU_WAR_IN_UA May 16 '23 at 09:08
  • 1
    @Andrew_STOP_RU_WAR_IN_UA Be aware, I changed the code this morning: try it again, and let me know if you see errors with it. – VonC May 16 '23 at 09:10
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/253673/discussion-between-vonc-and-andrew-stop-ru-war-in-ua). – VonC May 16 '23 at 09:10
  • Looks like this is exactly what I need! Give me few hours to check details and I will mark this as the correct answer! Really appreciated for your help – Andrew_STOP_RU_WAR_IN_UA May 16 '23 at 09:23
  • `movedTo` function never called, so app cannot be hidden. I did separate CollectionView component from my app and configured repo for you. Please check it: https://github.com/ukushu/SwiftUITestProj – Andrew_STOP_RU_WAR_IN_UA May 16 '23 at 16:39
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/253689/discussion-between-vonc-and-andrew-stop-ru-war-in-ua). – VonC May 16 '23 at 20:56
1

I don't think hiding the app during a drag will work. Hiding the window is possible.

Subclass NSCollectionView and override func draggingSession(_ session: NSDraggingSession, movedTo screenPoint: NSPoint). Hide the window when screenPoint is outside the window.

extension InternalCollectionView {
    override func draggingSession(_ session: NSDraggingSession, movedTo screenPoint: NSPoint) {
        super.draggingSession(session, movedTo: screenPoint)
        
        guard let currentWnd = self.window, currentWnd.isVisible else { return }
            
        //if drag is going outside all of FileBo windows
        guard NSApp.windows.compactMap({ $0.frame.contains(screenPoint) }).allSatisfy({ $0 == false}) else { return }
        
        currentWnd.setIsVisible(false)
        
        hideApp()
    }
}

Andrew_STOP_RU_WAR_IN_UA
  • 9,318
  • 5
  • 65
  • 101
Willeke
  • 14,578
  • 4
  • 19
  • 47