Skip to content

Commit

Permalink
Fix LanguageTokenField crash. Also fix incorrect edit string when loa…
Browse files Browse the repository at this point in the history
…ding from saved prefs. Disallow duplicate language entries. Trim language entries for whitespace & make lowercase.
  • Loading branch information
svobs committed Nov 12, 2022
1 parent a29301c commit c9a3488
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 46 deletions.
100 changes: 58 additions & 42 deletions iina/LanguageTokenField.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,47 +8,37 @@

import Cocoa

fileprivate class Token: NSObject {
var content: String
var code: String
class LanguageTokenField: NSTokenField {
private var layoutManager: NSLayoutManager?

init(_ content: String) {
self.content = content
self.code = ISO639Helper.descriptionRegex.captures(in: content)[at: 1] ?? content
fileprivate var tokens: [String] {
return (objectValue as? NSArray)?.compactMap({ $0 as? String }) ?? []
}
}

class LanguageTokenField: NSTokenField {
private var layoutManager: NSLayoutManager?
var commaSeparatedValues: String {
get {
return tokens.map({ "\($0)".trimmingCharacters(in: .whitespaces) }).joined(separator: ",")
} set {
self.objectValue = newValue.count == 0 ? [] : newValue.components(separatedBy: ",").map{ $0.trimmingCharacters(in: .whitespaces) }
}
}

override func awakeFromNib() {
super.awakeFromNib()
self.delegate = self
self.tokenStyle = .rounded
}

override var stringValue: String {
set {
self.objectValue = newValue.count == 0 ?
[] : newValue.components(separatedBy: ",").map(Token.init)
}
get {
return (objectValue as? NSArray)?.map({ val in
if let token = val as? Token {
return token.code
} else if let str = val as? String {
return str
}
return ""
}).joined(separator: ",") ?? ""
}
@objc func controlTextDidEndEditing(_ notification: Notification) {
executeAction()
}

func controlTextDidChange(_ obj: Notification) {
guard let layoutManager = layoutManager else { return }
let attachmentChar = Character(UnicodeScalar(NSTextAttachment.character)!)
let finished = layoutManager.attributedString().string.split(separator: attachmentChar).count == 0
if finished, let target = target, let action = action {
target.performSelector(onMainThread: action, with: self, waitUntilDone: false)
if finished {
executeAction()
}
}

Expand All @@ -58,42 +48,68 @@ class LanguageTokenField: NSTokenField {
}
return true
}

func executeAction() {
if let target = target, let action = action {
target.performSelector(onMainThread: action, with: self, waitUntilDone: false)
}
}
}

extension LanguageTokenField: NSTokenFieldDelegate {
func tokenField(_ tokenField: NSTokenField, styleForRepresentedObject representedObject: Any) -> NSTokenField.TokenStyle {
return .rounded
}

func tokenField(_ tokenField: NSTokenField, displayStringForRepresentedObject representedObject: Any) -> String? {
if let token = representedObject as? Token {
return token.code
} else {
return representedObject as? String
func tokenField(_ tokenField: NSTokenField, shouldAdd tokens: [Any], at index: Int) -> [Any] {
var toAdd: [String] = []
for rawToken in tokens {
if let dirtyToken = rawToken as? String {
let cleanToken = dirtyToken.lowercased().trimmingCharacters(in: .whitespaces)

// Don't allow duplicates. But keep in mind `self.tokens` already includes the added token,
// so it's a duplicate if it occurs twice or more there
if self.tokens.filter({ $0 == cleanToken || $0 == dirtyToken }).count <= 1 {
Logger.log("Adding language token: \"\(cleanToken)\"", level: .verbose)
toAdd.append(cleanToken)
}
}
}
if !toAdd.isEmpty {
executeAction()
}
return toAdd
}

func tokenField(_ tokenField: NSTokenField, hasMenuForRepresentedObject representedObject: Any) -> Bool {
// Tokens never have a context menu
return false
}

func tokenField(_ tokenField: NSTokenField, completionsForSubstring substring: String, indexOfToken tokenIndex: Int, indexOfSelectedItem selectedIndex: UnsafeMutablePointer<Int>?) -> [Any]? {
// Returns array of auto-completion results for user's typed string (`substring`)
func tokenField(_ tokenField: NSTokenField, completionsForSubstring substring: String,
indexOfToken tokenIndex: Int, indexOfSelectedItem selectedIndex: UnsafeMutablePointer<Int>?) -> [Any]? {
let lowSubString = substring.lowercased()
let currentLangCodes = Set(self.tokens)
let matches = ISO639Helper.languages.filter { lang in
return lang.name.contains { $0.lowercased().hasPrefix(lowSubString) }
return !currentLangCodes.contains(lang.code) && lang.name.contains { $0.lowercased().hasPrefix(lowSubString) }
}
return matches.map { $0.description }
}

// Called by AppKit. Returns the string to use when displaying as a token
func tokenField(_ tokenField: NSTokenField, displayStringForRepresentedObject representedObject: Any) -> String? {
return representedObject as? String
}

// Called by AppKit. Returns the string to use when editing a token
func tokenField(_ tokenField: NSTokenField, editingStringForRepresentedObject representedObject: Any) -> String? {
if let token = representedObject as? Token {
return token.content
} else {
return representedObject as? String
}
guard let token = representedObject as? String else { return nil }

let matchingLangs = ISO639Helper.languages.filter({ $0.code == token })
return matchingLangs.isEmpty ? token : matchingLangs[0].description
}

// Called by AppKit. Returns a token for the given string
func tokenField(_ tokenField: NSTokenField, representedObjectForEditing editingString: String) -> Any? {
return Token(editingString)
// Return language code (if possible)
return ISO639Helper.descriptionRegex.captures(in: editingString)[at: 1] ?? editingString
}
}
8 changes: 6 additions & 2 deletions iina/PrefCodecViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class PrefCodecViewController: PreferenceViewController, PreferenceWindowEmbedda

override func viewDidLoad() {
super.viewDidLoad()
audioLangTokenField.stringValue = Preference.string(for: .audioLanguage) ?? ""
audioLangTokenField.commaSeparatedValues = Preference.string(for: .audioLanguage) ?? ""
updateHwdecDescription()
}

Expand Down Expand Up @@ -88,7 +88,11 @@ class PrefCodecViewController: PreferenceViewController, PreferenceWindowEmbedda
}

@IBAction func preferredLanguageAction(_ sender: LanguageTokenField) {
Preference.set(sender.stringValue, for: .audioLanguage)
let csv = sender.commaSeparatedValues
if Preference.string(for: .audioLanguage) != csv {
Logger.log("Saving \(Preference.Key.audioLanguage.rawValue): \"\(csv)\"", level: .verbose)
Preference.set(csv, for: .audioLanguage)
}
}

private func updateHwdecDescription() {
Expand Down
8 changes: 6 additions & 2 deletions iina/PrefSubViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ class PrefSubViewController: PreferenceViewController, PreferenceWindowEmbeddabl
defaultEncodingList.menu?.insertItem(NSMenuItem.separator(), at: 1)
loginIndicator.isHidden = true

subLangTokenView.stringValue = Preference.string(for: .subLang) ?? ""
subLangTokenView.commaSeparatedValues = Preference.string(for: .subLang) ?? ""

refreshSubSources()
refreshSubSourceAccessoryView()
Expand Down Expand Up @@ -136,7 +136,11 @@ class PrefSubViewController: PreferenceViewController, PreferenceWindowEmbeddabl
}

@IBAction func preferredLanguageAction(_ sender: LanguageTokenField) {
Preference.set(sender.stringValue, for: .subLang)
let csv = sender.commaSeparatedValues
if Preference.string(for: .subLang) != csv {
Logger.log("Saving \(Preference.Key.subLang.rawValue): \"\(csv)\"", level: .verbose)
Preference.set(csv, for: .subLang)
}
}

private func refreshSubSources() {
Expand Down

0 comments on commit c9a3488

Please sign in to comment.