Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Page Up/Down Keys #38

Merged
merged 4 commits into from
Jun 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -189,9 +189,9 @@ public class TextLayoutManager: NSObject {
// MARK: - Layout

/// Lays out all visible lines
func layoutLines() { // swiftlint:disable:this function_body_length
func layoutLines(in rect: NSRect? = nil) { // swiftlint:disable:this function_body_length
guard layoutView?.superview != nil,
let visibleRect = delegate?.visibleRect,
let visibleRect = rect ?? delegate?.visibleRect,
!isInTransaction,
let textStorage else {
return
Expand Down Expand Up @@ -299,8 +299,8 @@ public class TextLayoutManager: NSObject {

var height: CGFloat = 0
var width: CGFloat = 0
var relativeMinY = max(layoutData.minY - position.yPos, 0)
var relativeMaxY = max(layoutData.maxY - position.yPos, relativeMinY)
let relativeMinY = max(layoutData.minY - position.yPos, 0)
let relativeMaxY = max(layoutData.maxY - position.yPos, relativeMinY)

for lineFragmentPosition in line.typesetter.lineFragments.linesStartingAt(
relativeMinY,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ package extension TextSelectionManager {
)
case .word:
return extendSelectionWord(string: string, from: offset, delta: delta)
case .line, .container:
case .line:
return extendSelectionLine(string: string, from: offset, delta: delta)
case .visualLine:
return extendSelectionVisualLine(string: string, from: offset, delta: delta)
Expand All @@ -46,6 +46,8 @@ package extension TextSelectionManager {
} else {
return NSRange(location: 0, length: offset)
}
case .page: // Not a valid destination horizontally.
return NSRange(location: offset, length: 0)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ package extension TextSelectionManager {
return extendSelectionVerticalCharacter(from: offset, up: up, suggestedXPos: suggestedXPos)
case .word, .line, .visualLine:
return extendSelectionVerticalLine(from: offset, up: up)
case .container:
return extendSelectionContainer(from: offset, delta: up ? 1 : -1)
case .page:
return extendSelectionPage(from: offset, delta: up ? 1 : -1, suggestedXPos: suggestedXPos)
case .document:
if up {
return NSRange(location: 0, length: offset)
Expand All @@ -61,7 +61,7 @@ package extension TextSelectionManager {
guard let point = layoutManager?.rectForOffset(offset)?.origin,
let newOffset = layoutManager?.textOffsetAtPoint(
CGPoint(
x: suggestedXPos == nil ? point.x : suggestedXPos!,
x: suggestedXPos ?? point.x,
y: point.y - (layoutManager?.estimateLineHeight() ?? 2.0)/2 * (up ? 1 : -3)
)
) else {
Expand Down Expand Up @@ -115,22 +115,36 @@ package extension TextSelectionManager {
}
}

/// Extends a selection one "container" long.
/// Extends a selection one "page" long.
/// - Parameters:
/// - offset: The location to start extending the selection from.
/// - delta: The direction the selection should be extended. `1` for forwards, `-1` for backwards.
/// - Returns: The range of the extended selection.
private func extendSelectionContainer(from offset: Int, delta: Int) -> NSRange {
guard let textView, let endOffset = layoutManager?.textOffsetAtPoint(
CGPoint(
x: delta > 0 ? textView.frame.maxX : textView.frame.minX,
y: delta > 0 ? textView.frame.maxY : textView.frame.minY
)
) else {
private func extendSelectionPage(from offset: Int, delta: Int, suggestedXPos: CGFloat?) -> NSRange {
guard let textView = textView,
let layoutManager,
let currentYPos = layoutManager.rectForOffset(offset)?.origin.y else {
return NSRange(location: offset, length: 0)
}
return endOffset > offset
? NSRange(location: offset, length: endOffset - offset)
: NSRange(location: endOffset, length: offset - endOffset)

let pageHeight = textView.visibleRect.height

// Grab the line where the next selection should be. Then use the suggestedXPos to find where in the line the
// selection should be extended to.
layoutManager.layoutLines(
in: NSRect(x: 0, y: currentYPos, width: layoutManager.maxLineWidth, height: pageHeight)
)
guard let nextPageOffset = layoutManager.textOffsetAtPoint(CGPoint(
x: suggestedXPos ?? 0,
y: min(textView.frame.height, max(0, currentYPos + (delta > 0 ? -pageHeight : pageHeight)))
)) else {
return NSRange(location: offset, length: 0)
}

if delta > 0 {
return NSRange(location: nextPageOffset, length: offset - nextPageOffset)
} else {
return NSRange(location: offset, length: nextPageOffset - offset)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,7 @@ public class TextSelectionManager: NSObject {
case word
case line
case visualLine
/// Eg: Bottom of screen
case container
case page
case document
}

Expand Down Expand Up @@ -323,10 +322,12 @@ public class TextSelectionManager: NSObject {

let fillRects = getFillRects(in: rect, for: textSelection)

let min = fillRects.min(by: { $0.origin.y < $1.origin.y })?.origin ?? .zero
let max = fillRects.max(by: { $0.origin.y < $1.origin.y }) ?? .zero
let size = CGSize(width: max.maxX - min.x, height: max.maxY - min.y)
textSelection.boundingRect = CGRect(origin: min, size: size)
let minX = fillRects.min(by: { $0.origin.x < $1.origin.x })?.origin.x ?? 0
let minY = fillRects.min(by: { $0.origin.y < $1.origin.y })?.origin.y ?? 0
let max = fillRects.max(by: { $0.maxY < $1.maxY }) ?? .zero
let origin = CGPoint(x: minX, y: minY)
let size = CGSize(width: max.maxX - minX, height: max.maxY - minY)
textSelection.boundingRect = CGRect(origin: origin, size: size)

context.fill(fillRects)
context.restoreGState()
Expand Down
57 changes: 57 additions & 0 deletions Sources/CodeEditTextView/TextView/TextView+FirstResponder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//
// TextView+FirstResponder.swift
// CodeEditTextView
//
// Created by Khan Winter on 6/15/24.
//

import AppKit

extension TextView {
open override func becomeFirstResponder() -> Bool {
isFirstResponder = true
selectionManager.cursorTimer.resetTimer()
needsDisplay = true
return super.becomeFirstResponder()
}

open override func resignFirstResponder() -> Bool {
isFirstResponder = false
selectionManager.removeCursors()
needsDisplay = true
return super.resignFirstResponder()
}

open override var canBecomeKeyView: Bool {
super.canBecomeKeyView && acceptsFirstResponder && !isHiddenOrHasHiddenAncestor
}

/// Sent to the window's first responder when `NSWindow.makeKey()` occurs.
@objc private func becomeKeyWindow() {
_ = becomeFirstResponder()
}

/// Sent to the window's first responder when `NSWindow.resignKey()` occurs.
@objc private func resignKeyWindow() {
_ = resignFirstResponder()
}

open override var needsPanelToBecomeKey: Bool {
isSelectable || isEditable
}

open override var acceptsFirstResponder: Bool {
isSelectable
}

open override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
return true
}

open override func resetCursorRects() {
super.resetCursorRects()
if isSelectable {
addCursorRect(visibleRect, cursor: .iBeam)
}
}
}
50 changes: 50 additions & 0 deletions Sources/CodeEditTextView/TextView/TextView+KeyDown.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//
// TextView+KeyDown.swift
// CodeEditTextView
//
// Created by Khan Winter on 6/15/24.
//

import AppKit
import Carbon.HIToolbox

extension TextView {
override public func keyDown(with event: NSEvent) {
guard isEditable else {
super.keyDown(with: event)
return
}

NSCursor.setHiddenUntilMouseMoves(true)

if !(inputContext?.handleEvent(event) ?? false) {
interpretKeyEvents([event])
} else {
// Not handled, ignore so we don't double trigger events.
return
}
}

override public func performKeyEquivalent(with event: NSEvent) -> Bool {
guard isEditable else {
return super.performKeyEquivalent(with: event)
}

switch Int(event.keyCode) {
case kVK_PageUp:
if !event.modifierFlags.contains(.shift) {
self.pageUp(event)
return true
}
case kVK_PageDown:
if !event.modifierFlags.contains(.shift) {
self.pageDown(event)
return true
}
default:
return false
}

return false
}
}
95 changes: 95 additions & 0 deletions Sources/CodeEditTextView/TextView/TextView+Layout.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
//
// TextView+Layout.swift
// CodeEditTextView
//
// Created by Khan Winter on 6/15/24.
//

import Foundation

extension TextView {
open override class var isCompatibleWithResponsiveScrolling: Bool {
true
}

open override func prepareContent(in rect: NSRect) {
needsLayout = true
super.prepareContent(in: rect)
}

override public func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
if isSelectable {
selectionManager.drawSelections(in: dirtyRect)
}
}

override open var isFlipped: Bool {
true
}

override public var visibleRect: NSRect {
if let scrollView {
var rect = scrollView.documentVisibleRect
rect.origin.y += scrollView.contentInsets.top
return rect.pixelAligned
} else {
return super.visibleRect
}
}

public var visibleTextRange: NSRange? {
let minY = max(visibleRect.minY, 0)
let maxY = min(visibleRect.maxY, layoutManager.estimatedHeight())
guard let minYLine = layoutManager.textLineForPosition(minY),
let maxYLine = layoutManager.textLineForPosition(maxY) else {
return nil
}
return NSRange(
location: minYLine.range.location,
length: (maxYLine.range.location - minYLine.range.location) + maxYLine.range.length
)
}

public func updatedViewport(_ newRect: CGRect) {
if !updateFrameIfNeeded() {
layoutManager.layoutLines()
}
inputContext?.invalidateCharacterCoordinates()
}

@discardableResult
public func updateFrameIfNeeded() -> Bool {
var availableSize = scrollView?.contentSize ?? .zero
availableSize.height -= (scrollView?.contentInsets.top ?? 0) + (scrollView?.contentInsets.bottom ?? 0)
let newHeight = max(layoutManager.estimatedHeight(), availableSize.height)
let newWidth = layoutManager.estimatedWidth()

var didUpdate = false

if newHeight >= availableSize.height && frame.size.height != newHeight {
frame.size.height = newHeight
// No need to update layout after height adjustment
}

if wrapLines && frame.size.width != availableSize.width {
frame.size.width = availableSize.width
didUpdate = true
} else if !wrapLines && frame.size.width != max(newWidth, availableSize.width) {
frame.size.width = max(newWidth, availableSize.width)
didUpdate = true
}

if didUpdate {
needsLayout = true
needsDisplay = true
layoutManager.layoutLines()
}

if isSelectable {
selectionManager?.updateSelectionViews()
}

return didUpdate
}
}
18 changes: 18 additions & 0 deletions Sources/CodeEditTextView/TextView/TextView+Move.swift
Original file line number Diff line number Diff line change
Expand Up @@ -158,4 +158,22 @@ extension TextView {
selectionManager.moveSelections(direction: .down, destination: .document, modifySelection: true)
updateAfterMove()
}

override public func pageUp(_ sender: Any?) {
enclosingScrollView?.pageUp(sender)
}

override public func pageUpAndModifySelection(_ sender: Any?) {
selectionManager.moveSelections(direction: .up, destination: .page, modifySelection: true)
updateAfterMove()
}

override public func pageDown(_ sender: Any?) {
enclosingScrollView?.pageDown(sender)
}

override public func pageDownAndModifySelection(_ sender: Any?) {
selectionManager.moveSelections(direction: .down, destination: .page, modifySelection: true)
updateAfterMove()
}
}
Loading
Loading