mirror of
https://github.com/coteditor/CotEditor.git
synced 2024-09-11 11:25:57 +03:00
Subclass NSTextContainer for hanging indent
This commit is contained in:
parent
f00c3301d9
commit
965a4e0e20
@ -16,6 +16,7 @@ Change Log
|
||||
- Add feedback about search in VoiceOver.
|
||||
- Optimize the performance of finding the matching brace to highlight.
|
||||
- Optimize the performance of line number drawing.
|
||||
- Optimize the performance of hanging indent calculation.
|
||||
- Hide meanless items in the font panel toolbar.
|
||||
|
||||
|
||||
|
@ -708,6 +708,8 @@
|
||||
2AFB30E01E4B8F5B00BFAEF3 /* Debouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AFB30DE1E4B8F5B00BFAEF3 /* Debouncer.swift */; };
|
||||
2AFB5AE81D597ABB003895A7 /* DefaultSettings+Encodings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AFB5AE71D597ABB003895A7 /* DefaultSettings+Encodings.swift */; };
|
||||
2AFB5AE91D597ABB003895A7 /* DefaultSettings+Encodings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AFB5AE71D597ABB003895A7 /* DefaultSettings+Encodings.swift */; };
|
||||
2AFE848622AE71130001C4ED /* TextContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AFE848522AE71130001C4ED /* TextContainer.swift */; };
|
||||
2AFE848722AE71130001C4ED /* TextContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AFE848522AE71130001C4ED /* TextContainer.swift */; };
|
||||
2AFECF5A2171C0E60065A7DE /* Bundle+AppInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AFECF592171C0E60065A7DE /* Bundle+AppInfo.swift */; };
|
||||
2AFECF5B2171C0E60065A7DE /* Bundle+AppInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AFECF592171C0E60065A7DE /* Bundle+AppInfo.swift */; };
|
||||
6C6DAE3E13833C0E007F2326 /* dsa_pub.pem in Resources */ = {isa = PBXBuildFile; fileRef = 6C6DAE3D13833C0E007F2326 /* dsa_pub.pem */; };
|
||||
@ -1432,6 +1434,7 @@
|
||||
2AFAFD491D41487600F1458F /* PrintTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrintTextView.swift; sourceTree = "<group>"; };
|
||||
2AFB30DE1E4B8F5B00BFAEF3 /* Debouncer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Debouncer.swift; sourceTree = "<group>"; };
|
||||
2AFB5AE71D597ABB003895A7 /* DefaultSettings+Encodings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DefaultSettings+Encodings.swift"; sourceTree = "<group>"; };
|
||||
2AFE848522AE71130001C4ED /* TextContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextContainer.swift; sourceTree = "<group>"; };
|
||||
2AFECF592171C0E60065A7DE /* Bundle+AppInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+AppInfo.swift"; sourceTree = "<group>"; };
|
||||
4B7998191A1F1BCD0088D167 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
4B79981C1A1F1BCD0088D167 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = "zh-Hans"; path = "zh-Hans.lproj/ReportTemplate.md"; sourceTree = "<group>"; };
|
||||
@ -1963,6 +1966,7 @@
|
||||
2AA4F69F20A1C190003FD515 /* NSTextView+RoundedBackground.swift */,
|
||||
2ADD0AD7217A967200F78732 /* NSTextView+LineNumber.swift */,
|
||||
2A72DA0F209B778B005242B9 /* NSTextView+MultiCursor.swift */,
|
||||
2AFE848522AE71130001C4ED /* TextContainer.swift */,
|
||||
2A6FD9E61D394F5900A59784 /* LayoutManager.swift */,
|
||||
2A6FD9DC1D392EFF00A59784 /* ATSTypesetter.swift */,
|
||||
2A0BF8A71DD8E7F90088961B /* TextSizeTouchBar.swift */,
|
||||
@ -2955,6 +2959,7 @@
|
||||
2A1814B921CF8BD500602214 /* RegularExpressionFormatter.swift in Sources */,
|
||||
2AA4F6A120A1C190003FD515 /* NSTextView+RoundedBackground.swift in Sources */,
|
||||
2A0DD6341E655C4A001CAAA3 /* TokenTextView.swift in Sources */,
|
||||
2AFE848722AE71130001C4ED /* TextContainer.swift in Sources */,
|
||||
2A91C3221D1C40E4007CF8BE /* FileDropPaneController.swift in Sources */,
|
||||
2A4257BA1D2392A40086DAAD /* EditorTextView+Accessories.swift in Sources */,
|
||||
2A71BC7C1DDC50530085AE1C /* DocumentViewController+TouchBar.swift in Sources */,
|
||||
@ -3260,6 +3265,7 @@
|
||||
2AF0C12D1D3DABD000B6FCB6 /* Document+ScriptingSupport.swift in Sources */,
|
||||
2A1814B821CF8BD500602214 /* RegularExpressionFormatter.swift in Sources */,
|
||||
2ACDC0911D1726BD009B72D6 /* DotView.swift in Sources */,
|
||||
2AFE848622AE71130001C4ED /* TextContainer.swift in Sources */,
|
||||
2A1856051D47E7FF008FA79E /* NSTextView+TextReplacement.swift in Sources */,
|
||||
2AAB4BF91D2435AC0049A68B /* DocumentInspectorViewController.swift in Sources */,
|
||||
2A5DCE4F1D185F1B00D5D74C /* CharacterField.swift in Sources */,
|
||||
|
@ -121,6 +121,11 @@ final class EditorTextView: NSTextView, Themable, CurrentLineHighlighting, Multi
|
||||
self.scaleUnitSquare(to: self.convert(.unit, from: nil)) // reset scale
|
||||
|
||||
// setup layoutManager and textContainer
|
||||
let textContainer = TextContainer()
|
||||
textContainer.isHangingIndentEnabled = defaults[.enablesHangingIndent]
|
||||
textContainer.hangingIndentWidth = defaults[.hangingIndentWidth]
|
||||
self.replaceTextContainer(textContainer)
|
||||
|
||||
let layoutManager = LayoutManager()
|
||||
self.textContainer!.replaceLayoutManager(layoutManager)
|
||||
self.layoutManager?.allowsNonContiguousLayout = true
|
||||
@ -1120,13 +1125,9 @@ final class EditorTextView: NSTextView, Themable, CurrentLineHighlighting, Multi
|
||||
assert(Thread.isMainThread)
|
||||
|
||||
guard let textStorage = self.textStorage else { return assertionFailure() }
|
||||
guard textStorage.length > 0 else { return }
|
||||
|
||||
let range = textStorage.mutableString.range
|
||||
|
||||
guard !range.isEmpty else { return }
|
||||
|
||||
textStorage.addAttributes(self.typingAttributes, range: range)
|
||||
(self.layoutManager as? LayoutManager)?.invalidateIndent(in: range)
|
||||
textStorage.addAttributes(self.typingAttributes, range: NSRange(0..<textStorage.length))
|
||||
}
|
||||
|
||||
|
||||
@ -1615,19 +1616,10 @@ final class EditorTextView: NSTextView, Themable, CurrentLineHighlighting, Multi
|
||||
}
|
||||
|
||||
case .enablesHangingIndent:
|
||||
let wholeRange = self.string.nsRange
|
||||
if !(new as! Bool) {
|
||||
if let paragraphStyle = self.defaultParagraphStyle {
|
||||
self.textStorage?.addAttribute(.paragraphStyle, value: paragraphStyle, range: wholeRange)
|
||||
} else {
|
||||
self.textStorage?.removeAttribute(.paragraphStyle, range: wholeRange)
|
||||
}
|
||||
} else {
|
||||
(self.layoutManager as? LayoutManager)?.invalidateIndent(in: wholeRange)
|
||||
}
|
||||
(self.textContainer as? TextContainer)?.isHangingIndentEnabled = new as! Bool
|
||||
|
||||
case .hangingIndentWidth:
|
||||
(self.layoutManager as? LayoutManager)?.invalidateIndent(in: self.string.nsRange)
|
||||
(self.textContainer as? TextContainer)?.hangingIndentWidth = new as! Int
|
||||
|
||||
default:
|
||||
preconditionFailure()
|
||||
|
@ -275,18 +275,6 @@ final class LayoutManager: NSLayoutManager, ValidationIgnorable {
|
||||
}
|
||||
|
||||
|
||||
/// textStorage did update
|
||||
override func processEditing(for textStorage: NSTextStorage, edited editMask: NSTextStorageEditActions, range newCharRange: NSRange, changeInLength delta: Int, invalidatedRange invalidatedCharRange: NSRange) {
|
||||
|
||||
// invalidate wrapping line indent in editRange if needed
|
||||
if editMask.contains(.editedCharacters) {
|
||||
self.invalidateIndent(in: newCharRange)
|
||||
}
|
||||
|
||||
super.processEditing(for: textStorage, edited: editMask, range: newCharRange, changeInLength: delta, invalidatedRange: invalidatedCharRange)
|
||||
}
|
||||
|
||||
|
||||
/// fill background rectangles with a color
|
||||
override func fillBackgroundRectArray(_ rectArray: UnsafePointer<NSRect>, count rectCount: Int, forCharacterRange charRange: NSRange, color: NSColor) {
|
||||
|
||||
@ -327,76 +315,6 @@ final class LayoutManager: NSLayoutManager, ValidationIgnorable {
|
||||
}
|
||||
|
||||
|
||||
/// invalidate indent of wrapped lines
|
||||
func invalidateIndent(in range: NSRange) {
|
||||
|
||||
assert(Thread.isMainThread)
|
||||
|
||||
guard UserDefaults.standard[.enablesHangingIndent] else { return }
|
||||
|
||||
guard
|
||||
let textStorage = self.textStorage,
|
||||
let textView = self.firstTextView
|
||||
else { return assertionFailure() }
|
||||
|
||||
// only on focused editor
|
||||
if let window = textView.window, !self.layoutManagerOwnsFirstResponder(in: window) { return }
|
||||
|
||||
let string = textStorage.string as NSString
|
||||
let lineRange = string.lineRange(for: range)
|
||||
|
||||
guard !lineRange.isEmpty else { return }
|
||||
|
||||
let hangingIndent = self.spaceWidth * CGFloat(UserDefaults.standard[.hangingIndentWidth])
|
||||
let regex = try! NSRegularExpression(pattern: "^[ \\t]+(?!$)")
|
||||
|
||||
// get dummy attributes to make calculation of indent width the same as layoutManager's calculation (2016-04)
|
||||
let defaultParagraphStyle = textView.defaultParagraphStyle ?? .default
|
||||
let indentAttributes: [NSAttributedString.Key: Any] = {
|
||||
let typingParagraphStyle = (textView.typingAttributes[.paragraphStyle] as? NSParagraphStyle)?.mutable
|
||||
typingParagraphStyle?.headIndent = 1.0 // dummy indent value for size calculation (2016-04)
|
||||
|
||||
return [.font: self.textFont,
|
||||
.paragraphStyle: typingParagraphStyle,
|
||||
].compactMapValues { $0 }
|
||||
}()
|
||||
|
||||
var cache = [String: CGFloat]()
|
||||
|
||||
// process line by line
|
||||
textStorage.beginEditing()
|
||||
string.enumerateSubstrings(in: lineRange, options: .byLines) { (substring: String?, substringRange, enclosingRange, stop) in
|
||||
guard let substring = substring else { return }
|
||||
|
||||
var indent = hangingIndent
|
||||
|
||||
// add base indent
|
||||
let baseIndentRange = regex.rangeOfFirstMatch(in: substring, range: substring.nsRange)
|
||||
if baseIndentRange.location != NSNotFound {
|
||||
let indentString = (substring as NSString).substring(with: baseIndentRange)
|
||||
if let width = cache[indentString] {
|
||||
indent += width
|
||||
} else {
|
||||
let width = NSAttributedString(string: indentString, attributes: indentAttributes).size().width
|
||||
cache[indentString] = width
|
||||
indent += width
|
||||
}
|
||||
}
|
||||
|
||||
// apply new indent only if needed
|
||||
let paragraphStyle = textStorage.attribute(.paragraphStyle, at: substringRange.location, effectiveRange: nil) as? NSParagraphStyle
|
||||
if indent != paragraphStyle?.headIndent {
|
||||
let mutableParagraphStyle = (paragraphStyle ?? defaultParagraphStyle).mutable
|
||||
mutableParagraphStyle.headIndent = indent
|
||||
|
||||
textStorage.addAttribute(.paragraphStyle, value: mutableParagraphStyle, range: substringRange)
|
||||
}
|
||||
}
|
||||
|
||||
textStorage.endEditing()
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MARK: Private Methods
|
||||
|
||||
|
99
CotEditor/Sources/TextContainer.swift
Normal file
99
CotEditor/Sources/TextContainer.swift
Normal file
@ -0,0 +1,99 @@
|
||||
//
|
||||
// TextContainer.swift
|
||||
//
|
||||
// CotEditor
|
||||
// https://coteditor.com
|
||||
//
|
||||
// Created by 1024jp on 2019-06-10.
|
||||
//
|
||||
// ---------------------------------------------------------------------------
|
||||
//
|
||||
// © 2019 1024jp
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
final class TextContainer: NSTextContainer {
|
||||
|
||||
// MARK: Public Properties
|
||||
|
||||
var isHangingIndentEnabled = false { didSet { self.invalidateLayout() } }
|
||||
var hangingIndentWidth = 0 { didSet { self.invalidateLayout() } }
|
||||
|
||||
|
||||
|
||||
// MARK: -
|
||||
// MARK: Text Container Methods
|
||||
|
||||
override var isSimpleRectangularTextContainer: Bool {
|
||||
|
||||
return !self.isHangingIndentEnabled
|
||||
}
|
||||
|
||||
|
||||
override func lineFragmentRect(forProposedRect proposedRect: NSRect, at characterIndex: Int, writingDirection baseWritingDirection: NSWritingDirection, remaining remainingRect: UnsafeMutablePointer<NSRect>?) -> NSRect {
|
||||
|
||||
assert(self.hangingIndentWidth >= 0)
|
||||
|
||||
var rect = super.lineFragmentRect(forProposedRect: proposedRect, at: characterIndex, writingDirection: baseWritingDirection, remaining: remainingRect)
|
||||
|
||||
guard
|
||||
self.isHangingIndentEnabled,
|
||||
let layoutManager = self.layoutManager as? LayoutManager,
|
||||
let storage = layoutManager.textStorage
|
||||
else { return rect }
|
||||
|
||||
let string = storage.string as NSString
|
||||
let lineRange = string.lineRange(for: NSRange(characterIndex..<characterIndex))
|
||||
|
||||
// no hanging indent for new line
|
||||
guard lineRange.location < characterIndex else { return rect }
|
||||
|
||||
// get base indent
|
||||
let indentRange = string.range(of: "[ \t]+", options: [.regularExpression, .anchored], range: lineRange)
|
||||
let baseIndent = (indentRange == .notFound) ? 0 : storage.attributedSubstring(from: indentRange).size().width
|
||||
|
||||
// calculate hanging indent
|
||||
let hangingIndent = CGFloat(self.hangingIndentWidth) * layoutManager.spaceWidth
|
||||
let indent = baseIndent + hangingIndent
|
||||
|
||||
// remove hanging indent space from rect
|
||||
rect.size.width -= indent
|
||||
switch baseWritingDirection {
|
||||
case .leftToRight, .natural:
|
||||
rect.origin.x += indent
|
||||
case .rightToLeft:
|
||||
break
|
||||
@unknown default:
|
||||
assertionFailure()
|
||||
}
|
||||
|
||||
return rect
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MARK: Private Methods
|
||||
|
||||
/// invalidate layout in layoutManager
|
||||
private func invalidateLayout() {
|
||||
|
||||
guard let layoutManager = self.layoutManager else { return }
|
||||
|
||||
let range = NSRange(0..<layoutManager.attributedString().length)
|
||||
layoutManager.invalidateLayout(forCharacterRange: range, actualCharacterRange: nil)
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user