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

Refreshing Tags Editor: Mark 3 #558

Merged
merged 17 commits into from Jun 19, 2020
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -1,6 +1,7 @@
1.12.0
------
- Refreshed the Tags Editor Interface!
- New Scroll Support in the Tags Editor area

1.11.0
------
@@ -156,6 +156,8 @@
B529F7B924586DF800B168F1 /* MarkdownViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = B529F7B724586DF800B168F1 /* MarkdownViewController.xib */; };
B52F8FDA24644DD00062B8ED /* NSFont+Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = B549B084245A2E7500FB95B9 /* NSFont+Theme.swift */; };
B532F8A820C71C1000EA3506 /* WPAuthHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 37D4DD6820B3574C00C225EA /* WPAuthHandler.m */; };
B535014B249D5908003837C8 /* HorizontalScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B535014A249D5908003837C8 /* HorizontalScrollView.swift */; };
B535014C249D5908003837C8 /* HorizontalScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B535014A249D5908003837C8 /* HorizontalScrollView.swift */; };
B53FF5452476EFFE0014E928 /* NSStringCondensingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B53FF5442476EFFE0014E928 /* NSStringCondensingTests.swift */; };
B53FF5472476F9450014E928 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = B53FF5462476F9450014E928 /* Constants.swift */; };
B542DAE824801BFF00FA6F99 /* ClipView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B542DAE724801BFF00FA6F99 /* ClipView.swift */; };
@@ -481,6 +483,7 @@
B5283BCF23B679C00085826F /* NSMutableAttributedString+Simplenote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSMutableAttributedString+Simplenote.swift"; sourceTree = "<group>"; };
B529F7B424586D8700B168F1 /* MarkdownViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownViewController.swift; sourceTree = "<group>"; };
B529F7B724586DF800B168F1 /* MarkdownViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MarkdownViewController.xib; sourceTree = "<group>"; };
B535014A249D5908003837C8 /* HorizontalScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HorizontalScrollView.swift; sourceTree = "<group>"; };
B53FF5442476EFFE0014E928 /* NSStringCondensingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSStringCondensingTests.swift; sourceTree = "<group>"; };
B53FF5462476F9450014E928 /* Constants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
B542DAE724801BFF00FA6F99 /* ClipView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipView.swift; sourceTree = "<group>"; };
@@ -872,6 +875,7 @@
B5ACE42D24785D8C00AB02C7 /* BackgroundView.swift */,
B542DAE724801BFF00FA6F99 /* ClipView.swift */,
B5F50914247319360042FA1D /* SplitView.swift */,
B535014A249D5908003837C8 /* HorizontalScrollView.swift */,
46F0E66117A32269005BB4D1 /* SPTableView.h */,
46F0E66217A32269005BB4D1 /* SPTableView.m */,
46EEA3C517B2FEA500914D1A /* SPTextView.h */,
@@ -1544,6 +1548,7 @@
B5985AD5242950B40044EDE9 /* NSColor+Simplenote.swift in Sources */,
B5C7DD3D243E1F1900BEE354 /* VersionsViewController.swift in Sources */,
B57CB87B244DEC4B00BA7969 /* MigrationsHandler.swift in Sources */,
B535014B249D5908003837C8 /* HorizontalScrollView.swift in Sources */,
2614F1E51405A0C60031AE94 /* Simplenote.xcdatamodeld in Sources */,
B5EB3ADA2458E8430089858D /* NSApplication+Simplenote.swift in Sources */,
B5A2F1F02432DE54003B29C6 /* NSRange+Simplenote.swift in Sources */,
@@ -1665,6 +1670,7 @@
466FFEAD17CC10A800399652 /* NSString+Condensing.m in Sources */,
B5009938242130F70037A431 /* UnicodeScalar+Simplenote.swift in Sources */,
B5985AD6242950B40044EDE9 /* NSColor+Simplenote.swift in Sources */,
B535014C249D5908003837C8 /* HorizontalScrollView.swift in Sources */,
B5C7DD3E243E1F1900BEE354 /* VersionsViewController.swift in Sources */,
B57CB87C244DEC4B00BA7969 /* MigrationsHandler.swift in Sources */,
466FFEAE17CC10A800399652 /* NSString+Metadata.m in Sources */,
@@ -1,14 +1,24 @@
import Foundation


// MARK: - Simplenote Methods
//
extension Array where Element: Hashable {
extension Array where Element == String {

/// Returns a copy of the receiver *containing Unique Elements*.
/// Returns a copy of the receiver containing Unique Strings (case insensitive comparison!)
///
var unique: Array {
guard let output = NSOrderedSet(array: self).array as? [Element] else {
return self
var caseInsensitiveUnique: [String] {
var seen = Set<String>()
var output = [String]()

for string in self {
let lowercased = string.lowercased()
if seen.contains(lowercased) {
continue
}

output.append(string)
seen.insert(lowercased)
}

return output
@@ -0,0 +1,43 @@
import Foundation
import Cocoa


// MARK: - HorizontalScrollView
// This NSScrollView subclass remaps Vertical Scroll events into Horizontal Scroll events, in order to
// support ScrollWheel events performed with a mouse (single axis device!).
//
class HorizontalScrollView: NSScrollView {

override func scrollWheel(with event: NSEvent) {
/// Whenever the scroll event happens on the Y axis, we'll generate a new Scroll Event, instead, and we'll remap the deltaY.
/// Why: we need to support Scroll Wheel events, performed with a mouse (with a single axis).
///
guard (abs(event.deltaX) <= abs(event.deltaY)), let cgEvent = event.cgEvent?.copy() else {
super.scrollWheel(with: event)
return
}

cgEvent.setIntegerValueField(.scrollWheelEventDeltaAxis2, value: Int64(event.deltaY))

guard let darkEvent: NSEvent = NSEvent(cgEvent: cgEvent) else {
super.scrollWheel(with: event)
return
}

super.scrollWheel(with: darkEvent)
}

/// Notes:
/// 1. Since we're overriding `scrollWheel:` we must set the `horizontalScroller.hidden = NO`. Otherwise scrolling won't work.
/// 2. In this override we're making sure the NSScroller does not affect layout!
///
/// References:
/// https://developer.apple.com/reference/appkit/nsview#//apple_ref/occ/clm/NSView/isCompatibleWithResponsiveScrolling
/// https://stackoverflow.com/questions/31186430/scrolling-in-nsscrollview-stops-when-overwriting-scrollwheel-function
///
override func tile() {
super.tile()
contentView.frame = bounds
horizontalScroller?.isHidden = true
}
}
@@ -248,7 +248,7 @@ extension NoteEditorViewController: TagsFieldDelegate {
return []
}

return note.filterUnassociatedTagNames(from: tags).unique
return note.filterUnassociatedTagNames(from: tags).caseInsensitiveUnique
}

public func tokenField(_ tokenField: NSTokenField, didChange tokens: [String]) {
@@ -258,7 +258,7 @@ extension NoteEditorViewController: TagsFieldDelegate {
//
// For that reason, we'll filtering out duplicates.
//
updateTags(withTokens: tokens.unique)
updateTags(withTokens: tokens.caseInsensitiveUnique)
}
}

@@ -83,6 +83,7 @@ class TagsField: NSTokenField {
set {
objectValue = newValue
needsDisplay = true
invalidateIntrinsicContentSize()
}
}

@@ -101,7 +102,7 @@ class TagsField: NSTokenField {
}


// MARK: - Overridden API(s)
// MARK: - Text Edition Overridden API
//
extension TagsField {

@@ -113,6 +114,11 @@ extension TagsField {
override func textDidChange(_ notification: Notification) {
super.textDidChange(notification)

/// Scroll: Increase the scrollable area + follow with the cursor!
///
invalidateIntrinsicContentSize()
ensureCaretIsOnscreen()

/// During edition, `Non Terminated Tokens` will show up in the `objectValue` array.
/// We need the actual number of `Closed Tokens`, and we'll simply count how many TextAttachments we've got.
/// Capisci?
@@ -129,25 +135,70 @@ extension TagsField {
override func textDidEndEditing(_ notification: Notification) {
super.textDidEndEditing(notification)

// Always recalculate intrinsicContentSize.
// There might be no tokens committed yet (and the height at this point **might** not fit the new attachments.
invalidateIntrinsicContentSize()

// Tokens can get created when the control loses focus, but none of the expected events fire.
// Fire one manually instead.
tagsFieldDelegate?.tokenField(self, didChange: tokens)
}
}


// MARK: - Scroll / Autolayout Support
//
extension TagsField {

override var intrinsicContentSize: NSSize {
guard let scrollView = enclosingScrollView, let cellSize = cell?.cellSize else {
return super.intrinsicContentSize
}

// Notes:
// 1. Always assume the container's full width
// 2. Leave a bit of empty space on the right hand side, enough to fill a Placeholder!
// 3. Whenever we've got a Field Editor AND it's empty, always return the Placeholder String Height.
// **Why:**
// A. We need this field to be vertically centered.
// B. When in edition mode, the `cellSize` will always match the Attachment Height.
// Even if the new token wasn't committed (and isn't "Bubbled Up" in the editor)
//
let placeholderSize = simplenotePlaceholderAttributedString.size()
let calculatedWidth = cellSize.width.rounded(.up) + placeholderSize.width
let newWidth = max(calculatedWidth, scrollView.bounds.width)
let newHeight = isEditorActiveAndEmpty ? placeholderSize.height : cellSize.height.rounded(.up)

return CGSize(width: newWidth, height: newHeight)
}

var isEditorActiveAndEmpty: Bool {
let textView = currentEditor() as? NSTextView
return textView?.attributedString().numberOfAttachments == .zero
}
}


// MARK: - NSTextViewDelegate
//
extension TagsField: NSTextViewDelegate {

func textView(_ textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
guard commandSelector == #selector(cancelOperation(_:)) else {
return false
/// keyPress: `ESC`>> `resignFirstResponder`!
///
if commandSelector == #selector(cancelOperation(_:)) {
window?.makeFirstResponder(nil)
return true
}

/// keyPress `ESC`>> `resignFirstResponder`!
window?.makeFirstResponder(nil)
return true
/// keyPress: `Arrow(s)`, `Delete`, `*`
/// We don't really know *who* or exactly when will eventually handle this command. Eventually, let's make sure the horizontal scroll offset is accurate.
///
DispatchQueue.main.async {
self.ensureCaretIsOnscreen()
}

return false
}

func textView(_ textView: NSTextView, clickedOn cell: NSTextAttachmentCellProtocol, in cellFrame: NSRect, at charIndex: Int) {
@@ -170,6 +221,39 @@ extension TagsField: NSTextViewDelegate {
}


// MARK: - Cursor Helpers
//
private extension TagsField {

func ensureCaretIsOnscreen() {
guard let newVisibleRect = proposedVisibleRectForEdition else {
return
}

scrollToVisible(newVisibleRect)
}

var proposedVisibleRectForEdition: NSRect? {
guard let textView = currentEditor() as? NSTextView, let lm = textView.layoutManager, let container = textView.textContainer else {
return nil
}

/// Determine the Editor's cursor location
///
var output = lm.boundingRect(forGlyphRange: textView.selectedRange(), in: container)

let placeholderWidth = simplenotePlaceholderAttributedString.size().width

/// Adjust the Viewport: always accommodate to support `placeholder.width`'s on both sides
///
output.origin.x = max(output.origin.x - placeholderWidth, .zero)
output.size.width = placeholderWidth * 2

return output
}
}


// MARK: - Mouse Support
//
private extension TagsField {
@@ -181,8 +265,10 @@ private extension TagsField {
return
}

let newSelectedLocation = NSRange(location: range.location + stringValue.count, length: .zero)
textView.replaceCharacters(in: range, with: stringValue)
invalidateIntrinsicContentSize()

let newSelectedLocation = NSRange(location: range.location + stringValue.count, length: .zero)
textView.setSelectedRange(newSelectedLocation)
}

@@ -218,7 +304,7 @@ private extension TagsField {
/// - Note: As a failsafe measure, we're making sure the tokens are Unique!
///
func reprocessTokens() {
let newTokens = tokens.unique
let newTokens = tokens.caseInsensitiveUnique
tokens = newTokens
}
}
@@ -228,19 +314,15 @@ private extension TagsField {
//
private extension TagsField {

var simplenotePlaceholderAttributedString: NSAttributedString? {
guard drawsPlaceholder else {
return nil
}

return NSAttributedString(string: placeholderText, attributes: [
var simplenotePlaceholderAttributedString: NSAttributedString {
NSAttributedString(string: placeholderText, attributes: [
.font: placeholderFont,
.foregroundColor: placeholderTextColor
])
}

func refreshPlaceholder() {
placeholderAttributedString = simplenotePlaceholderAttributedString
placeholderAttributedString = drawsPlaceholder ? simplenotePlaceholderAttributedString : nil
}

func setupTokenizationSettings() {