vChewing-macOS/Source/Modules/WindowControllers/CtlClientListMgr.swift

300 lines
12 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import AppKit
import MainAssembly
import UniformTypeIdentifiers
class CtlClientListMgr: NSWindowController, NSTableViewDelegate, NSTableViewDataSource {
@IBOutlet var tblClients: NSTableView!
@IBOutlet var btnRemoveClient: NSButton!
@IBOutlet var btnAddClient: NSButton!
@IBOutlet var lblClientMgrWindow: NSTextField!
public static var shared: CtlClientListMgr?
static func show() {
if shared == nil { shared = CtlClientListMgr(windowNibName: "frmClientListMgr") }
guard let shared = shared, let sharedWindow = shared.window else { return }
sharedWindow.setPosition(vertical: .center, horizontal: .right, padding: 20)
sharedWindow.orderFrontRegardless() //
sharedWindow.level = .statusBar
sharedWindow.titlebarAppearsTransparent = true
shared.showWindow(shared)
NSApp.popup()
}
override func windowDidLoad() {
super.windowDidLoad()
window?.setPosition(vertical: .center, horizontal: .right, padding: 20)
localize()
tblClients.delegate = self
tblClients.registerForDraggedTypes([.fileURL])
tblClients.allowsMultipleSelection = true
tblClients.dataSource = self
tblClients.action = #selector(onItemClicked(_:))
tblClients.target = self
tblClients.reloadData()
}
}
// MARK: - UserDefaults Handlers
extension CtlClientListMgr {
public static var clientsList: [String] { PrefMgr.shared.clientsIMKTextInputIncapable.keys.sorted() }
public static func removeClient(at index: Int) {
guard index < Self.clientsList.count else { return }
let key = Self.clientsList[index]
var dict = PrefMgr.shared.clientsIMKTextInputIncapable
dict[key] = nil
PrefMgr.shared.clientsIMKTextInputIncapable = dict
}
}
// MARK: - Implementations
extension CtlClientListMgr {
func numberOfRows(in _: NSTableView) -> Int {
Self.clientsList.count
}
@IBAction func btnAddClientClicked(_: Any) {
guard let window = window else { return }
let alert = NSAlert()
alert.messageText = NSLocalizedString(
"Please enter the client app bundle identifier(s) you want to register.", comment: ""
)
alert.informativeText = NSLocalizedString(
"One record per line. Use Option+Enter to break lines.\nBlank lines will be dismissed.", comment: ""
)
alert.addButton(withTitle: NSLocalizedString("OK", comment: ""))
alert.addButton(withTitle: NSLocalizedString("Just Select", comment: "") + "")
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: ""))
let maxFloat = Double(Float.greatestFiniteMagnitude)
let scrollview = NSScrollView(frame: NSRect(x: 0, y: 0, width: 370, height: 200))
let contentSize = scrollview.contentSize
scrollview.borderType = .noBorder
scrollview.hasVerticalScroller = true
scrollview.hasHorizontalScroller = true
scrollview.horizontalScroller?.scrollerStyle = .legacy
scrollview.verticalScroller?.scrollerStyle = .legacy
scrollview.autoresizingMask = [.width, .height]
let theTextView = NSTextView(frame: NSRect(x: 0, y: 0, width: contentSize.width, height: contentSize.height))
theTextView.minSize = NSSize(width: 0.0, height: contentSize.height)
theTextView.maxSize = NSSize(width: maxFloat, height: maxFloat)
theTextView.isVerticallyResizable = true
theTextView.isHorizontallyResizable = false
theTextView.autoresizingMask = .width
theTextView.textContainer?.containerSize = NSSize(width: contentSize.width, height: maxFloat)
theTextView.textContainer?.widthTracksTextView = true
scrollview.documentView = theTextView
theTextView.enclosingScrollView?.hasHorizontalScroller = true
theTextView.isHorizontallyResizable = true
theTextView.autoresizingMask = [.width, .height]
theTextView.textContainer?.containerSize = NSSize(width: maxFloat, height: maxFloat)
theTextView.textContainer?.widthTracksTextView = false
//
theTextView.textContainer?.textView?.string = {
let recentClients = SessionCtl.recentClientBundleIdentifiers.keys.compactMap {
PrefMgr.shared.clientsIMKTextInputIncapable.keys.contains($0) ? nil : $0
}
return recentClients.sorted().joined(separator: "\n")
}()
alert.accessoryView = scrollview
alert.beginSheetModal(for: window) { result in
resultCheck: switch result {
case .alertFirstButtonReturn, .alertSecondButtonReturn:
theTextView.textContainer?.textView?.string.components(separatedBy: "\n").filter { !$0.isEmpty }.forEach {
self.applyNewValue($0, highMitigation: result == .alertFirstButtonReturn)
}
if result == .alertFirstButtonReturn { break }
if #unavailable(macOS 10.13) {
window.callAlert(title: "Please drag the apps into the Client Manager window from Finder.".localized)
break resultCheck
}
let dlgOpenPath = NSOpenPanel()
dlgOpenPath.title = NSLocalizedString(
"Choose the target application bundle.", comment: ""
)
dlgOpenPath.showsResizeIndicator = true
dlgOpenPath.allowsMultipleSelection = true
dlgOpenPath.allowedContentTypes = [UTType.applicationBundle]
dlgOpenPath.allowsOtherFileTypes = false
dlgOpenPath.showsHiddenFiles = true
dlgOpenPath.canChooseFiles = true
dlgOpenPath.canChooseDirectories = false
dlgOpenPath.beginSheetModal(for: window) { result in
switch result {
case .OK:
for url in dlgOpenPath.urls {
let title = NSLocalizedString(
"The selected item is either not a valid macOS application bundle or not having a valid app bundle identifier.",
comment: ""
)
let text = url.path + "\n\n" + NSLocalizedString("Please try again.", comment: "")
guard let bundle = Bundle(url: url) else {
self.window?.callAlert(title: title, text: text)
return
}
guard let identifier = bundle.bundleIdentifier else {
self.window?.callAlert(title: title, text: text)
return
}
let isIdentifierAlreadyRegistered = Self.clientsList.contains(identifier)
let alert2 = NSAlert()
alert2.messageText =
"Do you want to enable the popup composition buffer for this client?".localized
alert2.informativeText = "\(identifier)\n\n"
+ "Some client apps may have different compatibility issues in IMKTextInput implementation.".localized
alert2.addButton(withTitle: "Yes".localized)
alert2.addButton(withTitle: "No".localized)
alert2.beginSheetModal(for: window) { result2 in
let oldValue = PrefMgr.shared.clientsIMKTextInputIncapable[identifier]
let newValue = result2 == .alertFirstButtonReturn
if !(isIdentifierAlreadyRegistered && oldValue == newValue) {
self.applyNewValue(identifier, highMitigation: newValue)
}
}
}
default: return
}
}
default: return
}
}
}
private func applyNewValue(_ newValue: String, highMitigation mitigation: Bool = true) {
guard !newValue.isEmpty else { return }
var dict = PrefMgr.shared.clientsIMKTextInputIncapable
dict[newValue] = mitigation
PrefMgr.shared.clientsIMKTextInputIncapable = dict
tblClients.reloadData()
btnRemoveClient.isEnabled = (0 ..< Self.clientsList.count).contains(
tblClients.selectedRow)
}
@IBAction func btnRemoveClientClicked(_: Any) {
guard let minIndexSelected = tblClients.selectedRowIndexes.min() else { return }
if minIndexSelected >= Self.clientsList.count { return }
if minIndexSelected < 0 { return }
var isLastRow = false
tblClients.selectedRowIndexes.sorted().reversed().forEach { index in
isLastRow = {
if Self.clientsList.count < 2 { return false }
return minIndexSelected == Self.clientsList.count - 1
}()
if index < Self.clientsList.count {
Self.removeClient(at: index)
}
}
if isLastRow {
tblClients.selectRowIndexes(.init(arrayLiteral: minIndexSelected - 1), byExtendingSelection: false)
}
tblClients.reloadData()
btnRemoveClient.isEnabled = (0 ..< Self.clientsList.count).contains(minIndexSelected)
}
@objc func onItemClicked(_: Any!) {
guard tblClients.clickedColumn == 0 else { return }
PrefMgr.shared.clientsIMKTextInputIncapable[Self.clientsList[tblClients.clickedRow]]?.toggle()
tblClients.reloadData()
}
func tableView(_: NSTableView, shouldEdit _: NSTableColumn?, row _: Int) -> Bool {
false
}
func tableView(_: NSTableView, objectValueFor column: NSTableColumn?, row: Int) -> Any? {
defer {
self.btnRemoveClient.isEnabled = (0 ..< Self.clientsList.count).contains(
self.tblClients.selectedRow)
}
guard row < Self.clientsList.count else { return "" }
if let column = column {
let colName = column.identifier.rawValue
switch colName {
case "colPCBEnabled":
let tick = PrefMgr.shared.clientsIMKTextInputIncapable[Self.clientsList[row]] ?? true
return tick
case "colClient": return Self.clientsList[row]
default: return ""
}
}
return Self.clientsList[row]
}
/// NSDraggingInfo URL App Bundle
/// - Parameters:
/// - info: NSDraggingInfo
/// - onError: 滿 lambda expression
/// - handler: 滿 lambda expression URL
private func validatePasteboardForAppBundles(
neta info: NSDraggingInfo, onError: @escaping () -> Void?, handler: (([URL]) -> Void)? = nil
) {
let board = info.draggingPasteboard
let type = UTType.applicationBundle
let options: [NSPasteboard.ReadingOptionKey: Any] = [
.urlReadingFileURLsOnly: true,
.urlReadingContentsConformToTypes: [type],
]
guard let urls = board.readObjects(forClasses: [NSURL.self], options: options) as? [URL], !urls.isEmpty else {
onError()
return
}
if let handler = handler {
handler(urls)
}
}
func tableView(
_: NSTableView, validateDrop info: NSDraggingInfo, proposedRow _: Int,
proposedDropOperation _: NSTableView.DropOperation
) -> NSDragOperation {
var result = NSDragOperation.copy
validatePasteboardForAppBundles(
neta: info, onError: { result = .init(rawValue: 0) } // NSDragOperationNone
)
return result
}
func tableView(
_: NSTableView, acceptDrop info: NSDraggingInfo,
row _: Int, dropOperation _: NSTableView.DropOperation
) -> Bool {
var result = true
validatePasteboardForAppBundles(
neta: info, onError: { result = false } // NSDragOperationNone
) { theURLs in
var dealt = false
theURLs.forEach { url in
guard let bundle = Bundle(url: url), let bundleID = bundle.bundleIdentifier else { return }
self.applyNewValue(bundleID, highMitigation: true)
dealt = true
}
result = dealt
}
defer { if result { tblClients.reloadData() } }
return result
}
private func localize() {
guard let window = window else { return }
window.title = NSLocalizedString("Client Manager", comment: "")
lblClientMgrWindow.stringValue = NSLocalizedString(
"Please manage the list of those clients here which are: 1) IMKTextInput-incompatible; 2) suspected from abusing the contents of the inline composition buffer. A client listed here, if checked, will use popup composition buffer with maximum 20 reading counts holdable.",
comment: ""
)
btnAddClient.title = NSLocalizedString("Add Client", comment: "")
btnRemoveClient.title = NSLocalizedString("Remove Selected", comment: "")
}
}