CloverBootloader/CloverApp/Clover/Plist Editor/PE_Undo.swift
vectorsigma72 1f53df2339 Plist Editor is now available in Yosemite
Redone Find and Replace views that now are separate views with fixed constraints.
Added support for OS X 10.10
2020-04-24 14:00:50 +02:00

604 lines
25 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
// MARK: Undo and redo support
@available(OSX 10.10, *)
extension PEOutlineView {
// MARK: Undo Manager object
/// Prepares PEOutlineView for the undo operation
func prepareUndoObj() -> AnyObject {
// nil is not expecte here. Be happy if throw an exception
return self.undoManager?.prepare(withInvocationTarget: self) as AnyObject
}
// MARK: Undo drag Item
/// Undo operation when an Item is dragged here an there in PEOutlineView
@objc func undo_DragAndDrop(item: PENode,
fromParent: PENode,
fromIndex: Int,
toParent: PENode,
toIndex: Int) {
self.prepareUndoObj().undo_DragAndDrop(item: item,
fromParent: toParent,
fromIndex: toIndex,
toParent: fromParent,
toIndex: fromIndex)
self.undoManager?.setActionName(localizedUndoRedoMoveItem)
let toParentCount = toParent.mutableChildren.count
let new = item.copy() as! PENode
if fromParent == toParent {
var mutatingToIndex = toIndex
if fromIndex < toIndex {
mutatingToIndex -= 1
}
fromParent.mutableChildren.removeObject(at: fromIndex)
fromParent.mutableChildren.insert(new, at: mutatingToIndex)
DispatchQueue.main.async {
self.reloadItem(fromParent, reloadChildren: true)
}
} else {
self.editorVC?.asyncExpand(item: toParent, expandParentOnly: false)
// drag and past from to another parent
if toParentCount == 0 {
toParent.mutableChildren.add(new)
DispatchQueue.main.async {
self.animator().moveItem(at: fromIndex,
inParent: fromParent,
to: toIndex,
inParent: toParent)
}
} else {
DispatchQueue.main.async {
gDeduplicateKeyInParent(parent:toParent, newNode: new)
toParent.mutableChildren.insert(new, at: toIndex)
fromParent.mutableChildren.removeObject(at: fromIndex)
self.animator().moveItem(at: fromIndex,
inParent: fromParent,
to: toIndex,
inParent: toParent)
self.reloadItem(toParent, reloadChildren: true)
let row = self.row(forItem: new)
if row >= 0 {
self.scrollRowToVisible(row)
self.selectRowIndexes(IndexSet(integer: row), byExtendingSelection: false)
}
}
}
}
// select the new dragged item
DispatchQueue.main.async {
let toBeSelected = self.row(forItem: new)
if toBeSelected >= 0 {
self.scrollRowToVisible(toBeSelected)
self.selectRowIndexes(IndexSet(integer: toBeSelected), byExtendingSelection: false)
}
}
}
// MARK: Undo replace
/// Undo operation for Search & Replace in PEOutlineView
@objc func undo_FindAndReplace(nodes: NSArray, oldTreeData: NSArray, newTreeData: NSArray) {
self.prepareUndoObj().undo_FindAndReplace(nodes: nodes,
oldTreeData: newTreeData,
newTreeData: oldTreeData)
// select and expand, one by one, rows in oldNodes
self.undoManager?.setActionName(localizedReplace.lowercased())
var index : Int = 0
for n in nodes {
let node = n as! PENode
self.editorVC?.asyncExpand(item: node, expandParentOnly: true)
let newData = newTreeData.object(at: index) as? TagData
node.tagdata?.key = (newData?.key)!
node.tagdata?.value = newData?.value
DispatchQueue.main.async {
let row = self.row(forItem: node)
if row >= 0 {
self.scrollRowToVisible(row)
self.reloadData(forRowIndexes: IndexSet(integer: row), columnIndexes: IndexSet(integer: 0))
self.reloadData(forRowIndexes: IndexSet(integer: row), columnIndexes: IndexSet(integer: 2))
self.selectRowIndexes(IndexSet(integer: row), byExtendingSelection: false)
}
}
index+=1
}
self.editorVC?.performDelayedSearch()
}
// MARK: Undo set key string
/// Undo operation for a key editing (Key column) in PEOutlineView
@objc func undo_SetKey(node: PENode, newKey: String, oldKey: String) {
// we are setting the key for a specific row and ther's no need to reload the entire group
self.prepareUndoObj().undo_SetKey(node: node,
newKey: oldKey,
oldKey: newKey)
self.undoManager?.setActionName(localizedUndoRedoTyping)
node.tagdata?.key = newKey
self.editorVC?.asyncExpand(item: node, expandParentOnly: true)
//selecting the involved row, but we need to reload its view? isn't that already happened?
DispatchQueue.main.async {
self.reloadItem(node.parent, reloadChildren: false)
let row = self.row(forItem: node)
if row >= 0 {
self.scrollRowToVisible(row)
self.reloadData(forRowIndexes: IndexSet(integer: row), columnIndexes: IndexSet(integer: 0))
self.selectRowIndexes(IndexSet(integer: row), byExtendingSelection: false)
}
}
}
// MARK: Undo Replace Existing key
/// Undo operation for replacing a duplicate key-value pairs in PEOutlineView
@objc func undo_ReplaceExisting(item: PENode,
inParent: PENode,
indexChild: Int,
editedNode: PENode,
oldKey: String,
newKey: String) {
self.prepareUndoObj().redo_ReplaceExisting(item: item,
inParent: inParent,
indexChild: indexChild,
editedNode: editedNode,
oldKey: newKey,
newKey: oldKey)
self.undoManager?.setActionName(localizedUndoRedoReplaceDuplicateKey)
// expand the node if isn't. (an item can be added to a not expanded group??)
self.editorVC?.asyncExpand(item: item, expandParentOnly: true)
inParent.mutableChildren.removeObject(at: indexChild)
DispatchQueue.main.async {
self.removeItems(at: IndexSet(integer: indexChild),
inParent: inParent,
withAnimation: [])
editedNode.tagdata?.key = newKey
// keep trak of the item because must be selected after the parent gets reloaded
let row = self.row(forItem: editedNode)
//reload the parent. In this case the parent it's the same
//self.outline.reloadItem(inParent, reloadChildren: true)
if row >= 0 {
self.scrollRowToVisible(row)
self.selectRowIndexes(IndexSet(integer: row), byExtendingSelection: false)
}
}
}
// MARK: Redo Replace Existing key
/// Reversed undo operation (Redo) for replacing a duplicate key-value pairs in PEOutlineView
@objc func redo_ReplaceExisting(item: PENode,
inParent: PENode,
indexChild: Int,
editedNode: PENode,
oldKey: String,
newKey: String) {
self.prepareUndoObj().undo_ReplaceExisting(item: item,
inParent: inParent,
indexChild: indexChild,
editedNode: editedNode,
oldKey: newKey,
newKey: oldKey)
self.undoManager?.setActionName(localizedUndoRedoReplaceDuplicateKey)
// expand the node if isn't. (an item can be added to a not expanded group??)
self.editorVC?.asyncExpand(item: item, expandParentOnly: true)
inParent.mutableChildren.insert(item, at: indexChild)
editedNode.tagdata?.key = newKey
DispatchQueue.main.async {
self.insertItems(at: IndexSet(integer: indexChild),
inParent: inParent,
withAnimation: [])
// keep trak of the item because must be selected after the parent gets reloaded
let row = self.row(forItem: editedNode)
//reload the parent. In this case the parent it's the same
self.reloadItem(inParent, reloadChildren: true)
if row >= 0 {
self.scrollRowToVisible(row)
self.selectRowIndexes(IndexSet(integer: row), byExtendingSelection: false)
}
}
}
// MARK: Undo set new value
/// Undo operation (Redo) editing a value (Value Column) in PEOutlineView.
/// See also undoableSetBoolValue(node: , sender:, originalValue:, actualValue:)
@objc func undo_SetValue(node: PENode, newValue: Any, oldValue: Any) {
let row = self.row(forItem: node)
self.prepareUndoObj().undo_SetValue(node: node,
newValue: oldValue,
oldValue: newValue)
self.editorVC?.asyncExpand(item: node, expandParentOnly: true)
self.undoManager?.setActionName(localizedUndoRedoTyping)
node.tagdata?.value = newValue
DispatchQueue.main.async {
self.reloadItem(node.parent, reloadChildren: false)
if row >= 0 {
self.scrollRowToVisible(row)
self.reloadData(forRowIndexes: IndexSet(integer: row), columnIndexes: IndexSet(integer: 2))
self.selectRowIndexes(IndexSet(integer: row), byExtendingSelection: false)
}
}
}
// MARK: Undo change to Bool tag
/// Undo operation (Redo) editing a Bool value (Value Column) in PEOutlineView
@objc func undo_SetBoolValue(node: PENode,
sender: PEPopUpButton,
originalValue: Bool,
actualValue: Bool) {
self.prepareUndoObj().undo_SetBoolValue(node: node,
sender: sender,
originalValue: actualValue,
actualValue: originalValue)
self.undoManager?.setActionName(localizedUndoRedoChangeBoolValue)
node.tagdata?.value = actualValue
// root node cannot have parent and is always visible
if (node.parent != nil) {
self.editorVC?.asyncExpand(item: node, expandParentOnly: true)
}
DispatchQueue.main.async {
let row = self.row(forItem: node)
if row >= 0 {
self.scrollRowToVisible(row)
self.selectRowIndexes(IndexSet(integer: row), byExtendingSelection: false)
}
sender.animator().selectItem(withTitle:
actualValue ? localizedYes : localizedNo)
}
}
// MARK: Undo change tag
/// Undo operation changing a tag (Tag Column) in PEOutlineView.
@objc func undo_ChangeType(node: PENode,
newData: TagData,
oldData: TagData,
newChilds: [Any]?,
oldChilds: [Any]?,
sender: PEPopUpButton) {
self.prepareUndoObj().undo_ChangeType(node: node,
newData: oldData,
oldData: newData,
newChilds: oldChilds,
oldChilds: newChilds,
sender: sender)
self.undoManager?.setActionName(localizedUndoRedoChangeType)
node.representedObject = newData
node.mutableChildren.removeAllObjects()
if let childrens = newChilds {
node.mutableChildren.addObjects(from: childrens)
}
// root node cannot have parent and is always visible
if (node.parent != nil) {
self.editorVC?.asyncExpand(item: node, expandParentOnly: true)
}
DispatchQueue.main.async {
self.reloadItem(node, reloadChildren: true)
let row = self.row(forItem: node)
if row >= 0 {
self.scrollRowToVisible(row)
self.selectRowIndexes(IndexSet(integer: row), byExtendingSelection: false)
}
}
}
// MARK: Undo change contenitor (Dictionary to Array and viceversa)
/// Undo operation changing a Dictionary to Array and viceversa (Tag Column) in PEOutlineView.
@objc func undo_ChangeContenitor(node: PENode,
sender: PEPopUpButton,
originalType: String,
actualType: String,
oldKeys: PEArray,
newKeys: PEArray) {
/*
We cannot leave old keys, but instead we should:
- Dictionary, must have localized "New Item -x" for each children
- Array, must be localized "Item x" for each children
*/
self.prepareUndoObj().undo_ChangeContenitor(node: node,
sender: sender,
originalType: actualType,
actualType: originalType,
oldKeys: newKeys,
newKeys: oldKeys)
self.undoManager?.setActionName(localizedUndoRedoChangeType)
// root node cannot have parent and is always visible
if (node.parent != nil) {
self.editorVC?.asyncExpand(item: node, expandParentOnly: false)
}
let actualTag = gPlistTag(from: actualType)
if actualTag == .Dictionary {
node.tagdata?.value = nil
node.tagdata?.type = .Dictionary
} else if actualTag == .Array {
node.tagdata?.value = nil
node.tagdata?.type = .Array
}
var index : Int = 0
for n in node.mutableChildren {
let modded : PENode = n as! PENode
modded.tagdata?.key = newKeys[index] as! String
index+=1
}
DispatchQueue.main.async {
sender.animator().selectItem(withTitle: gPlistTagStr(tag: actualTag).locale)
}
self.editorVC?.asyncExpand(item: node, expandParentOnly: true)
DispatchQueue.main.async {
let row = self.row(forItem: node)
if self.isItemExpanded(node) {
self.reloadItem(node, reloadChildren: true)
} else {
self.reloadItem(node, reloadChildren: false)
}
if row >= 0 {
self.scrollRowToVisible(row)
self.selectRowIndexes(IndexSet(integer: row), byExtendingSelection: false)
}
}
}
// MARK: Undo Paste
/// Undo operation for "Paste" in PEOutlineView.
/// See redo_PasteItem(item: , inParent: , indexChild: ) for the reversed operation.
@objc func undo_Paste(item: PENode, inParent: PENode, indexChild: Int) {
self.prepareUndoObj().redo_Paste(item: item,
inParent: inParent,
indexChild: indexChild)
self.editorVC?.asyncExpand(item: item, expandParentOnly: true)
self.undoManager?.setActionName(localizedUndoRedoPasteItem)
inParent.mutableChildren.insert(item, at: indexChild)
DispatchQueue.main.async {
self.insertItems(at: IndexSet(integer: indexChild), inParent: inParent, withAnimation: [])
// basically we are adding a new Item. Reload the parent and the cildrens..and select new item
self.reloadItem(inParent, reloadChildren: true)
// find new item that should be there now..
let row = self.row(forItem: item)
if row >= 0 {
self.scrollRowToVisible(row)
self.selectRowIndexes(IndexSet(integer: row), byExtendingSelection: false)
}
}
}
// MARK: Undo Paste (reverse)
/// Redo operation for "Paste" in PEOutlineView.
/// See undo_PasteItem(item: , inParent: , indexChild: ) for the reversed operation.
@objc func redo_Paste(item: PENode, inParent: PENode, indexChild: Int) {
self.prepareUndoObj().undo_Paste(item: item,
inParent: inParent,
indexChild: indexChild)
self.undoManager?.setActionName(localizedUndoRedoPasteItem)
// expand the node if isn't
self.editorVC?.asyncExpand(item: item, expandParentOnly: true)
// find the row. After deleting it, it will be the row to select again (if exist, othewise select last row in the outline)
DispatchQueue.main.async {
let row = self.row(forItem: item)
inParent.mutableChildren.removeObject(at: indexChild)
self.removeItems(at: IndexSet(integer: indexChild), inParent: inParent, withAnimation: [])
// basically we are removing a new Item just added. Reload the parent and the cildrens..
self.reloadItem(inParent, reloadChildren: true)
//..and select the next row
if row >= 0 {
if (self.item(atRow: row) != nil) {
self.selectRowIndexes(IndexSet(integer: row), byExtendingSelection: false)
} else {
// row it's out of bounds.. select last in the outline
let lastRow = self.numberOfRows - 1
if lastRow >= 0 {
self.scrollRowToVisible(lastRow)
self.selectRowIndexes(IndexSet(integer: lastRow), byExtendingSelection: false)
}
}
}
}
}
// MARK: Undo Cut
/// Undo operation for "Cut" in PEOutlineView.
/// See redo_Cut(item: , inParent: , indexChild: ) for the reversed operation.
@objc func undo_Cut(item: PENode, inParent: PENode, indexChild: Int) {
// find the row. After deleting it, it will be the row to select again (if exist, othewise select last row in the outline)
self.prepareUndoObj().redo_Cut(item: item,
inParent: inParent,
indexChild: indexChild)
self.editorVC?.asyncExpand(item: item, expandParentOnly: true)
self.undoManager?.setActionName(localizedUndoRedoCutItem)
inParent.mutableChildren.removeObject(at: indexChild)
self.removeItems(at: IndexSet(integer: indexChild), inParent: inParent, withAnimation: [])
// basically we are removing a new Item just added. Reload the parent and the cildrens..
DispatchQueue.main.async {
self.reloadItem(inParent, reloadChildren: true)
let row = self.row(forItem: item)
//..and select the next row
if row >= 0 {
if (self.item(atRow: row) != nil) {
self.selectRowIndexes(IndexSet(integer: row), byExtendingSelection: false)
} else {
// row it's out of bounds.. select last in the outline
let lastRow = self.numberOfRows - 1
if lastRow >= 0 {
self.scrollRowToVisible(lastRow)
self.selectRowIndexes(IndexSet(integer: lastRow), byExtendingSelection: false)
}
}
}
}
}
// MARK: Undo Cut (reverse)
/// Redo operation for "Cut" in PEOutlineView.
/// See undo_Cut(item: , inParent: , indexChild: ) for the reversed operation.
@objc func redo_Cut(item: PENode, inParent: PENode, indexChild: Int) {
self.prepareUndoObj().undo_Cut(item: item,
inParent: inParent,
indexChild: indexChild)
self.undoManager?.setActionName(localizedUndoRedoCutItem)
// expand the node if isn't
self.editorVC?.asyncExpand(item: item, expandParentOnly: true)
// we are adding an item..
inParent.mutableChildren.insert(item, at: indexChild)
DispatchQueue.main.async {
self.insertItems(at: IndexSet(integer: indexChild), inParent: inParent, withAnimation: [])
// select the new one after reloading the group
self.reloadItem(inParent, reloadChildren: true)
let row = self.row(forItem: item)
if row >= 0 {
self.scrollRowToVisible(row)
self.selectRowIndexes(IndexSet(integer: row), byExtendingSelection: false)
}
}
}
// MARK: Undo add item
/// Undo operation adding New items in PEOutlineView.
/// See undo_Remove(item: , inParent: , indexChild: ) for the reverse operation.
@objc func undo_Add(item: PENode,
inParent: PENode,
indexChild: Int,
target outlineView: PEOutlineView) {
outlineView.prepareUndoObj().undo_Remove(item: item,
inParent: inParent,
indexChild: indexChild,
target: outlineView)
outlineView.undoManager?.setActionName(localizedUndoRedoAddNewItem)
outlineView.editorVC?.asyncExpand(item: item, expandParentOnly: true)
inParent.mutableChildren.insert(item, at: indexChild)
DispatchQueue.main.async {
outlineView.insertItems(at: IndexSet(integer: indexChild), inParent: inParent, withAnimation: [])
// select the new one after reloading the group
outlineView.reloadItem(inParent, reloadChildren: true)
let row = outlineView.row(forItem: item)
if row >= 0 {
outlineView.scrollRowToVisible(row)
outlineView.selectRowIndexes(IndexSet(integer: row), byExtendingSelection: false)
}
}
}
// MARK: Redo remove Item
/// Redo operation adding New items in PEOutlineView.
/// See undo_Add(item: , inParent: , indexChild: ) for the reverse operation.
@objc func undo_Remove(item: PENode,
inParent: PENode,
indexChild: Int,
target outlineView: PEOutlineView) {
// find the row. After deleting it, it will be the row to select again (if exist, othewise select last row in the outline)
let row = outlineView.row(forItem: item)
outlineView.prepareUndoObj().undo_Add(item: item,
inParent: inParent,
indexChild: indexChild,
target: outlineView)
outlineView.undoManager?.setActionName(localizedUndoRedoRemoveItem)
// expand the node if isn't
outlineView.editorVC?.asyncExpand(item: item, expandParentOnly: true)
inParent.mutableChildren.removeObject(at: indexChild)
DispatchQueue.main.async {
outlineView.removeItems(at: IndexSet(integer: indexChild), inParent: inParent, withAnimation: [])
// Reload the parent and the cildrens..
outlineView.reloadItem(inParent, reloadChildren: true)
//..and select the next row
if row >= 0 {
outlineView.scrollRowToVisible(row)
if (outlineView.item(atRow: row) != nil) {
outlineView.selectRowIndexes(IndexSet(integer: row), byExtendingSelection: false)
} else {
// row it's out of bounds.. select last in the outline
let lastRow = outlineView.numberOfRows - 1
if lastRow >= 0 {
outlineView.selectRowIndexes(IndexSet(integer: lastRow), byExtendingSelection: false)
}
}
}
}
}
}