Last active
April 7, 2024 14:43
-
-
Save jpmhouston/c7452bfee90bd697bc2ebef05e0c9b8f to your computer and use it in GitHub Desktop.
HyperlinkTextView
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// BareTextView.swift | |
// Cleepp | |
// | |
// Created by Pierre Houston on 2024-03-27. | |
// Copyright © 2024 Bananameter Labs. All rights reserved. | |
// | |
// Based on snippits from https://stackoverflow.com/a/56854375/592739 | |
// and https://stackoverflow.com/a/14469815/592739 | |
// and https://gist.github.com/mminer/597c1b2c40adcf3c319f7feeade62ed4 | |
// | |
// I can't remember the state of this, whether it was working or not | |
// before I implemented HyperlinkTextField instead. I think it was | |
// working but once I saw that addHyperlinkCursorRects for the hand | |
// cursor would work for a NSTextField subclass too, I saw no need to | |
// use a NSTextView subclass for what I needed. | |
// | |
import AppKit | |
class HyperlinkTextView: NSTextView | |
{ | |
var maximumLines = 1 | |
var maximumWidth: CGFloat = 0.0 | |
init() { | |
super.init(frame: NSRect.zero) | |
configure() | |
} | |
// It is not possible to set up a lone NSTextView in Interface Builder, however you can set it up | |
// as a CustomView if you are happy to have all your presentation properties initialised | |
// programatically. This initialises an NSTextView as it would be with the default init... | |
required init(coder: NSCoder) { | |
super.init(coder: coder)! | |
let textStorage = NSTextStorage() | |
let layoutManager = NSLayoutManager() | |
textStorage.addLayoutManager(layoutManager) | |
// By default, NSTextContainers do not track the bounds of the NSTextview | |
let textContainer = NSTextContainer(containerSize: CGSize.zero) | |
textContainer.widthTracksTextView = true | |
textContainer.heightTracksTextView = true | |
layoutManager.addTextContainer(textContainer) | |
replaceTextContainer(textContainer) | |
configure() | |
} | |
private func configure() { | |
isEditable = false | |
isSelectable = false | |
backgroundColor = NSColor.clear | |
} | |
override var intrinsicContentSize: NSSize { | |
if let textContainer = textContainer, let layoutManager = layoutManager { | |
textContainer.maximumNumberOfLines = maximumLines | |
textContainer.size = CGSize(width: maximumWidth, height: 0.0) | |
layoutManager.ensureLayout(for: textContainer) | |
let rect = layoutManager.usedRect(for: textContainer) | |
return rect.size | |
} | |
return NSSize.zero | |
} | |
override func mouseDown(with event: NSEvent) { | |
super.mouseDown(with: event) | |
openClickedHyperlink(with: event) | |
} | |
override func resetCursorRects() { | |
super.resetCursorRects() | |
addHyperlinkCursorRects() | |
} | |
// Displays a hand cursor when a link is hovered over. | |
private func addHyperlinkCursorRects() { | |
guard let layoutManager = layoutManager, let textContainer = textContainer else { | |
return | |
} | |
let attributedStringValue = attributedString() | |
let range = NSRange(location: 0, length: attributedStringValue.length) | |
attributedStringValue.enumerateAttribute(.link, in: range) { value, range, _ in | |
guard value != nil else { | |
return | |
} | |
let rect = layoutManager.boundingRect(forGlyphRange: range, in: textContainer) | |
addCursorRect(rect, cursor: .pointingHand) | |
} | |
} | |
// Opens links when clicked. | |
private func openClickedHyperlink(with event: NSEvent) { | |
let attributedStringValue = attributedString() | |
let point = convert(event.locationInWindow, from: nil) | |
let characterIndex = characterIndexForInsertion(at: point) | |
guard characterIndex < attributedStringValue.length else { | |
return | |
} | |
let attributes = attributedStringValue.attributes(at: characterIndex, effectiveRange: nil) | |
var url = attributes[.link] as? URL | |
// example code got this attribute as a string, maybe on certain versions of this OS? support this possibility | |
if url == nil, let urlString = attributes[.link] as? String { | |
url = URL(string: urlString) | |
} | |
guard let url = url else { | |
return | |
} | |
NSWorkspace.shared.open(url) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment