1282 lines
46 KiB
Swift
1282 lines
46 KiB
Swift
/*
|
|
* vector sigma (https://github.com/vectorsigma72)
|
|
* Copyright 2020 vector sigma All Rights Reserved.
|
|
*
|
|
* The source code contained or described herein and all documents related
|
|
* to the source code ("Material") are owned by vector sigma.
|
|
* Title to the Material remains with vector sigma or its suppliers and licensors.
|
|
* The Material is proprietary of vector sigma and is protected by worldwide copyright.
|
|
* No part of the Material may be used, copied, reproduced, modified, published,
|
|
* uploaded, posted, transmitted, distributed, or disclosed in any way without
|
|
* vector sigma's prior express written permission.
|
|
*
|
|
* No license under any patent, copyright, trade secret or other intellectual
|
|
* property right is granted to or conferred upon you by disclosure or delivery
|
|
* of the Materials, either expressly, by implication, inducement, estoppel or
|
|
* otherwise. Any license under such intellectual property rights must be
|
|
* express and approved by vector sigma in writing.
|
|
*
|
|
* Unless otherwise agreed by vector sigma in writing, you may not remove or alter
|
|
* this notice or any other notice embedded in Materials by vector sigma in any way.
|
|
*
|
|
* The license is granted for the CloverBootloader project (i.e. https://github.com/CloverHackyColor/CloverBootloader)
|
|
* and all the users as long as the Material is used only within the
|
|
* source code and for the exclusive use of CloverBootloader, which must
|
|
* be free from any type of payment or commercial service for the license to be valid.
|
|
*/
|
|
|
|
import Cocoa
|
|
|
|
@available(OSX 10.11, *)
|
|
final class PlistEditorVC: NSViewController,
|
|
NSOutlineViewDelegate,
|
|
NSOutlineViewDataSource,
|
|
NSTextFieldDelegate,
|
|
NSSearchFieldDelegate, NSSplitViewDelegate {
|
|
@IBOutlet var findView: NSView!
|
|
@IBOutlet var editorView: NSView!
|
|
@IBOutlet var scrollView : NSScrollView!
|
|
@IBOutlet var outline : PEOutlineView!
|
|
@IBOutlet var doneBtn: NSButton?
|
|
@IBOutlet var showFindViewBtn: FindButton?
|
|
@IBOutlet var showReplaceBtn: ReplaceButton?
|
|
@IBOutlet var nextOrPreviousSegment: NSSegmentedControl?
|
|
@IBOutlet var replaceOneOrAllSegment: NSSegmentedControl?
|
|
@IBOutlet var searchField: PESearchField?
|
|
@IBOutlet var replaceField: PEReplaceField?
|
|
|
|
@IBOutlet var findAndReplaceViewHeightConstraint: NSLayoutConstraint!
|
|
|
|
var parser : PlistParser?
|
|
var vcLoaded : Bool = false
|
|
var isSearching : Bool = true
|
|
var pbTreeNode : PENode?
|
|
var edited : Bool = false
|
|
var plistPath : URL?
|
|
var searches : [PENode] = [PENode]()
|
|
var isAddingNewItem : Bool = false
|
|
var isEditable : Bool = true
|
|
var rootNode : PENode?
|
|
var searchTimer : Timer? = nil
|
|
var doc : Document? = nil
|
|
|
|
var document: Document? {
|
|
get {
|
|
self.doc = view.window?.windowController?.document as? Document
|
|
return self.doc
|
|
}
|
|
set {
|
|
self.doc = newValue
|
|
}
|
|
}
|
|
|
|
deinit {
|
|
NotificationCenter.default.removeObserver(self,
|
|
name: PESearchFieldTextDidChange,
|
|
object: nil)
|
|
}
|
|
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
self.findAndReplaceViewHeightConstraint.constant = 0
|
|
|
|
self.searchField?.placeholderString = localizedSearch
|
|
self.searchField?.countLabel?.stringValue = ""
|
|
|
|
self.doneBtn?.target = self
|
|
self.doneBtn?.action = #selector(self.doneButtonPressed(_:))
|
|
self.nextOrPreviousSegment?.target = self
|
|
self.nextOrPreviousSegment?.action = #selector(self.segmentScrollerPressed(_:))
|
|
self.replaceOneOrAllSegment?.target = self
|
|
self.replaceOneOrAllSegment?.action = #selector(self.replaceOneOrAllSegmentPressed(_:))
|
|
if self.replaceOneOrAllSegment != nil {
|
|
for i in 0..<self.replaceOneOrAllSegment!.segmentCount {
|
|
let label = self.replaceOneOrAllSegment!.label(forSegment: i)
|
|
self.replaceOneOrAllSegment!.setLabel(label!.locale, forSegment: i)
|
|
}
|
|
}
|
|
|
|
/*
|
|
Interface Builder is a mess and doesn't want to constraints the scrollView:
|
|
fu__!, who cares! ..I'll do by my self.
|
|
*/
|
|
gAddConstraintsToFit(superView: self.editorView , subView: self.scrollView)
|
|
}
|
|
|
|
override func viewDidAppear() {
|
|
super.viewDidAppear()
|
|
self.searchField?.delegate = self
|
|
|
|
if !self.vcLoaded {
|
|
self.outline.target = self
|
|
self.outline.editorVC = self
|
|
self.outline.doubleAction = #selector(self.customDoubleClick)
|
|
self.doc = self.document
|
|
|
|
if ((self.doc?.fileURL) != nil) {
|
|
self.view.window?.title = (self.doc?.fileURL?.path)!
|
|
}
|
|
// -----------------------------
|
|
|
|
self.rootNode = self.parser!.root
|
|
|
|
let type = self.rootNode!.tagdata!.type
|
|
if type == .Dictionary {
|
|
let ro = TagData(key: "Root", type: .Dictionary, value: NSDictionary())
|
|
let root = PENode(representedObject: ro)
|
|
root.mutableChildren.add(self.rootNode!)
|
|
self.rootNode = root
|
|
} else if type == .Array {
|
|
let root = PENode(representedObject: TagData(key: "Root", type: .Array, value: NSArray()))
|
|
root.mutableChildren.add(self.rootNode!)
|
|
self.rootNode = root
|
|
} else {
|
|
let root = PENode(representedObject: TagData(key: "Root", type: .Dictionary, value: NSDictionary()))
|
|
root.mutableChildren.add(self.rootNode!)
|
|
self.rootNode!.tagdata?.key = "Root" // override
|
|
self.rootNode = root
|
|
}
|
|
// -------------------------------------------------------------
|
|
// What to do if the specified plist path is a directory or not exist or isn't valid?
|
|
// open a new empty editor w/o specifying the real path!
|
|
var isDir : ObjCBool = false
|
|
|
|
if (plistPath != nil) && fm.fileExists(atPath: (plistPath?.path)!, isDirectory: &isDir) {
|
|
self.plistPath = plistPath!
|
|
} else {
|
|
self.plistPath = nil
|
|
}
|
|
// -----------------------------
|
|
self.outline.focusRingType = .exterior
|
|
self.outline.selectionHighlightStyle = .regular
|
|
self.outline.gridStyleMask = .solidHorizontalGridLineMask
|
|
|
|
self.outline.tableColumns[0].headerCell.stringValue = "Key".locale
|
|
self.outline.tableColumns[1].headerCell.stringValue = "Type".locale
|
|
self.outline.tableColumns[2].headerCell.stringValue = "Value".locale
|
|
|
|
self.outline.intercellSpacing = NSMakeSize(0, 0)
|
|
self.outline.registerForDraggedTypes([kMyPBoardTypeXml, kMyPBoardTypeData])
|
|
|
|
self.outline.reloadData()
|
|
DispatchQueue.main.async {
|
|
self.outline.expandItem(self.outline.item(atRow: 0))
|
|
self.outline.selectRowIndexes(IndexSet(integer: 0), byExtendingSelection: false)
|
|
}
|
|
|
|
NotificationCenter.default.addObserver(self,
|
|
selector: #selector(self.peSearchFieldTextDidChange(_:)),
|
|
name: PESearchFieldTextDidChange,
|
|
object: nil)
|
|
|
|
self.document?.undoManager?.enableUndoRegistration()
|
|
|
|
self.vcLoaded = true
|
|
|
|
NSApp.setActivationPolicy(.regular)
|
|
self.view.window?.makeFirstResponder(self.outline)
|
|
|
|
if let mainMenu = NSApplication.shared.mainMenu {
|
|
mainMenu.update()
|
|
for i in mainMenu.items {
|
|
if i.title == "Format" {
|
|
mainMenu.removeItem(i)
|
|
break
|
|
}
|
|
}
|
|
mainMenu.translate()
|
|
}
|
|
}
|
|
}
|
|
|
|
override var representedObject: Any? {
|
|
didSet {
|
|
// Update the view, if already loaded.
|
|
}
|
|
}
|
|
|
|
@objc func performFindPanelAction(_ sender: Any) {
|
|
if let mItem = sender as? NSMenuItem {
|
|
switch mItem.tag {
|
|
case 0: /* Jump to selection */
|
|
NSSound.beep()
|
|
break
|
|
case 1: /* Find */
|
|
self.showFindView(sender: sender)
|
|
break
|
|
case 2: /* Find Next */
|
|
self.showFindView(sender: sender)
|
|
break
|
|
case 3: /* Find Previous */
|
|
self.showFindView(sender: sender)
|
|
break
|
|
case 7: /* Use selection for Find */
|
|
NSSound.beep()
|
|
break
|
|
case 12: /* Find And Replace */
|
|
self.showReplaceView(sender: sender)
|
|
break
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc func showHelp(_ sender: Any?) {
|
|
if Locale.current.languageCode?.lowercased() == "ru" {
|
|
NSWorkspace.shared.open(URL(string: "https://applelife.ru/threads/clover.42089/")!)
|
|
} else {
|
|
NSWorkspace.shared.open(URL(string: "https://www.insanelymac.com/forum/topic/304530-clover-change-explanations/")!)
|
|
}
|
|
}
|
|
|
|
func hideFindAndReplaceView() {
|
|
self.findAndReplaceViewHeightConstraint.animator().constant = 0
|
|
self.view.window?.makeFirstResponder(self.outline)
|
|
}
|
|
|
|
@IBAction func showFindView(sender: Any?) {
|
|
self.findAndReplaceViewHeightConstraint.animator().constant = 28
|
|
self.view.window?.makeFirstResponder(self.searchField)
|
|
}
|
|
|
|
@IBAction func showReplaceView(sender: Any?) {
|
|
self.findAndReplaceViewHeightConstraint.animator().constant = 57
|
|
self.view.window?.makeFirstResponder(self.replaceField)
|
|
}
|
|
|
|
@objc func customDoubleClick() {
|
|
let clicked = self.outline.clickedRow
|
|
if clicked > 0 {
|
|
let selectedColumn = self.outline.clickedColumn
|
|
switch selectedColumn {
|
|
case 0:
|
|
let petcv = self.outline.view(atColumn: selectedColumn,
|
|
row: clicked,
|
|
makeIfNecessary: false) as! PETableCellView?
|
|
if (petcv != nil) {
|
|
self.outline.window?.makeFirstResponder(petcv?.textField)
|
|
}
|
|
break
|
|
case 2:
|
|
if let nstcv = self.outline.view(atColumn: selectedColumn,
|
|
row: clicked,
|
|
makeIfNecessary: false) as! PETableCellView? {
|
|
if (nstcv.field?.isEditable)! {
|
|
self.outline.window?.makeFirstResponder(nstcv.textField)
|
|
}
|
|
}
|
|
break
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
// ----------------------------------------------------------------------
|
|
|
|
// MARK: outline view delegate
|
|
// -------------------------------------------------------------
|
|
func childrenFor(item: Any?) -> [NSTreeNode] {
|
|
if item != nil {
|
|
return (item as! PENode).children!
|
|
} else {
|
|
return (self.rootNode!.children)!
|
|
}
|
|
}
|
|
|
|
func outlineView(_ outlineView: NSOutlineView,
|
|
numberOfChildrenOfItem item: Any?) -> Int {
|
|
if self.rootNode == nil {
|
|
return 0
|
|
}
|
|
let count = childrenFor(item: item).count
|
|
return count
|
|
}
|
|
|
|
func outlineView(_ outlineView: NSOutlineView,
|
|
heightOfRowByItem item: Any) -> CGFloat {
|
|
return 18.0
|
|
}
|
|
|
|
func outlineView(_ outlineView: NSOutlineView,
|
|
child index: Int,
|
|
ofItem item: Any?) -> Any {
|
|
return childrenFor(item: item)[index]
|
|
}
|
|
|
|
func outlineView(_ outlineView: NSOutlineView,
|
|
viewFor tableColumn: NSTableColumn?,
|
|
item: Any) -> NSView? {
|
|
switch item {
|
|
case let i as PENode:
|
|
if tableColumn?.identifier.rawValue == "keyColumn" {
|
|
var view : PETableCellView?
|
|
view = outlineView.makeView(withIdentifier: "keyColumn".interfaceId(),
|
|
owner: self) as? PETableCellView
|
|
view?.setup(outline: self.outline, column: 0, type: .key)
|
|
|
|
view?.node = i
|
|
// hide buttonsView
|
|
if ((self.outline.item(atRow: 0) as! PENode) == i) {
|
|
view?.removeButton?.isHidden = true
|
|
view?.removeButton?.isEnabled = false
|
|
if i.tagdata?.type != .Dictionary && i.tagdata?.type != .Array {
|
|
view?.addButton?.isHidden = true
|
|
view?.addButton?.isEnabled = false
|
|
} else {
|
|
view?.addButton?.isHidden = false
|
|
view?.addButton?.isEnabled = true
|
|
}
|
|
} else {
|
|
view?.removeButton?.isHidden = false
|
|
view?.removeButton?.isEnabled = true
|
|
view?.addButton?.isHidden = false
|
|
view?.addButton?.isEnabled = true
|
|
}
|
|
|
|
if let textField = view?.field {
|
|
textField.node = i
|
|
if ((self.outline.item(atRow: 0) as! PENode) == i) {
|
|
textField.isEditable = false
|
|
textField.isSelectable = false
|
|
textField.stringValue = (i.tagdata?.key)!
|
|
} else {
|
|
if (i.peparent != nil) && i.peparent!.tagdata!.type == .Array {
|
|
let childIndex : Int = i.peparent!.mutableChildren.index(of: i)
|
|
textField.stringValue = "\(localizedItem) \(childIndex)"
|
|
textField.isEditable = false
|
|
textField.isSelectable = false
|
|
} else {
|
|
if (i.highLightPattern != nil) && (i.highLightPattern)!.count > 0 {
|
|
textField.allowsEditingTextAttributes = true
|
|
textField.attributedStringValue = self.setHiglight(substring: i.highLightPattern!,
|
|
in: (i.tagdata?.key)!)
|
|
textField.allowsEditingTextAttributes = false
|
|
} else {
|
|
//textField.allowsEditingTextAttributes = false
|
|
textField.stringValue = (i.tagdata?.key)!
|
|
}
|
|
textField.isEditable = true
|
|
textField.isSelectable = true
|
|
}
|
|
}
|
|
|
|
if !self.isEditable {
|
|
textField.isEditable = false
|
|
}
|
|
}
|
|
|
|
return view
|
|
|
|
} else if tableColumn?.identifier.rawValue == "typeColumn" {
|
|
var view : PETableCellViewPop?
|
|
view = outlineView.makeView(withIdentifier: "typeColumn".interfaceId(),
|
|
owner: self) as? PETableCellViewPop
|
|
|
|
view?.setup(with: .tags, outline: self.outline)
|
|
|
|
view?.popup.node = i
|
|
view?.popup.setAsAllType()
|
|
|
|
view?.popup.selectItem(withTitle: gPlistTagStr(tag: i.tagdata!.type).locale)
|
|
if !self.isEditable {
|
|
view?.popup.isEnabled = false
|
|
}
|
|
return view
|
|
} else if tableColumn?.identifier.rawValue == "valueColumn" {
|
|
if i.tagdata!.type == .Bool {
|
|
var view = outlineView.makeView(withIdentifier: "valueBoolView".interfaceId(),
|
|
owner: self) as? PETableCellViewPop
|
|
if view == nil {
|
|
view = outlineView.makeView(withIdentifier: "typeColumn".interfaceId(),
|
|
owner: self) as? PETableCellViewPop
|
|
view?.identifier = "valueBoolView".interfaceId()
|
|
view?.popup.setAsBool()
|
|
}
|
|
|
|
view?.setup(with: .bool, outline: self.outline)
|
|
|
|
view?.popup.node = i
|
|
let result : String = i.tagdata!.value as! Bool
|
|
? localizedYes
|
|
: localizedNo
|
|
|
|
|
|
view?.popup.selectItem(withTitle: result)
|
|
if !self.isEditable {
|
|
view?.popup.isEnabled = false
|
|
}
|
|
return view
|
|
} else {
|
|
var view = outlineView.makeView(withIdentifier: "valueView".interfaceId(),
|
|
owner: self) as? PETableCellView
|
|
if view == nil {
|
|
view = outlineView.makeView(withIdentifier: "keyColumn".interfaceId(),
|
|
owner: self) as? PETableCellView
|
|
view?.setup(outline: self.outline, column: 2, type: .value)
|
|
view?.identifier = "valueView".interfaceId()
|
|
}
|
|
|
|
view?.field?.node = i
|
|
if i.tagdata!.type == .Array || i.tagdata!.type == .Dictionary {
|
|
view?.field?.isEditable = false
|
|
view?.field?.isSelectable = false
|
|
view?.field?.stringValue = ""
|
|
let count = i.count
|
|
let countString : String = "\(count)"
|
|
view?.field?.placeholderString = countString + " " + ((count == 1) ? localizedItem : localizedItems)
|
|
} else {
|
|
view?.field?.placeholderString = ""
|
|
view?.field?.isEditable = true
|
|
view?.field?.isSelectable = true
|
|
|
|
var str : String = ""
|
|
switch i.tagdata!.type {
|
|
case .Number:
|
|
let num = i.tagdata?.value as! NSNumber
|
|
str = localizedStringFrom(number: num)
|
|
break
|
|
case .Date:
|
|
let date = i.tagdata?.value as! Date
|
|
str = localizedDateToString(date)
|
|
break
|
|
case .Data:
|
|
let data = i.tagdata?.value as! Data
|
|
str = "<"
|
|
let dataCount = data.count
|
|
for i in 0..<dataCount {
|
|
let byte = data[i]
|
|
str += String(format: "%02x", byte)
|
|
if i < (data.count - 1) {
|
|
str += " "
|
|
}
|
|
}
|
|
str += ">"
|
|
break
|
|
default:
|
|
str = "\((i.tagdata?.value) ?? "")"
|
|
}
|
|
|
|
if (i.highLightPattern != nil) && (i.highLightPattern)!.count > 0 {
|
|
view?.field?.stringValue = str
|
|
view?.field?.allowsEditingTextAttributes = true
|
|
view?.field?.attributedStringValue = self.setHiglight(substring: i.highLightPattern!, in: str)
|
|
view?.field?.allowsEditingTextAttributes = false
|
|
} else {
|
|
view?.field?.allowsEditingTextAttributes = false
|
|
view?.field?.stringValue = str
|
|
}
|
|
}
|
|
|
|
|
|
return view
|
|
}
|
|
}
|
|
default:
|
|
return nil
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func setHiglight(substring: String, in string: String) -> NSAttributedString {
|
|
var attributed : NSMutableAttributedString
|
|
attributed = NSMutableAttributedString(string: string)
|
|
|
|
let range : NSRange = (attributed.string.lowercased() as NSString).range(of: substring)
|
|
if range.location != NSNotFound {
|
|
let attributes = [
|
|
NSAttributedString.Key.foregroundColor: NSColor.black,
|
|
NSAttributedString.Key.backgroundColor: NSColor.yellow
|
|
]
|
|
|
|
attributed.addAttributes(attributes, range: range)
|
|
}
|
|
|
|
return attributed
|
|
}
|
|
|
|
@objc func doneButtonPressed(_ sender: NSButton) {
|
|
self.searchField?.stringValue = ""
|
|
self.replaceField?.stringValue = ""
|
|
self.performDelayedSearch()
|
|
self.searchField?.countLabel?.placeholderString = ""
|
|
self.hideFindAndReplaceView()
|
|
}
|
|
|
|
func outlineView(_ outlineView: NSOutlineView, rowViewForItem item: Any) -> NSTableRowView? {
|
|
let rw = PETableRowView()
|
|
rw.node = item as? PENode
|
|
rw.outline = outlineView as? PEOutlineView
|
|
return rw
|
|
}
|
|
|
|
func outlineView(_ outlineView: NSOutlineView, didAdd rowView: NSTableRowView, forRow row: Int) {
|
|
if row >= 0 && self.isAddingNewItem {
|
|
self.outline.selectRowIndexes(IndexSet(integer: row), byExtendingSelection: false)
|
|
if let nstck = self.outline.view(atColumn: 0, row: row, makeIfNecessary: false) as! PETableCellView? {
|
|
self.outline.window?.makeFirstResponder(nstck.textField)
|
|
}
|
|
}
|
|
}
|
|
|
|
// not used because no delegate is set)
|
|
func controlTextDidChange(_ obj: Notification) {
|
|
if (obj.object as AnyObject) as? NSObject == self.searchField {
|
|
|
|
}
|
|
}
|
|
|
|
// called by NSSearchField subclass
|
|
@objc func peSearchFieldTextDidChange(_ obj: Notification) {
|
|
if (self.outline.window != nil) && (obj.object != nil) && (self.outline.window?.isKeyWindow)! {
|
|
if (obj.object is PESearchField) && (obj.object as! PESearchField) == self.searchField {
|
|
if (self.searchTimer != nil) && (self.searchTimer?.isValid)! {
|
|
self.searchTimer?.invalidate()
|
|
}
|
|
|
|
// shedule a timer for the searches. This is to not call performDelayedSearch() so often
|
|
self.searchTimer = Timer.scheduledTimer(timeInterval: 0.8,
|
|
target: self,
|
|
selector: #selector(self.performDelayedSearch),
|
|
userInfo: nil,
|
|
repeats: false)
|
|
} else if (obj.object is PEReplaceField) && (obj.object as! PEReplaceField) == self.replaceField {
|
|
// doing nothing
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc func performDelayedSearch() {
|
|
if (self.searchTimer != nil) {
|
|
if self.searchTimer!.isValid {
|
|
self.searchTimer!.invalidate()
|
|
}
|
|
self.searchTimer = nil
|
|
}
|
|
let pattern = self.searchField?.stringValue.lowercased()
|
|
self.searches = self.findAllSearches(with: pattern!)
|
|
self.searchField?.countLabel?.placeholderString = "\(self.searches.count)"
|
|
|
|
for node in self.searches {
|
|
self.asyncExpand(item: node, expandParentOnly: true)
|
|
}
|
|
|
|
/*
|
|
Reload all visible rows to update our findings.
|
|
Also will normalize previously highlighted fields
|
|
*/
|
|
var maxIndex : Int = self.outline.numberOfRows - 1
|
|
if maxIndex > 0 {
|
|
while maxIndex >= 0 {
|
|
self.outline.reloadData(forRowIndexes: IndexSet(integer: maxIndex),
|
|
columnIndexes: IndexSet(integer: 0))
|
|
self.outline.reloadData(forRowIndexes: IndexSet(integer: maxIndex),
|
|
columnIndexes: IndexSet(integer: 2))
|
|
|
|
maxIndex-=1
|
|
}
|
|
}
|
|
}
|
|
|
|
//FIXME: change "asyncExpand" to other as is no longer asynchronous
|
|
func asyncExpand(item: PENode, expandParentOnly: Bool) {
|
|
var parents : [PENode] = [PENode]()
|
|
var parent : PENode? = item.peparent
|
|
|
|
self.outline.expandItem(self.outline.item(atRow: 0))
|
|
|
|
repeat {
|
|
if (parent != nil) {
|
|
parents.insert(parent!, at: 0)
|
|
}
|
|
parent = parent?.peparent
|
|
} while parent != nil
|
|
|
|
for n in parents {
|
|
if !self.outline.isItemExpanded(n) {
|
|
self.outline.expandItem(n)
|
|
}
|
|
}
|
|
|
|
if !expandParentOnly {
|
|
if !self.outline.isItemExpanded(item) {
|
|
self.outline.expandItem(item)
|
|
}
|
|
}
|
|
}
|
|
|
|
func findAllSearches(with pattern: String) -> [PENode] {
|
|
// where to start? --> from outline object at index 0
|
|
var founds : [PENode] = [PENode]()
|
|
let root : PENode? = self.outline.item(atRow: 0) as? PENode
|
|
|
|
if (root != nil) &&
|
|
(root?.tagdata?.type == .Dictionary || root?.tagdata?.type == .Array) {
|
|
for n in root!.mutableChildren {
|
|
self.populateSearches(pattern: pattern, in: n as! PENode, array: &founds)
|
|
}
|
|
|
|
}
|
|
|
|
return founds
|
|
}
|
|
|
|
func populateSearches(pattern: String, in node: PENode, array: inout [PENode]) {
|
|
// always search in the key if parent isn't array
|
|
// always search in the value if node is not a contenitor (Dictionary or array)
|
|
// Skip searching in boolean and Data values
|
|
// problem: some value can have a localized result (Date and Number).. what to do here???
|
|
|
|
let root : PENode? = self.outline.item(atRow: 0) as? PENode
|
|
if node != root {
|
|
var parentIsArray : Bool = false
|
|
var found : Bool = false
|
|
if node.peparent?.tagdata?.type == .Array {
|
|
parentIsArray = true
|
|
}
|
|
|
|
// First: search a match in the key
|
|
if !parentIsArray {
|
|
if (node.tagdata?.key.lowercased().range(of: pattern) != nil) {
|
|
node.highLightPattern = pattern
|
|
array.append(node)
|
|
found = true
|
|
} else {
|
|
node.highLightPattern = nil
|
|
}
|
|
}
|
|
// Second: search a match in the value
|
|
if !found {
|
|
var localizedVal : String = ""
|
|
|
|
switch node.tagdata!.type {
|
|
case .String:
|
|
localizedVal = node.tagdata?.value as! String
|
|
break
|
|
case .Number:
|
|
let num = node.tagdata?.value as! NSNumber
|
|
localizedVal = localizedStringFrom(number: num)
|
|
break
|
|
case .Date:
|
|
let date = node.tagdata?.value as! Date
|
|
localizedVal = localizedDateToString(date)
|
|
break
|
|
default:
|
|
break
|
|
}
|
|
|
|
if (localizedVal.lowercased().range(of: pattern) != nil) {
|
|
node.highLightPattern = pattern
|
|
array.append(node)
|
|
} else {
|
|
node.highLightPattern = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
if node.tagdata?.type == .Dictionary || node.tagdata?.type == .Array {
|
|
for n in node.mutableChildren {
|
|
self.populateSearches(pattern: pattern, in: n as! PENode, array: &array)
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc func segmentScrollerPressed(_ sender: NSSegmentedControl?) {
|
|
if (self.searchField?.stringValue.count)! > 0 {
|
|
if self.searches.count > 0 {
|
|
self.view.window?.makeFirstResponder(self.outline)
|
|
var index : Int = 0
|
|
/*
|
|
check if we have a selected row and backup the object for later use:
|
|
after expanding We may lose the selection
|
|
*/
|
|
var selectedRow = self.outline.selectedRow
|
|
var selectedNode: PENode? = nil
|
|
if selectedRow >= 0 {
|
|
selectedNode = self.outline.item(atRow: selectedRow) as? PENode
|
|
}
|
|
/*
|
|
since we are calculating with rows indexes, expand now all involved nodes-parent before doing anything
|
|
*/
|
|
for node in self.searches {
|
|
self.asyncExpand(item: node, expandParentOnly: true)
|
|
}
|
|
|
|
/*
|
|
(selectedNode != nil) means that user has starting scrolling already?
|
|
sure, but can be nil if user has loses the selection by clicking on any
|
|
If that happened, We know We have to start from self.searches[0].
|
|
Otherwise (a row is selected but isn't in self.searches) we have to find
|
|
its row index and start scrolling from there
|
|
*/
|
|
|
|
if (selectedNode != nil) {
|
|
//selectedRow was valid.. but its in our searches?
|
|
if self.searches.contains(selectedNode!) {
|
|
index = self.searches.firstIndex(of: selectedNode!)!
|
|
} else {
|
|
// nope, jump to the nearest one, but befor or after?
|
|
selectedRow = self.outline.row(forItem: selectedNode)
|
|
let nrowsIndex : Int = self.outline.numberOfRows - 1
|
|
var i : Int = selectedRow
|
|
if sender?.selectedSegment == 0 {
|
|
// scan back
|
|
while i > 0 {
|
|
let scannedNode = self.outline.item(atRow: i) as! PENode
|
|
if self.searches.contains(scannedNode) {
|
|
index = self.searches.firstIndex(of: scannedNode)! + 1
|
|
selectedNode = scannedNode
|
|
break
|
|
}
|
|
i-=1
|
|
}
|
|
} else {
|
|
// scan forward
|
|
while i < nrowsIndex {
|
|
let scannedNode = self.outline.item(atRow: i) as! PENode
|
|
if self.searches.contains(scannedNode) {
|
|
index = self.searches.firstIndex(of: scannedNode)! - 1
|
|
selectedNode = scannedNode
|
|
break
|
|
}
|
|
i+=1
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
index = 0
|
|
selectedNode = self.searches[0]
|
|
}
|
|
|
|
/* jump to previous or next search */
|
|
var jumpTo : Int = index
|
|
let lastIndex : Int = self.searches.count - 1
|
|
if sender?.selectedSegment == 0 {
|
|
// go back
|
|
jumpTo = (jumpTo == 0) ? lastIndex : (jumpTo - 1)
|
|
selectedNode = self.searches[jumpTo]
|
|
let row = self.outline.row(forItem: selectedNode)
|
|
if row >= 0 {
|
|
self.outline.scrollRowToVisible(self.outline.row(forItem: selectedNode))
|
|
self.outline.selectRowIndexes(IndexSet(integer: row), byExtendingSelection: false)
|
|
}
|
|
} else {
|
|
// go ahead
|
|
//let lastRowInOutLine : Int = self.outline.numberOfRows - 1
|
|
let lastSearchInOutline : Int = self.outline.row(forItem: self.searches.last)
|
|
|
|
if selectedRow > lastSearchInOutline {
|
|
jumpTo = 0
|
|
} else {
|
|
jumpTo = (jumpTo == lastIndex) ? 0 : (jumpTo + 1)
|
|
}
|
|
|
|
selectedNode = self.searches[jumpTo]
|
|
let row = self.outline.row(forItem: selectedNode)
|
|
if row >= 0 {
|
|
self.outline.scrollRowToVisible(self.outline.row(forItem: selectedNode))
|
|
self.outline.selectRowIndexes(IndexSet(integer: row), byExtendingSelection: false)
|
|
}
|
|
}
|
|
} else {
|
|
NSSound.beep()
|
|
}
|
|
} else {
|
|
NSSound.beep()
|
|
}
|
|
}
|
|
|
|
@objc func replaceOneOrAllSegmentPressed(_ sender: NSSegmentedControl) {
|
|
/* here we perform replacements.
|
|
allow replace text field to be empty ("")
|
|
*/
|
|
|
|
if (self.searchField?.stringValue.count)! > 0 && self.searches.count > 0 {
|
|
self.view.window?.makeFirstResponder(self.outline)
|
|
let nodes : NSMutableArray = NSMutableArray()
|
|
let oldTreeData : NSMutableArray = NSMutableArray()
|
|
let newTreeData : NSMutableArray = NSMutableArray()
|
|
|
|
for n in ((sender.selectedSegment == 0) ? self.searches : [self.searches[0]]) {
|
|
nodes.add(n)
|
|
/*
|
|
replace selected with Undo/Redo.
|
|
To do that we should jump to the nearest search,
|
|
apparently Xcode do that by always search from scratch everytime the segment is pressed
|
|
and so it start always from index 0 and it looks scrolling ... but isn't!
|
|
*/
|
|
let originalNode : PENode = n
|
|
let parent : PENode = originalNode.peparent!
|
|
let moddedNode : PENode = originalNode.copy() as! PENode
|
|
|
|
/*
|
|
Problem, we should ensure the node parent has not a key already present after the replace operation:
|
|
in this case we must add a suffix (- 1, -2 etc.).
|
|
*/
|
|
let replacement = self.replaceField?.stringValue ?? ""
|
|
/* replacing on key */
|
|
let poposedNewKeyName : String = (moddedNode.tagdata?.key.replacingOccurrencesOf(inSensitive: originalNode.highLightPattern!, withSensitive: replacement))!
|
|
/* replacing on value:
|
|
- Bool: cannot be replaced, but should not be there
|
|
- Data: cannot be replaced, but should not be there
|
|
- Number: is localized formmatted, so return a value only if make sense
|
|
- Date: is localized formmatted, so return a value only if make sense
|
|
- String: no problem
|
|
*/
|
|
var localizedVal : String = ""
|
|
switch originalNode.tagdata!.type {
|
|
case .String:
|
|
localizedVal = moddedNode.tagdata?.value as! String
|
|
localizedVal = localizedVal.replacingOccurrencesOf(inSensitive: originalNode.highLightPattern!,
|
|
withSensitive: replacement)
|
|
moddedNode.tagdata?.value = localizedVal
|
|
break
|
|
case .Number:
|
|
let num = originalNode.tagdata?.value as! NSNumber
|
|
localizedVal = localizedStringFrom(number: num)
|
|
|
|
localizedVal = localizedVal.replacingOccurrencesOf(inSensitive: originalNode.highLightPattern!,
|
|
withSensitive: replacement)
|
|
// is still a valid Number??
|
|
var nv : Int = 0
|
|
localizedVal = localizedVal.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
|
|
if localizedVal.count > 0 {
|
|
nv = Int(localizedVal)!
|
|
}
|
|
moddedNode.tagdata?.value = nv
|
|
break
|
|
case .Date:
|
|
let date = originalNode.tagdata?.value as! Date
|
|
localizedVal = localizedDateToString(date)
|
|
localizedVal = localizedVal.replacingOccurrencesOf(inSensitive: originalNode.highLightPattern!,
|
|
withSensitive: replacement)
|
|
// is still a valid date??
|
|
var dv : Date? = nil
|
|
dv = localizedStringToDateS(localizedVal) // first try with "S" version that can be nil
|
|
|
|
if (dv == nil) {
|
|
// secondly try to detect it (NSDataDetector + NSTextCheckingResult)
|
|
// This allow us to have the old date if after replacing is bad
|
|
dv = funcyDateFromUser(localizedVal)
|
|
}
|
|
|
|
if (dv == nil) {
|
|
dv = Date() // no way? init a new one...
|
|
}
|
|
|
|
moddedNode.tagdata?.value = dv! as NSDate
|
|
break
|
|
default:
|
|
break
|
|
}
|
|
|
|
/* assing an univoque key name.
|
|
Don't use deduplicateKeyInParent() because we are not adding a new key:
|
|
parent must not contains duplicate keys, but this one is just the same as before,
|
|
maybe that after replacing an occurences can be equal to another one
|
|
*/
|
|
let childs = parent.mutableChildren
|
|
if childs.count > 1 {
|
|
// more than one item
|
|
var actualKeys = [String]()
|
|
for item in parent.mutableChildren {
|
|
/* we're looking for duplicate keys, but since our node is already present
|
|
remove it from actualKeys and see if poposedNewKeyName is already present
|
|
*/
|
|
if (item as! PENode) != originalNode {
|
|
actualKeys.append((item as! PENode).tagdata!.key)
|
|
}
|
|
}
|
|
moddedNode.tagdata?.key = gProposedNewItem(with: poposedNewKeyName, in: actualKeys)
|
|
} else {
|
|
// replace the only available children's key
|
|
moddedNode.tagdata?.key = poposedNewKeyName
|
|
}
|
|
|
|
|
|
/*
|
|
remove highLightPattern from originalNode
|
|
After a Undo/Redo user have to searcg again?
|
|
*/
|
|
originalNode.highLightPattern = nil
|
|
moddedNode.highLightPattern = nil
|
|
|
|
oldTreeData.add(originalNode.tagdata!.copy())
|
|
newTreeData.add(moddedNode.tagdata!.copy())
|
|
}
|
|
|
|
if sender.selectedSegment == 0 {
|
|
self.searches.removeAll()
|
|
} else {
|
|
self.searches.remove(at: 0)
|
|
}
|
|
|
|
self.outline.undo_FindAndReplace(nodes: nodes,
|
|
oldTreeData: oldTreeData,
|
|
newTreeData: newTreeData)
|
|
} else {
|
|
NSSound.beep()
|
|
}
|
|
}
|
|
|
|
func controlTextDidBeginEditing(_ obj: Notification) {
|
|
if obj.object is PETextField {
|
|
self.outline.wrongValue = true // here only indicate that we're editing
|
|
}
|
|
}
|
|
|
|
func controlTextDidEndEditing(_ obj: Notification) {
|
|
|
|
if obj.object is PETextField {
|
|
self.outline.wrongValue = false // will be set to true again if value is wrong
|
|
let textField = obj.object as! PETextField
|
|
|
|
let ro = textField.node?.tagdata
|
|
if textField.node?.parent == nil {
|
|
return // usefull if user undo after adding a new row
|
|
}
|
|
let parent = textField.node!.peparent!
|
|
|
|
switch textField.column {
|
|
case 0:
|
|
var canEdit : Bool = true
|
|
let oldKey = ro?.key
|
|
let newKey = textField.stringValue
|
|
|
|
if oldKey != newKey {
|
|
for i in parent.mutableChildren {
|
|
let node = i as! PENode
|
|
if node.tagdata?.key == newKey {
|
|
self.outline.wrongValue = true
|
|
canEdit = false
|
|
NSSound.beep()
|
|
textField.window?.makeFirstResponder(textField)
|
|
let alert = PEAlert(in: (self.outline.window)!)
|
|
alert.messageText = localizedDuplicateKeyMsgText
|
|
|
|
let withFormat = localizedDuplicateKeyInfoText
|
|
let question = String(format: withFormat, newKey)
|
|
|
|
alert.informativeText = question
|
|
alert.addButton(withTitle: localizedKeepEditing)
|
|
alert.addButton(withTitle: localizedDuplicateKeyReplaceButton)
|
|
|
|
alert.beginSheetModal(for: self.outline.window!, completionHandler: { (modalResponse) -> Void in
|
|
if modalResponse == NSApplication.ModalResponse.alertFirstButtonReturn {
|
|
self.outline.window?.makeFirstResponder(textField)
|
|
} else {
|
|
let indexChild : Int = parent.mutableChildren.index(of: node)
|
|
self.outline.undo_ReplaceExisting(item: node,
|
|
inParent: parent,
|
|
indexChild: indexChild,
|
|
editedNode: textField.node!,
|
|
oldKey: oldKey!,
|
|
newKey: newKey)
|
|
}
|
|
})
|
|
break
|
|
}
|
|
}
|
|
if canEdit {
|
|
self.outline.undo_SetKey(node: textField.node!, newKey: newKey, oldKey: oldKey!)
|
|
}
|
|
}
|
|
break
|
|
case 2:
|
|
switch ro!.type {
|
|
case .String:
|
|
self.outline.undo_SetValue(node: textField.node!,
|
|
newValue: textField.stringValue,
|
|
oldValue: ro!.value!)
|
|
break
|
|
case .Number:
|
|
let newNum = numberFromLocalizedString(string: textField.stringValue)
|
|
self.outline.undo_SetValue(node: textField.node!,
|
|
newValue: (newNum is PEReal) ? newNum as! PEReal : newNum as! PEReal,
|
|
oldValue: ro!.value!)
|
|
break
|
|
case .Data:
|
|
let res = isHexStringValid(string: textField.stringValue.lowercased())
|
|
if res != "HexSuccess" {
|
|
self.outline.wrongValue = true
|
|
NSSound.beep()
|
|
textField.window?.makeFirstResponder(textField)
|
|
let alert = PEAlert(in: self.outline.window)
|
|
alert.messageText = localizedInvalidValueMsgText
|
|
|
|
alert.informativeText = localizedInvalidValueInfoText + "\n\n(" + res + ")"
|
|
|
|
alert.addButton(withTitle: localizedKeepEditing)
|
|
alert.addButton(withTitle: localizedUndo)
|
|
|
|
alert.beginSheetModal(for: self.outline.window!, completionHandler: { (modalResponse) -> Void in
|
|
if modalResponse == NSApplication.ModalResponse.alertFirstButtonReturn {
|
|
self.outline.window?.makeFirstResponder(textField)
|
|
} else {
|
|
self.outline.reloadItem(parent, reloadChildren: true)
|
|
}
|
|
})
|
|
} else {
|
|
let str : String = textField.stringValue.lowercased()
|
|
self.outline.undo_SetValue(node: textField.node!,
|
|
newValue: (str.hexadecimal() ?? Data()),
|
|
oldValue: ro!.value!)
|
|
}
|
|
break
|
|
case .Date:
|
|
if (localizedStringToDateS(textField.stringValue) == nil) {
|
|
self.outline.wrongValue = true
|
|
NSSound.beep()
|
|
textField.window?.makeFirstResponder(textField)
|
|
let alert = PEAlert(in: (self.outline.window)!)
|
|
alert.messageText = localizedInvalidValueMsgText
|
|
|
|
alert.informativeText = localizedInvalidValueInfoText
|
|
|
|
alert.addButton(withTitle: localizedKeepEditing)
|
|
alert.addButton(withTitle: localizedUndo)
|
|
|
|
alert.beginSheetModal(for: self.outline.window!, completionHandler: { (modalResponse) -> Void in
|
|
if modalResponse == NSApplication.ModalResponse.alertFirstButtonReturn {
|
|
self.outline.window?.makeFirstResponder(textField)
|
|
} else {
|
|
self.outline.reloadItem(parent, reloadChildren: true)
|
|
}
|
|
})
|
|
} else {
|
|
let str : String = textField.stringValue
|
|
self.outline.undo_SetValue(node: textField.node!,
|
|
newValue: localizedStringToDate(str),
|
|
oldValue: ro!.value!)
|
|
}
|
|
break
|
|
default:
|
|
break
|
|
}
|
|
break
|
|
default:
|
|
break
|
|
}
|
|
self.outline.wrongValue = false
|
|
}
|
|
}
|
|
|
|
func outlineViewItemDidExpand(_ notification: Notification) {
|
|
let sr = self.outline.row(forItem: notification.userInfo?["NSObject"])
|
|
if sr >= 0 {
|
|
self.outline.selectRowIndexes(IndexSet(integer: sr), byExtendingSelection: false)
|
|
}
|
|
}
|
|
|
|
func outlineViewItemDidCollapse(_ notification: Notification) {
|
|
let sr = self.outline.row(forItem: notification.userInfo?["NSObject"])
|
|
if sr >= 0 {
|
|
self.outline.selectRowIndexes(IndexSet(integer: sr), byExtendingSelection: false)
|
|
}
|
|
}
|
|
|
|
func outlineViewSelectionDidChange(_ notification: Notification) {
|
|
self.highLightRow()
|
|
}
|
|
|
|
func highLightRow() {
|
|
self.outline.enumerateAvailableRowViews { _, row in
|
|
if let rv = self.outline.view(atColumn: 0,
|
|
row: row,
|
|
makeIfNecessary: false)?.superview as? PETableRowView {
|
|
rv.setBorderType()
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: determine if is a container
|
|
func outlineView(_ outlineView: NSOutlineView, isGroupItem item: Any) -> Bool {
|
|
return false
|
|
}
|
|
|
|
func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
|
|
if let n = item as? PENode {
|
|
return n.isExpandable
|
|
}
|
|
return false
|
|
}
|
|
|
|
func outlineView(_ outlineView: NSOutlineView, shouldExpandItem item: Any) -> Bool {
|
|
if self.outline.wrongValue {
|
|
NSSound.beep()
|
|
return false
|
|
}
|
|
if let n = item as? PENode {
|
|
return n.isExpandable
|
|
}
|
|
return false
|
|
}
|
|
|
|
|
|
func selectionShouldChange(in outlineView: NSOutlineView) -> Bool {
|
|
return self.outline.wrongValue ? false : true
|
|
}
|
|
|
|
// MARK: disclosure triangle
|
|
func outlineView(_ outlineView: NSOutlineView, shouldShowOutlineCellForItem item: Any) -> Bool {
|
|
return true
|
|
}
|
|
|
|
// MARK: OutlineView drag and drop
|
|
func outlineView(_ outlineView: NSOutlineView,
|
|
pasteboardWriterForItem item: Any) -> NSPasteboardWriting? {
|
|
self.pbTreeNode = nil
|
|
|
|
if (item as! PENode) != (self.outline.item(atRow: 0) as! PENode) {
|
|
self.pbTreeNode = (item as! PENode)
|
|
let pb = NSPasteboardItem()
|
|
let a = NSKeyedArchiver.archivedData(withRootObject: self.pbTreeNode!)
|
|
pb.setData(a, forType: kMyPBoardTypeData)
|
|
return pb
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func outlineView(_ outlineView: NSOutlineView,
|
|
draggingSession session: NSDraggingSession,
|
|
willBeginAt screenPoint: NSPoint,
|
|
forItems draggedItems: [Any]) {
|
|
|
|
}
|
|
func outlineView(_ outlineView: NSOutlineView,
|
|
draggingSession session: NSDraggingSession,
|
|
endedAt screenPoint: NSPoint,
|
|
operation: NSDragOperation) {
|
|
}
|
|
|
|
func outlineView(_ outlineView: NSOutlineView,
|
|
validateDrop info: NSDraggingInfo,
|
|
proposedItem item: Any?,
|
|
proposedChildIndex index: Int) -> NSDragOperation {
|
|
|
|
if let dragOutline = info.draggingSource as? PEOutlineView {
|
|
if dragOutline != self.outline {
|
|
return NSDragOperation.move
|
|
}
|
|
if dragOutline != self.outline || item == nil {
|
|
return (index > 0) ? NSDragOperation.move : []
|
|
}
|
|
}
|
|
|
|
// item is the destination
|
|
if let node = item as? PENode, let ol = outlineView as? PEOutlineView {
|
|
// refuse if target is the same
|
|
if node == ol.editorVC?.pbTreeNode { return [] }
|
|
// all fine
|
|
return NSDragOperation.move
|
|
}
|
|
return []
|
|
}
|
|
|
|
func outlineView(_ outlineView: NSOutlineView,
|
|
acceptDrop info: NSDraggingInfo,
|
|
item: Any?,
|
|
childIndex index: Int) -> Bool {
|
|
|
|
if !self.isEditable || index < 0 {
|
|
return false
|
|
}
|
|
|
|
// Accept drag & drop on self
|
|
if (info.draggingSource as? PEOutlineView) == self.outline {
|
|
if self.pbTreeNode == nil {
|
|
return false
|
|
}
|
|
if let parent = (item as? PENode) {
|
|
|
|
guard let fromIndex : Int = self.pbTreeNode?.indexPath.last else {
|
|
return false
|
|
}
|
|
let maxIndex = parent.count
|
|
|
|
if index > maxIndex {
|
|
return false
|
|
}
|
|
|
|
self.outline.undo_DragAndDrop(item: self.pbTreeNode!,
|
|
fromParent: self.pbTreeNode!.peparent!,
|
|
fromIndex: fromIndex,
|
|
toParent: parent,
|
|
toIndex: index)
|
|
self.pbTreeNode = nil
|
|
}
|
|
} else {
|
|
// Accept drag & drop from another document
|
|
if let data = info.draggingPasteboard.data(forType: kMyPBoardTypeData),
|
|
let node = NSKeyedUnarchiver.unarchiveObject(with: data) as? PENode,
|
|
let otherOutline = info.draggingSource as? PEOutlineView, let parent = item as? PENode {
|
|
|
|
gDeduplicateKeyInParent(parent:parent, newNode: node)
|
|
otherOutline.undo_Add(item: node,
|
|
inParent: parent,
|
|
indexChild: index,
|
|
target: outlineView as! PEOutlineView)
|
|
|
|
self.pbTreeNode = nil
|
|
return true
|
|
}
|
|
|
|
}
|
|
return false
|
|
}
|
|
|
|
// MARK: OutlineView save
|
|
func save() -> Data {
|
|
let root = self.outline.item(atRow: 0) as? PENode
|
|
root?.isRootNode = true // mark row 0 as root
|
|
let plist = gConvertPENodeToPlist(node: root)
|
|
root?.isRootNode = false // restore
|
|
return plist.data(using: .utf8)!
|
|
}
|
|
|
|
func convertToBinaryPlist() -> Data? {
|
|
let data = save()
|
|
var any : AnyObject
|
|
|
|
var fmt = PropertyListSerialization.PropertyListFormat.xml
|
|
do {
|
|
any = try PropertyListSerialization.propertyList(from: data,
|
|
options: .mutableContainersAndLeaves,
|
|
format: &fmt) as AnyObject
|
|
|
|
} catch {
|
|
return nil
|
|
}
|
|
|
|
do {
|
|
|
|
try any = PropertyListSerialization.data(fromPropertyList: any,
|
|
format: PropertyListSerialization.PropertyListFormat.binary,
|
|
options: PropertyListSerialization.WriteOptions(0)) as NSData
|
|
|
|
} catch {
|
|
return nil
|
|
}
|
|
|
|
if any is NSData {
|
|
return any as? Data
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
extension NSMenu {
|
|
func translate() {
|
|
for i in self.items {
|
|
i.title = i.title.locale
|
|
if i.submenu != nil {
|
|
i.submenu!.title = i.submenu!.title.locale
|
|
i.submenu!.translate()
|
|
}
|
|
}
|
|
}
|
|
}
|