Xcode Swift MacOS App, drag and drop file into NSTextField - swift

I'm implementing my first app for MacOS and the user should input a file path to be processed.
I have a NSTextField on my NSViewController app and I'd like to let the user just drag and drop one file there so I could get the file path, open it and put on the NSTextField some text wi the info about the file.
Can you please help me? I saw that if I make the NSTextField editable I can drop the file but I don't want the NSTextField to be editable (just selectable to copy paste info)
Thanks!

First of all, you need to read this guide.
Second, I post here some code that I use to do something similar to what you are asking.
However, my strategy is not to subclass NSTextField but rather place this field inside an NSBox, which I subclass. This has the advantage of providing to the user some visual feedback using a focus ring.
Pay attention to performDragOperation where the string value is set via the window's controller, which then forwards it to the text field to set its string value to the path to the dropped file.
You can filter what you can accept by prepareForDragOperation. Check that too.
class DropBox: NSBox
{
let dragType = NSPasteboard.PasteboardType(kUTTypeFileURL as String)
var doHighlight = false
// ---------------------------------------------------------------------------------
// awakeFromNib
// ---------------------------------------------------------------------------------
override func awakeFromNib()
{
registerForDraggedTypes([dragType])
}
// ---------------------------------------------------------------------------------
// acceptsFirstMouse
// ---------------------------------------------------------------------------------
// Accept activation click as click in window, so source doesn't have to be the
// active window
override func acceptsFirstMouse(for event: NSEvent?) -> Bool
{
return true
}
// ---------------------------------------------------------------------------------
// draggingEntered
// ---------------------------------------------------------------------------------
override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation
{
let pasteboard = sender.draggingPasteboard
let mask = sender.draggingSourceOperationMask
if let types = pasteboard.types, types.contains(dragType)
{
if mask.contains(.link)
{
doHighlight = true
needsDisplay = true
return .link
}
}
return []
}
// ---------------------------------------------------------------------------------
// draggingExited
// ---------------------------------------------------------------------------------
override func draggingExited(_ sender: NSDraggingInfo?)
{
doHighlight = false
needsDisplay = true
}
// ---------------------------------------------------------------------------------
// drawRect
// ---------------------------------------------------------------------------------
override func draw(_ dirtyRect: NSRect)
{
super.draw(dirtyRect)
if doHighlight {
let rect = NSRect(x: dirtyRect.origin.x,
y: dirtyRect.origin.y,
width: NSWidth(dirtyRect),
height: NSHeight(dirtyRect) - NSHeight(titleRect) + 1.0)
NSFocusRingPlacement.only.set()
let contentRect = rect.insetBy(dx: 4, dy: 4)
NSBezierPath(rect: contentRect).fill()
}
}
// ---------------------------------------------------------------------------------
// performDragOperation
// ---------------------------------------------------------------------------------
// Method to handle drop data
override func performDragOperation(_ sender: NSDraggingInfo) -> Bool
{
if let source = sender.draggingSource as? NSBox {
if source === self {
return false
}
}
let pasteboard = sender.draggingPasteboard
let options = [NSPasteboard.ReadingOptionKey.urlReadingFileURLsOnly:true]
if let urls = pasteboard.readObjects(forClasses: [NSURL.self], options: options) as? [URL],
let controller = self.window?.delegate as? WindowController
{
for url in urls {
if SchISCoreFileUtilities.isValid(url.path) {
controller.setApplicationPath(url.path)
return true
}
}
}
return false
}
// ---------------------------------------------------------------------------------
// prepareForDragOperation
// ---------------------------------------------------------------------------------
// Method to determine if we can accept the drop (filter for urls to apps)
override func prepareForDragOperation(_ sender: NSDraggingInfo) -> Bool
{
doHighlight = false
needsDisplay = true
let pasteboard = sender.draggingPasteboard
if let types = pasteboard.types, types.contains(dragType)
{
let options = [NSPasteboard.ReadingOptionKey.urlReadingFileURLsOnly:true]
if let urls = pasteboard.readObjects(forClasses: [NSURL.self], options: options) as? [URL]
{
for url in urls {
if url.pathExtension == "app" {
return true
}
}
}
}
return false
}
}

Related

NSDocument-Based Application: Selecting Default File Type in NSSavePanel

I'm playing with the NSDocument class to make a simple document-based application. My info.plist contains four document content type identifiers including public.text, public.plain-text, public.source-cde, public.rtf as shown above. And I get those file types listed if I involke the save-panel (NSSavePanel) as shown below.
My question is whether or not it is possible to select one of the file types programmatically. Can I select 'rich text (RTF)' when the save panel appears?
The following is part of my document (NSDocument) file.
import Cocoa
class Document: NSDocument {
override init() {
super.init()
}
override class var autosavesInPlace: Bool {
return false
}
override func save(withDelegate delegate: Any?, didSave didSaveSelector: Selector?, contextInfo: UnsafeMutableRawPointer?) {
if let _ = fileURL {
Swift.print("Saved!!!")
} else {
Swift.print("Not saved yet...")
NSApp.sendAction(#selector(NSDocument.saveAs(_:)), to: nil, from: self)
}
}
override func writableTypes(for saveOperation: NSDocument.SaveOperationType) -> [String] {
return super.writableTypes(for: saveOperation)
}
override func prepareSavePanel(_ savePanel: NSSavePanel) -> Bool {
savePanel.allowsOtherFileTypes = true
savePanel.isExtensionHidden = false
guard let accessoryView = savePanel.accessoryView else { return true }
for sub in accessoryView.subviews {
Swift.print("Class: \(sub.className)")
/*
if sub.isKind(of: NSPopUpButton.self) {
if let popUpButton = sub as? NSPopUpButton {
popUpButton.selectItem(at: 5)
Swift.print("Sure")
}
}
*/
}
return true
}
}
I see this topic as a similar title where he uses IKSaveOptions, which is used 'for saving image data' according to the doc. My application deals with text.
Thanks.
The default file format is fileType. Set fileType in writableTypes(for:) or runModalSavePanel(for:delegate:didSave:contextInfo:).
override func writableTypes(for saveOperation: NSDocument.SaveOperationType) -> [String] {
fileType = "public.rtf"
return super.writableTypes(for: saveOperation)
}

Swift. How to allow dragging an URL from the browser to a NSView

I have an NSView that accepts dropped files and store their fileURLs but I'd like it to accept HTTP URLs dragged from an Internet Browser as well. I mean, the same behavior as dragging it to Finder to create a .webloc file but inside my app.
What's the proper PasteboardType to use? I've tried .fileContents, .urL, .fileURL, .html, .string… to no avail.
EDIT: This is the relevant code as requested:
required init?(coder: NSCoder) {
super.init(coder: coder)
wantsLayer = true
layer?.backgroundColor = NSColor.clear.cgColor
registerForDraggedTypes([NSPasteboard.PasteboardType(rawValue: "NSFilenamesPboardType"), .URL, .string, .html])
}
override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
showFrame(true)
return .copy
}
override func draggingExited(_ sender: NSDraggingInfo?) {
showFrame(false)
}
override func draggingEnded(_ sender: NSDraggingInfo) {
showFrame(false)
}
override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
if let board = sender.draggingPasteboard.propertyList(forType: NSPasteboard.PasteboardType(rawValue: "NSFilenamesPboardType")) as? NSArray
{
print("FILE: \(board)")
return true
} else if let board = sender.draggingPasteboard.propertyList(forType: .URL) as? NSArray {
print("URL: \(board)")
return true
} else if let board = sender.draggingPasteboard.propertyList(forType: .string) as? NSArray {
print("STRING: \(board)")
return true
} else if let board = sender.draggingPasteboard.propertyList(forType: .html) as? NSArray {
print("HTML: \(board)")
return true
}
return false
}
EDIT AGAIN:
I've found what is the problem. It turns out that I have a tableview inside this NSView and it is registered for .string dragged types too (to be able to reorder the cells). It seems the table "swallows" the URL type but let the filenames pass through. I'll have to handle this issue but this is another question.
The data on the pasteboard is not a property list and URL is not a property list compatible class. Use readObjects(forClasses:options:) instead of propertyList(forType:).
if let board = sender.draggingPasteboard().readObjects(forClasses: [NSURL.self]) {
print("URL: \(board)")
return true
}

Change search field's icon

I try to implement search behavior like in Xcode: if you enter something in search field, icon changes color.
I delegate both searchFieldDidStartSearching and searchFieldDidEndSearching to controller and change the image.
The problem is icon's image changes only when window lose it's focus.
class ViewController: NSViewController {
#IBOutlet weak var searchField: NSSearchField!
func searchFieldDidStartSearching(_ sender: NSSearchField) {
print("\(#function)")
(searchField.cell as! NSSearchFieldCell).searchButtonCell?.image = NSImage.init(named: "NSActionTemplate")
}
func searchFieldDidEndSearching(_ sender: NSSearchField) {
print("\(#function)")
(searchField.cell as! NSSearchFieldCell).searchButtonCell?.image = NSImage.init(named: "NSHomeTemplate")
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
}
Thanks in advance for any ideas/suggestions.
Although I don't know the reason, it works:
NSApp.mainWindow?.resignMain()
NSApp.mainWindow?.becomeMain()
Here is the whole code:
class MyViewController: NSViewController {
private lazy var searchField: NSSearchField = {
let searchField = NSSearchField(string: "")
if let searchButtonCell = searchField.searchButtonCell {
searchButtonCell.setButtonType(.toggle)
let filterImage = #imageLiteral(resourceName: "filter")
searchButtonCell.image = filterImage.tinted(with: .systemGray)
searchButtonCell.alternateImage = filterImage.tinted(with: .systemBlue)
}
searchField.focusRingType = .none
searchField.bezelStyle = .roundedBezel
searchField.delegate = self
return searchField
}()
...
}
extension MyViewController: NSSearchFieldDelegate {
func searchFieldDidStartSearching(_ sender: NSSearchField) {
sender.searchable = true
}
func searchFieldDidEndSearching(_ sender: NSSearchField) {
sender.searchable = false
}
}
extension NSSearchField {
var searchButtonCell: NSButtonCell? {
(self.cell as? NSSearchFieldCell)?.searchButtonCell
}
var searchable: Bool {
get {
self.searchButtonCell?.state == .on
}
set {
self.searchButtonCell?.state = newValue ? .on : .off
self.refreshSearchIcon()
}
}
private func refreshSearchIcon() {
NSApp.mainWindow?.resignMain()
NSApp.mainWindow?.becomeMain()
}
}
extension NSImage {
func tinted(with color: NSColor) -> NSImage? {
guard let image = self.copy() as? NSImage else { return nil }
image.lockFocus()
color.set()
NSRect(origin: NSZeroPoint, size: self.size).fill(using: .sourceAtop)
image.unlockFocus()
image.isTemplate = false
return image
}
}
I was having the same issue. A simple override fixed this issue for me
extension NSSearchField{
open override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
}
}
As you can see when you click inside the view it's still focussed on the search text field(as you can still type in it after you clicked underneath it). Since the change image is on when it loses focus, you should check if you clicked outside of the text field.
Solve problem by subclassing NSSearchFieldCell and assign this class to field's cell.
You don't even need to subclass NSSearchFieldCell.
When you create your NSSearchField from code, you can do something like this:
if let searchFieldCell = searchField.cell as? NSSearchFieldCell {
let image = NSImage(named: "YourImageName")
searchFieldCell.searchButtonCell?.image = image
searchFieldCell.searchButtonCell?.alternateImage = image // Optionally
}
If you're using storyboards, you can do the same in didSet of your #IBOutlet.

Cocoa Drag and Drop doesn't work the first time app is launched

I've an NSTableView and an NSOutlineView in my app. The user should be able to drag an item from the outline view to the table view; if it's an acceptable operation (i.e. the selected rows indices are in the pasteboard) then the table view's destination row lights up. Everything works except for when the item(s) are initially dragged into the table view.
Each NSTableCellView in the receiving table has these methods:
func dragContainsIndices(pasteboard: NSPasteboard) -> IndexSet {
if let types:[String] = pasteboard.types {
for type in types {
if type == BTVPBoardType { //_1
if let indexData: Data = pasteboard.data(forType: BTVPBoardType) {. //_2
let indicies: IndexSet = NSKeyedUnarchiver.unarchiveObject(with: indexData) as! IndexSet
if indicies.count > 0 {
return indicies
}
}
}
}
}
return IndexSet()
}
override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
let pastboard: NSPasteboard = sender.draggingPasteboard()
if self.dragContainsIndices(pasteboard: pastboard).count > 0 {
self.backingIcon.imageColour = .darkGreen //proprietary background colouring method
}
return .link
}
If the user tries to drag row(s) from the outline view to the table view then the draggingEntered(_:) in the table cell view runs but dragContainsIndices(_:) doesn't recognise the NSPasteboard as having any data even though it contains the correct type. The check at //_1 passes but then the code finds no data. So it returns an empty set which causes the background colour not to be changed.
Without releasing anything, if the user drags the item off the cell and then back on to it then the index set will unwrap correctly and the colour will change.
In the NSTableView I provide the drag session with theses methods:
override func mouseDragged(with event: NSEvent) {
let mouseDown = self.mouseDownEvent.locationInWindow
let dragPoint = event.locationInWindow
let dragDistance = hypot(mouseDown.x - dragPoint.x, mouseDown.y - dragPoint.y)
if dragDistance < 3 || self.selectedRowIndexes.count < 1 {
return
}
let pboardItem: NSPasteboardItem = NSPasteboardItem()
if pboardItem.setDataProvider(self, forTypes: [BTVPBoardType]) {
Swift.print ("Set data")
}
let dragItem: NSDraggingItem = NSDraggingItem(pasteboardWriter: pboardItem)
var dragPos = self.convert(event.locationInWindow, from: nil)
dragImage = NSImage.init(named: NSImageNameMultipleDocuments)!
dragPos.x += dragImage.size.width
dragPos.y += dragImage.size.height
dragItem.imageComponentsProvider = {
let component = NSDraggingImageComponent(key: NSDraggingImageComponentIconKey)
component.contents = self.dragImage
component.frame = NSRect(origin: NSPoint(), size: self.dragImage.size)
return [component]
}
beginDraggingSession(with: [dragItem], event: event, source: self)
}
func pasteboard(_ pasteboard: NSPasteboard?, item: NSPasteboardItem, provideDataForType type: String) {
if files.count < 1 {return} //don't bother if there's no
if let pasteboard = pasteboard {
pasteboard.declareTypes([BTVPBoardType], owner: self)
let _: Int = pasteboard.clearContents()
let indices = self.selectedRowIndexes
pasteboard.setData(NSKeyedArchiver.archivedData(withRootObject: indices), forType: BTVPBoardType)
Swift.print("written objects") //always writes indices
}
}
Is the pasteboard(_ pasteboard: NSPasteboard?, item: NSPasteboardItem, provideDataForType type: String) is it something to do with lazy fetching? For some reason I am having a bit of a head block with drag and dropping on Mac.

Display Only "Customize Toolbar..." in NSToolbar's Context Menu in Swift

I know this question has been asked many times but it seems no better solution for it.
Changing the allowsUserCustomization property doesn't help. It seems there is no API to customize the items in toolbar's context menu.
Finder app has no "Use Small Size" while Notes app has only "Customize Toolbar.."
I would like to know if there is any way to subclass or extend or do whatever to the NSToolbar to achieve the purpose?
Updated 1:
According to #Khundragpan and this post, problem 1 can be solved by:
if let contextMenu = window?.contentView?.superview?.menu {
for item in contextMenu.items {
if item.title != "Customize Toolbar…" {
contextMenu.removeItem(item)
}
}
}
But I don't think it's the best way.
Update 2:
Another way to solve problem 1 (thanks to #1024jp to point out this file):
if let contextMenu = window?.contentView?.superview?.menu {
contextMenu.items.forEach({ (item) in
if let action = item.action,
NSStringFromSelector(action) != "runToolbarCustomizationPalette:" {
contextMenu.removeItem(item)
}
})
}
Update 3:
A ton of thanks to #1024jp for helping me. I'm able to remove those things with a few tips and tricks from him. Check the answer below.
After 3 days, I finally did it. Here is the result.
Source Code in Swift 3
You can implement and make your own class, but here I just want to keep everything in a file.
This is the WindowController.swift file. You can set the custom class of your window controller and run. Again thanks to #1024jp for the tips.
//
// WindowController.swift
// The Toolbar
//
// Created by João Oliveira on 22/09/2016.
// Copyright © 2016 João Oliveira. All rights reserved.
//
import Cocoa
class WindowController: NSWindowController {
override func windowDidLoad() {
super.windowDidLoad()
guard let window = window else { return }
window.delegate = self
window.toolbar = NSToolbar(identifier: "RestrictedToolbar")
window.toolbar?.allowsUserCustomization = true
window.toolbar?.displayMode = .iconOnly
window.toolbar?.delegate = self
keepOnlyCustomizableMenu()
}
// PROBLEM 1: Solution
func keepOnlyCustomizableMenu() {
if let contextMenu = window?.contentView?.superview?.menu {
contextMenu.items.forEach({ (item) in
if let action = item.action,
NSStringFromSelector(action) != "runToolbarCustomizationPalette:" {
contextMenu.removeItem(item)
}
})
}
}
}
// MARK: Window Delegate
// A ton of thanks to genius #1024jp
extension MyWindowController: NSWindowDelegate {
// PROBLEM 2: Solution
func window(_ window: NSWindow, willPositionSheet sheet: NSWindow, using rect: NSRect) -> NSRect {
if sheet.className == "NSToolbarConfigPanel" {
removeSizeAndDisplayMode(in: sheet)
}
return rect
}
func removeSizeAndDisplayMode(in sheet: NSWindow) {
guard let views = sheet.contentView?.subviews else { return }
// Hide Small Size Option
views.lazy
.flatMap { $0 as? NSButton }
.filter { button -> Bool in
guard let buttonTypeValue = button.cell?.value(forKey: "buttonType") as? UInt,
let buttonType = NSButtonType(rawValue: buttonTypeValue)
else { return false }
return buttonType == .switch
}
.first?.isHidden = true
// Hide Display Mode Option
views.lazy
.filter { view -> Bool in
return view.subviews.count == 2
}
.first?.isHidden = true
sheet.contentView?.needsDisplay = true
}
}
// MARK: Toolbar Delegate
extension MyWindowController: NSToolbarDelegate {
func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [String] {
return [
NSToolbarFlexibleSpaceItemIdentifier,
NSToolbarSpaceItemIdentifier,
NSToolbarToggleSidebarItemIdentifier
]
}
func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [String] {
return [NSToolbarToggleSidebarItemIdentifier]
}
func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: String, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
return nil
}
}