Skip to content

Instantly share code, notes, and snippets.

@jpmhouston
Last active April 7, 2024 14:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jpmhouston/c7452bfee90bd697bc2ebef05e0c9b8f to your computer and use it in GitHub Desktop.
Save jpmhouston/c7452bfee90bd697bc2ebef05e0c9b8f to your computer and use it in GitHub Desktop.
HyperlinkTextView
//
// 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