How do I get the bounds of selected text as a CGRect? - swift

I am trying to include a highlighting function within a pdfViewer. However, in order to add the highlight annotation, I need the bounds for the selected text as a CGRect. Is there any way I can obtain this?
let annotation = PDFAnnotation(bounds: bounds, forType: .highlight, withProperties: nil)

Get an array of selections where each selection corresponds to a single line of the selected text:
guard let selections = pdfView.currentSelection?.selectionsByLine()
else { return }
Loop over the selections line by line, then loop over the pages encompassed by each selection, Then create a new highlight annotation with the selection's bounds and add it to the page
selections.forEach({ selection in
selection.pages.forEach({ page in
let highlight = PDFAnnotation(bounds: selection.bounds(for: page), forType: .highlight, withProperties: nil)
highlight.color = .yellow
page.addAnnotation(highlight)
})
})

Related

Not getting selection from PDFView

I see somewhere this code for highlight selected text in pdf document:
let select = pdfView.currentSelection?.selectionsByLine()
//assuming for single-page pdf.
guard let page = select?.first?.pages.first else { return }
select?.forEach({ selection in
let highlight = PDFAnnotation(bounds: select.bounds(for: page), forType: .highlight, withProperties: nil)
highlight.endLineStyle = .square
highlight.color = UIColor.orange.withAlphaComponent(0.5)
page.addAnnotation(highlight)
})
for me ,
let select = pdfView.currentSelection?.selectionsByLine()
is always giving nil. where to put this code. I putting it in Gesture Recogniser Delegate methods.
Also it seems adding multiple annotation on page. Do we get single highlight annotation per selection on a page?

PDFAnnotation Radiobutton don't change background color or don't display value

I use PDFKit in iOS as PDF renderer and I have problem with background color in radio button.
I iterate all PDF annotations on all pages in the document.
override func viewDidAppear(_ animated: Bool) {
//let pdfData = ... received data from file storage ...
if let document = PDFDocument(data: pdfData) {
for i in 0...document.pageCount {
if let page = document.page(at: i) {
for annot in page.annotations {
if annot.widgetControlType == .radioButtonControl {
annot.backgroundColor = UIColor.red
}
}
}
}
}
}
When I set the background color, nothing happens.
But when I try to set the background color to the type of Text annotation, the color changes.
The difference between the Radio button and the Text annotation is in its type.
The Radio button has a widgetFieldType == .button and the Text has a widgetFieldType == .text.
I think this is the reason why it doesn't work.
Next I try to remove annotation, change his background color and add again. This also doesn't work.
if annot.widgetControlType == .radioButtonControl {
page.removeAnnotation(annot)
annot.backgroundColor = UIColor.red
page.addAnnotation(annot)
}
But when I create a new instance of PDFAnnotaion, and I add it to the page, it works.
if annot.widgetControlType == .radioButtonControl {
page.removeAnnotation(annot)
let newAnnot = PDFAnnotation(bounds: annot.bounds, forType: .widget, withProperties: nil)
newAnnot.backgroundColor = UIColor.red
newAnnot.widgetStringValue = annot.widgetStringValue
page.addAnnotation(newAnnot)
}
The big problem is that the value of this Radio button is not displayed, even though it contains the value itself.
If in another method I get all the values from PDF annotations, there is also the right value from this radio button.
The Radio button contains the value, only it is displayed.
I tried to copy all the properties PDF annotation from the annot to newAnnot, but it didn't help.
How to properly change the background color of the Radio button and display its value?

NSLayoutManager returns incorrect glyphIndex for mousePosition in NSTextView

I have an NSTextView subclass as the right item in an NSSplitViewController, the left panel is an NSOutlineView. To process mouse clicks while the command key is pressed in the text view, I added the following to find the glyph that is under the mouse:
override func mouseDown(with event: NSEvent) {
guard
let lm = self.layoutManager,
let tc = self.textContainer
else { return }
let localMousePosition = convert(event.locationInWindow, to: nil)
var partial = CGFloat(1.0)
let glyphIndex = lm.glyphIndex(for: localMousePosition, in: tc, fractionOfDistanceThroughGlyph: &partial)
print(glyphIndex)
}
However, this results in an index that is about 10 or so too high, thus selecting the wrong glyph. My text view has only monospaced characters, so the offset is not caused by additional glyphs.
Interestingly, if I collapse the left panel (in code or in the storyboard), I get the correct index. But the offset in the x direction is more than the width of the left panel.
What am I missing here, is there an error in the code above?
Based on the comment by #Willeke above, I made the following change to my code:
localMousePosition = convert(event.locationInWindow, from: nil)
if enclosingScrollView?.frame.width == window?.frame.width {
// left panel is collapsed, so use convert: to:
localMousePosition = convert(event.locationInWindow, to: nil)
}
Seems to work.
The problem was that I was using localMousePosition = convert(event.locationInWindow, to: nil) in mouseMoved as well (localMousePosition is a property of the view). Changing that to localMousePosition = convert(event.locationInWindow, from: nil) made it all work. Thanks again to #Willeke for pointing this out.

How to change text color of NSTextView

How can I change the color of all text in a NSTextView? In the example below, myTextView.textColor = .white only changes color of Hello but not World. I don't want to specify the color every time when I append some text.
Also I'm not sure if this is an appropriate way appending text to NSTextView.
override func viewDidLoad() {
super.viewDidLoad()
myTextView.string = "Hello"
myTextView.backgroundColor = .black
myTextView.textColor = .white
logTextView.textStorage?.append(NSAttributedString(string: "World"))
}
NSTextStorage is a subclass of NSMutableAttributedString so you can manipulate it as a mutable attributed string.
If you want the new text to carry on the attributes at the end of the current text, append to the mutable string:
myTextView.textStorage?.mutableString.append("World")
If you want to add more attributes to the new text (for example, adding an underline), get the attributes at the end of the current text and manipulate the attributes dictionary:
guard let textStorage = myTextView.textStorage else {
return
}
var attributes = textStorage.attributes(at: textStorage.length - 1, effectiveRange: nil)
attributes[.underlineStyle] = NSNumber(value: NSUnderlineStyle.styleSingle.rawValue)
textStorage.append(NSAttributedString(string: "World", attributes: attributes))
After this, if you call mutableString.append, the new text will be in white and underlined.

Color attribute is ignored in NSAttributedString with NSLinkAttributeName

In an NSAttributedString, a range of letters has a link attribute and a custom color attribute.
In Xcode 7 with Swift 2, it works:
In Xcode 8 with Swift 3, the custom attributed color for the link is always ignored (it should be orange in the screenshot).
Here's the code for testing.
Swift 2, Xcode 7:
import Cocoa
import XCPlayground
let text = "Hey #user!"
let attr = NSMutableAttributedString(string: text)
let range = NSRange(location: 4, length: 5)
attr.addAttribute(NSForegroundColorAttributeName, value: NSColor.orangeColor(), range: range)
attr.addAttribute(NSLinkAttributeName, value: "http://somesite.com/", range: range)
let tf = NSTextField(frame: NSRect(x: 0, y: 0, width: 200, height: 50))
tf.allowsEditingTextAttributes = true
tf.selectable = true
tf.stringValue = text
tf.attributedStringValue = attr
XCPlaygroundPage.currentPage.liveView = tf
Swift 3, Xcode 8:
import Cocoa
import PlaygroundSupport
let text = "Hey #user!"
let attr = NSMutableAttributedString(string: text)
let range = NSRange(location: 4, length: 5)
attr.addAttribute(NSForegroundColorAttributeName, value: NSColor.orange, range: range)
attr.addAttribute(NSLinkAttributeName, value: "http://somesite.com/", range: range)
let tf = NSTextField(frame: NSRect(x: 0, y: 0, width: 200, height: 50))
tf.allowsEditingTextAttributes = true
tf.isSelectable = true
tf.stringValue = text
tf.attributedStringValue = attr
PlaygroundPage.current.liveView = tf
I've sent a bug report to Apple, but in the meantime if someone has an idea for a fix or workaround in Xcode 8, that would be great.
Apple Developer has answered:
Please know that our engineering team has determined that this issue behaves as intended based on the information provided.
And they explain why it worked before but doesn't anymore:
Unfortunately, the previous behavior (attributed string ranges with NSLinkAttributeName rendering in a custom color) was not explicitly supported. It happened to work because NSTextField was only rendering the link when the field editor was present; without the field editor, we fall back to the color specified by NSForegroundColorAttributeName.
Version 10.12 updated NSLayoutManager and NSTextField to render links using the default link appearance, similar to iOS. (see AppKit release notes for 10.12.)
To promote consistency, the intended behavior is for ranges that represent links (specified via NSLinkAttributeName) to be drawn using the default link appearance. So the current behavior is the expected behavior.
(emphasis mine)
This answer is not a fix for the issue of NSLinkAttributeName ignoring custom colors, it's an alternative solution for having colored clickable words in NSAttributedString.
With this workaround we don't use NSLinkAttributeName at all, since it forces a style we don't want.
Instead, we use custom attributes, and we subclass the NSTextField/NSTextView to detect the attributes under the mouse click and act accordingly.
There's several constraints, obviously: you have to be able to subclass the field/view, to override mouseDown, etc, but "it works for me" while waiting for a fix.
When preparing your NSMutableAttributedString, where you would have set an NSLinkAttributeName, set the link as an attribute with a custom key instead:
theAttributedString.addAttribute("CUSTOM", value: theLink, range: theLinkRange)
theAttributedString.addAttribute(NSForegroundColorAttributeName, value: NSColor.orange, range: theLinkRange)
theAttributedString.addAttribute(NSCursorAttributeName, value: NSCursor.arrow(), range: theLinkRange)
The color and content for the link is set. Now we have to make it clickable.
For this, subclass your NSTextView and override mouseDown(with event: NSEvent).
We will get the location of the mouse event in the window, find the character index in the text view at that location, and ask for the attributes of the character at this index in the text view's attributed string.
class MyTextView: NSTextView {
override func mouseDown(with event: NSEvent) {
// the location of the click event in the window
let point = self.convert(event.locationInWindow, from: nil)
// the index of the character in the view at this location
let charIndex = self.characterIndexForInsertion(at: point)
// if we are not outside the string...
if charIndex < super.attributedString().length {
// ask for the attributes of the character at this location
let attributes = super.attributedString().attributes(at: charIndex, effectiveRange: nil)
// if the attributes contain our key, we have our link
if let link = attributes["CUSTOM"] as? String {
// open the link, or send it via delegate/notification
}
}
// cascade the event to super (optional)
super.mouseDown(with: event)
}
}
That's it.
In my case I needed to customize different words with different colors and link types, so instead of passing just the link as a string I pass a struct containing the link and additional meta information, but the idea is the same.
If you have to use an NSTextField instead of an NSTextView, it's a bit trickier to find the click event location. A solution is to create an NSTextView inside the NSTextField and from there use the same technique as before.
class MyTextField: NSTextField {
var referenceView: NSTextView {
let theRect = self.cell!.titleRect(forBounds: self.bounds)
let tv = NSTextView(frame: theRect)
tv.textStorage!.setAttributedString(self.attributedStringValue)
return tv
}
override func mouseDown(with event: NSEvent) {
let point = self.convert(event.locationInWindow, from: nil)
let charIndex = referenceView.textContainer!.textView!.characterIndexForInsertion(at: point)
if charIndex < self.attributedStringValue.length {
let attributes = self.attributedStringValue.attributes(at: charIndex, effectiveRange: nil)
if let link = attributes["CUSTOM"] as? String {
// ...
}
}
super.mouseDown(with: event)
}
}