I'm a total beginner to OSX GUI programming, so please be gentle with me.
I'm trying some experiments with adding light GUI elements from appkit to a CLI, so I'm working on a very small program to take the contents of a PDF and save it to a text file.
Here's the code I have
import AppKit
import Foundation
import Quartz
func helperReadPDF(_ filename: String) -> String {
let pdata = try! NSData(contentsOfFile: filename) as Data
let pdf = PDFDocument(data: pdata)
return pdf!.string!
}
func selectFile() -> URL? {
let dialog = NSOpenPanel()
dialog.allowedFileTypes = ["pdf"]
guard dialog.runModal() == .OK else { return nil }
return dialog.url
}
func getSaveLocation() -> URL? {
let sa = NSSavePanel()
sa.nameFieldStringValue = "Untitled.txt"
sa.canCreateDirectories = true
sa.allowedFileTypes = ["txt"]
guard sa.runModal() == .OK else { return nil }
return sa.url
}
let file = selectFile()?.path ?? ""
print("where to save?")
let dest = getSaveLocation()!
try! helperReadPDF(file).write(to: dest, atomically: true, encoding: .utf8)
(I know, there are lots of unidiomatic things in here, like all the forced unwrapping and pointlessly converting URLs to paths. I have obscure reasons...)
So this code mostly works: when I run it from a terminal window with swift guitest.swift it'll pop up a file picker window, let me select a pdf file, and then pop up a save dialogue and let me choose the directory, and then save the extracted text from the pdf into that directory.
But it won't let me change the filename. I can highlight the "Untitled.txt" provided by default, I can even get a cursor into the field... but it doesn't respond to keyboard input.
In this previous SO, someone suggested adding a nameFieldStringValue to make it editable, but, as you can see from the above code, I did that, and it doesn't work.
I see from this very old SO that at least in Objective-C-land, you have to initiate some kind of application object in order to accept keyboard input. Is that true today in Swift-land as well?
(Even though for some weird reason you can accept mouse input without doing any of that?!) If so, how do I do that here?
Edit: I get from the comments to that last prior SO I linked that this is probably a terrible idea, and that if I want to learn Mac GUI programming I should do it the heavy way with XCode and storyboards and all the rest. But could you indulge my doing it the stupid way in an effort to try to learn one thing at a time? (I.e., learn the GUI APIs on offer without also trying to learn XCode and Apple's preferred style of architecture at the same time.)
Thanks!
(Swift 4.2 on latest version of OSX. Not using XCode at all.)
Setting the application's ActivationPolicy will make it work.
// Import statements... (import Quartz)
NSApplication.shared.setActivationPolicy(.accessory)
// Functions and so on... (func helper..)
Related
I had this all working in Swift 3 and earlier but with Swift 4 no matter what variation I use this code will instead output text as a URL. If I put in "This is my sample text" the output after pasting the clipboard will be "This%20is%20my%20sample%20text". I have tried KuTTypeFileURL but that doesn't appear to make any difference either. What am I missing here? I have seen posts and discussions about how Apple is changing Pboards and other issues with sandboxing but I can't seem to figure this out at all.
original code what was working in swift 3 and earlier
private func copyToClipBoard(textToCopy: String) {
let pasteBoard = NSPasteboard.general()
pasteBoard.clearContents()
pasteBoard.setString(textToCopy, forType: NSStringPboardType)
}
This gives an error of
'NSStringPboardType' is unavailable in Swift: use 'PasteboardType.string'
After searching online I came across these posts that describe the same issue and the workaround was to use the kuTTypeUrl as String
Found here stackoverflow.com/questions/44537356/… and here forums.developer.apple.com/thread/79144
When I try it this way it simply outputs as a URL when I just need a String.
#IBOutlet weak var nameTextField: NSTextField!
#IBAction func nameCopy(_ sender: Any) {
copyToClipBoard(textToCopy: nameTextField.stringValue)
}
let NSStringPboardType = NSPasteboard.PasteboardType(kUTTypeURL as String)
private func copyToClipBoard(textToCopy: String) {
let pasteBoard = NSPasteboard.general
pasteBoard.clearContents()
pasteBoard.setString(textToCopy, forType: NSStringPboardType)
}
You are pasting an URL because you created a PasteboardType kUTTypeURL.
The solution is much simpler, there is a predefined string type
private func copyToClipBoard(textToCopy: String) {
let pasteBoard = NSPasteboard.general
pasteBoard.clearContents()
pasteBoard.setString(textToCopy, forType: .string)
}
The note in the documentation
Apps that adopt App Sandbox cannot access files identified using the string pasteboard type. Instead, use an NSURL object, a bookmark, or a filename pasteboard type.
is related to files (aka string paths), not to regular strings
I just ran into a similar issue. My code looked like this:
NSPasteboard.general.setString("Hello World", forType: .string)
Unfortunately, this didn't work. But I figured there is a bug that if you don't store the NSPasteboard.general into a variable, the object created as part of the general computed property gets deinitialized before the setString change is propagated to the system.
So if you tried doing this in one line like me, just split it up to two instead, which worked for me:
let pasteboard = NSPasteboard.general
pasteboard.setString("Hello World", forType: .string)
I reported this bug via Feedback Assistant to Apple (FB9988062).
UPDATE:
Apple answered my bug report, stating that you need to call declareTypes before setting a value, like so:
NSPasteboard.general.declareTypes([.string], owner: nil)
I'm trying to copy multiple NSImages to the Pasteboard using Swift 4 - like this:
private func putItemsOnPasteboard (_ items: [Int]) {
if let images = mainImageController?.getNSImages(for: items) {
NSPasteboard.general.clearContents()
NSPasteboard.general.writeObjects(images)
if let c = NSPasteboard.general.pasteboardItems?.count {
print (c)
}
}
}
NSImage implements NSPasteboardWriting, and it works fine. The snag is, if I then paste into, say, Mail, it only pastes the first image, and I'm struggling to figure out why.
For instance, if I call the function with three images, it all appears to work, and my diagnostic 'print (c)' correctly shows '3'. But if I paste into Mail (or OneNote etc.) it only paste the first image.
I know Mail supports pasting multiple images, because I can select three thumbnails in Apple Photos, copy them to the pasteboard, and it correctly pastes all three into Mail.
Any clues would be gratefully appreciated!
If I copy a group of photos in Photos, and then inspect the pasteboard types, I get:
import Cocoa
print(NSPasteboard.general.types?.map { $0.rawValue } ?? [])
outputs:
["dyn.ah62d4rv4gu8ywyc2nbu1g7dfqm10c6xekr1067dwr70g23pw", "IPXPasteboardController", "com.apple.PhotoPrintProduct.photoUUID", "public.file-url", "CorePasteboardFlavorType 0x6675726C", "dyn.ah62d4rv4gu8y6y4grf0gn5xbrzw1gydcr7u1e3cytf2gn", "NSFilenamesPboardType", "dyn.ah62d4rv4gu8yc6durvwwaznwmuuha2pxsvw0e55bsmwca7d3sbwu", "Apple URL pasteboard type"]
Of those types, the one that looks interesting to me is public.file-url, which suggests that Photos is copying a group of URLs onto the pasteboard. Let's test that hypothesis:
import Cocoa
print(NSPasteboard.general.readObjects(forClasses: [NSURL.self], options: nil) ?? [])
outputs:
[file:///Users/*deleted*/Pictures/Photos%20Library.photoslibrary/resources/proxies/derivatives/1e/00/1e03/UNADJUSTEDNONRAW_thumb_1e03.jpg, file:///Users/*deleted*/Pictures/Photos%20Library.photoslibrary/resources/proxies/derivatives/1e/00/1e04/UNADJUSTEDNONRAW_thumb_1e04.jpg, file:///Users/*deleted*/Pictures/Photos%20Library.photoslibrary/resources/proxies/derivatives/1e/00/1e05/kOBCUhzGRcyeVfBCC8VfvQ_thumb_1e05.jpg]
The output is a list of URLs to JPEG files, suggesting that Photos is indeed providing a list of URLs and putting those on the pasteboard. So perhaps you could save the TIFF representations of your NSImages to a temporary location, and then write the URLs to those temporary files onto the pasteboard, and it would work.
Charles Srstka's answer was very helpful here. It appears that though you can put multiple NSImages on the Pasteboard, you can only paste one.
Swift's URL object doesn't comply with NSPasteboardWriting, but NSURL does, and it doesn't have the restriction of being only able to paste one.
So this worked
private func putImagesOnPasteboard (_ images: [Image], folderURL: URL) {
let imageURLs = images.map() {
image in
return NSURL (fileURLWithPath: folderURL.appendingPathComponent(image.fileName).path)
}
NSPasteboard.general.clearContents()
NSPasteboard.general.writeObjects(imageURLs)
}
I’m writing a plugin to Xcode 7. I have the DVTSourceTextView and can manipulate it just fine. One of the things I want to find is which file is related to this. Unfortunately, DVTSourceTextView doesn’t appear to offer that information - or if it does, it is buried in a way I fail to see.
I’m sure it is rather trivial, I’m just missing something.
Okay, this was easier than I thought it was. I was approaching it from a different (although almost correct) way.
class func currentEditorView() -> (NSURL?, NSView?) {
let currentWindowController = NSApp.keyWindow?.windowController
guard currentWindowController!.className == "IDEWorkspaceWindowController" else { return (nil, nil) }
let filename = currentWindowController!.valueForKey("editorArea")!.valueForKey("lastActiveEditorContext")!.valueForKey("originalRequestedDocumentURL")
let editor = currentWindowController!.valueForKey("editorArea")!.valueForKey("lastActiveEditorContext")!.valueForKey("editor")!.valueForKey("textView")
return (filename as? NSURL, editor as? NSView)
}
This gives me both the filename as an NSURL as well as the DVTSourceTextView as an NSView without the need of including private headers. Spiffy.
Now not only do I know the name of the file I’m editing, but I can also determine if it is a swift, objc, c or c++ file! THAT is coolness!
Trying to follow along and code the Smashtag project while watching the Lecture 10 iTunes video.
When I add the dowloaded Twitter package to my Smashtag project, XCode couldn't find the Tweet class when I made reference to it in the TweetTableViewController.
Because of the problem described above, I added the four classes belonging to the Twitter package individually to the project. XCode found the four classes but adding them in this manner generated 11 compile errors.
I'm using XCode Version 6.3 (6D570) which is subsequent to the iOS 8.3 release.
Has anyone else encountered this issue?
Thank you for reading my question.
~ Lee
Possibly not the most-correct (read: best practice) way to do this, but I'm going to chalk it up to doing what it takes to finish the course.
I just went through the list of compile errors and changed the relevant properties to var instead of let. Constants can't be changed and in the new version of Swift they can only be instantiated once. So for the sake of not rewriting too much code, I chose to make certain properties vars instead of lets.
Other bugs I found following the iTunes U course:
The named ‘handler:’ argument needs the name explicitly in a few places.
The simulator will show "TwitterRequest: Couldn\'t discover Twitter account type.” until you go to Settings (inside the simulator) and set the Twitter account. At this point I had to reboot the device, as the call is made in the ViewDidLoad, and thus is only called the first time the view loads. (Alternatively, you could close out the app from the app switcher in the simulator and relaunch that way.)
Here is a gist with corrected code that you can use as a Twitter package that will work with the course and has fixes for the aforementioned bugs, minus the Twitter account setting:
https://gist.github.com/mattpetters/ccf87678ccce0c354398
As Christian R. Jimenez said, "I went to Settings in the Simulated iphone and add my Twitter Account. And everything works perfect." in http://cs193p.m2m.at/cs193p-lecture-10-table-view-winter-2015/. I just added my Twitter Account and tested it, it works!
I had similar problems with the Twitter packages using Swift 2.0 and Xcode 7.2
I'm very new to Swift, so there is a good chance the changes I made are not best practices, but the updated files do work: https://gist.github.com/awaxman11/9c48c0b4c622bffb879f.
For the most part I used Xcode's suggested changes. The two larger changes I made were:
In Tweet.swift I updated the the IndexedKeyword struct's init method to use advanceBy() instead of advance()
In TwitterRequest.swift I updated the signature of NSJSONSerialization to conform to the new error handling system
I've just had a big session fixing the Twitter package files for this same version of Xcode.
It seems that what has broken is that in this version of Swift, constants ('let x...') may only be initialized once, so if a constant is given a value in the declaration ('let x = false'), it may not be changed in the init() function. The Twitter package gives some constants initial values, but then changes the values in the init() function.
My solution to this was to follow the styles suggested in the current version of the Apple Swift language book: declare (many of) the constants as implicitly unwrapped optionals, unconditionally assign a value to them in the init() function (which value may be nil), then test whether any of them are nil, and, if so, return nil from init().
See https://developer.apple.com/library/mac/documentation/Swift/Conceptual/Swift_Programming_Language/Initialization.html, click "On This Page" and choose "Failable Initializers"
Also, in TwitterRequest.swift, I needed to add the parameter name 'handler:' in a couple of calls to performTwitterRequest(request, handler: handler).
As an example of constant initialization, in MediaItem.swift:
<< Original Code >>
...
public let aspectRatio: Double = 0
...
init?(data: NSDictionary?) {
var valid = false
if let urlString = data?.valueForKeyPath(TwitterKey.MediaURL) as? NSString {
if let url = NSURL(string: urlString) {
self.url = url
let h = data?.valueForKeyPath(TwitterKey.Height) as? NSNumber
let w = data?.valueForKeyPath(TwitterKey.Width) as? NSNumber
if h != nil && w != nil && h?.doubleValue != 0 {
aspectRatio = w!.doubleValue / h!.doubleValue
valid = true
}
}
}
if !valid {
return nil
}
}
...
<< Updated code >>
...
public let aspectRatio: Double
...
init?(data: NSDictionary?) {
if let urlString = data?.valueForKeyPath(TwitterKey.MediaURL) as? NSString {
if let url = NSURL(string: urlString as String) {
self.url = url
let h = data?.valueForKeyPath(TwitterKey.Height) as? NSNumber
let w = data?.valueForKeyPath(TwitterKey.Width) as? NSNumber
if h != nil && w != nil && h?.doubleValue != 0 {
aspectRatio = w!.doubleValue / h!.doubleValue
return
}
}
}
return nil
}
...
I will soon be working on an application which needs to get the currently selected text in the frontmost application window, be it Safari, Pages, TextEdit, Word, etc., and do something with that text.
My goal is to find a solution that works with as much applications as possible. So far I thought about using AppleScript, but that would limit the amount of applications which could be used with my service. At least these common applications must be supported: Safari, Firefox (no AppleScript?), Word, Pages, Excel, TextEdit, ...
I also thought about keeping the clipboard's content in a temporary variable then simulating a text copy operation (Cmd-C), getting the text and then put the original content back in. This would probably highlight the Edit menu item when the copy operation is simulated and seems a bit hacky to me. IMO this solution doesn't seem good enough for a commercial product.
I am also looking to get more than the selection (i.e: the complete contents of the page in Safari or Word, etc.) to add some additional features in the future.
Any ideas/details on how to implement this behavior?
Thanks in advance for any hints!
N.B: I need to support at least 10.4 and up, but ideally older than 10.4 too.
UPDATE:
The solution I've opted for: Using the "Chain of Responsibility" design pattern (GOF) to combine 3 different input methods (Pasteboard, AppleScript and Accessibility), using the best available input source automatically.
Note that when using NSAppleScript's executeAndReturnError: method which returns an NSAppleEventDescriptor (let's say a "descriptor" instance), for the [descriptor stringValue] method to return something, in your AppleScript you must use "return someString" OUTSIDE of a "tell" block else nothing will be returned.
Here's the Swift 5.5 implementation of what is described in the accepted answer.
extension AXUIElement {
static var focusedElement: AXUIElement? {
systemWide.element(for: kAXFocusedUIElementAttribute)
}
var selectedText: String? {
rawValue(for: kAXSelectedTextAttribute) as? String
}
private static var systemWide = AXUIElementCreateSystemWide()
private func element(for attribute: String) -> AXUIElement? {
guard let rawValue = rawValue(for: attribute), CFGetTypeID(rawValue) == AXUIElementGetTypeID() else { return nil }
return (rawValue as! AXUIElement)
}
private func rawValue(for attribute: String) -> AnyObject? {
var rawValue: AnyObject?
let error = AXUIElementCopyAttributeValue(self, attribute as CFString, &rawValue)
return error == .success ? rawValue : nil
}
}
Now, wherever you need to get the selected text from the frontmost application, you can just use AXUIElement.focusedElement?.selectedText.
As mentioned in the answer, this is not 100% reliable. So we're also implementing the other answer which simulates Command + C and copies from the clipboard. Also, ensure to remove the new item from the Clipboard if not required.
If you don't need selected text very frequently, you can programmatically press Command+C, then get the selected text from clipboard. But during my test, this is only works if you turn off App Sandbox (can't submit to Mac App Store).
Here is the Swift 3 code:
func performGlobalCopyShortcut() {
func keyEvents(forPressAndReleaseVirtualKey virtualKey: Int) -> [CGEvent] {
let eventSource = CGEventSource(stateID: .hidSystemState)
return [
CGEvent(keyboardEventSource: eventSource, virtualKey: CGKeyCode(virtualKey), keyDown: true)!,
CGEvent(keyboardEventSource: eventSource, virtualKey: CGKeyCode(virtualKey), keyDown: false)!,
]
}
let tapLocation = CGEventTapLocation.cghidEventTap
let events = keyEvents(forPressAndReleaseVirtualKey: kVK_ANSI_C)
events.forEach {
$0.flags = .maskCommand
$0.post(tap: tapLocation)
}
}
performGlobalCopyShortcut()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { // wait 0.05s for copy.
let clipboardText = NSPasteboard.general().readObjects(forClasses: [NSString.self], options: nil)?.first as? String ?? ""
print(clipboardText)
}
Accessibility will work, but only if access for assistive devices is on.
You'll need to get the current application, then get its focused UI element, then get its selected text ranges and its value (whole text) and selected text ranges. You could just get its selected text, but that would either concatenate or ignore multiple selections.
Be prepared for any of those steps to fail: The app may not have any windows up, there may be no UI element with focus, the focused UI element may have no text, and the focused UI element may have only an empty selected text range.