vChewing-macOS/Installer/AppDelegate.swift

182 lines
6.7 KiB
Swift

// (c) 2011 and onwards The OpenVanilla Project (MIT License).
// All possible vChewing-specific modifications are of:
// (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 Cocoa
import IMKUtils
import InputMethodKit
import SwiftExtension
public let kTargetBin = "vChewing"
public let kTargetBinPhraseEditor = "vChewingPhraseEditor"
public let kTargetType = "app"
public let kTargetBundle = "vChewing.app"
public let kTargetBundleWithComponents = "Library/Input%20Methods/vChewing.app"
public let realHomeDir = URL(
fileURLWithFileSystemRepresentation: getpwuid(getuid()).pointee.pw_dir, isDirectory: true, relativeTo: nil
)
public let urlDestinationPartial = realHomeDir.appendingPathComponent("Library/Input Methods")
public let urlTargetPartial = realHomeDir.appendingPathComponent(kTargetBundleWithComponents)
public let urlTargetFullBinPartial = urlTargetPartial.appendingPathComponent("Contents/MacOS")
.appendingPathComponent(kTargetBin)
public let kDestinationPartial = urlDestinationPartial.path
public let kTargetPartialPath = urlTargetPartial.path
public let kTargetFullBinPartialPath = urlTargetFullBinPartial.path
public let kTranslocationRemovalTickInterval: TimeInterval = 0.5
public let kTranslocationRemovalDeadline: TimeInterval = 60.0
@NSApplicationMain
@objc(AppDelegate)
class AppDelegate: NSWindowController, NSApplicationDelegate {
@IBOutlet var installButton: NSButton!
@IBOutlet var cancelButton: NSButton!
@IBOutlet var progressSheet: NSWindow!
@IBOutlet var progressIndicator: NSProgressIndicator!
@IBOutlet var appVersionLabel: NSTextField!
@IBOutlet var appCopyrightLabel: NSTextField!
@IBOutlet var appEULAContent: NSTextView!
var installingVersion = ""
var translocationRemovalStartTime: Date?
var currentVersionNumber: Int = 0
let imeURLInstalled = realHomeDir.appendingPathComponent("Library/Input Methods/vChewing.app")
var allRegisteredInstancesOfThisInputMethod: [TISInputSource] {
guard let components = Bundle(url: imeURLInstalled)?.infoDictionary?["ComponentInputModeDict"] as? [String: Any],
let tsInputModeListKey = components["tsInputModeListKey"] as? [String: Any]
else {
return []
}
return tsInputModeListKey.keys.compactMap { TISInputSource.generate(from: $0) }
}
func runAlertPanel(title: String, message: String, buttonTitle: String) {
let alert = NSAlert()
alert.alertStyle = .informational
alert.messageText = title
alert.informativeText = message
alert.addButton(withTitle: buttonTitle)
alert.runModal()
}
func applicationDidFinishLaunching(_: Notification) {
guard
let window = window,
let cell = installButton.cell as? NSButtonCell,
let installingVersion = Bundle.main.infoDictionary?[kCFBundleVersionKey as String] as? String,
let versionString = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String,
let copyrightLabel = Bundle.main.localizedInfoDictionary?["NSHumanReadableCopyright"] as? String,
let eulaContent = Bundle.main.localizedInfoDictionary?["CFEULAContent"] as? String,
let eulaContentUpstream = Bundle.main.infoDictionary?["CFUpstreamEULAContent"] as? String
else {
NSSound.beep()
NSLog("The vChewing App Installer failed its initial guard-let process on appDidFinishLaunching().")
return
}
self.installingVersion = installingVersion
cancelButton.nextKeyView = installButton
installButton.nextKeyView = cancelButton
window.defaultButtonCell = cell
appCopyrightLabel.stringValue = copyrightLabel
appEULAContent.string = eulaContent + "\n" + eulaContentUpstream
appVersionLabel.stringValue = "\(versionString) Build \(installingVersion)"
window.title = "\(window.title) (v\(versionString), Build \(installingVersion))"
window.standardWindowButton(.closeButton)?.isHidden = true
window.standardWindowButton(.miniaturizeButton)?.isHidden = true
window.standardWindowButton(.zoomButton)?.isHidden = true
window.titlebarAppearsTransparent = true
if FileManager.default.fileExists(atPath: kTargetPartialPath) {
let currentBundle = Bundle(path: kTargetPartialPath)
let shortVersion = currentBundle?.infoDictionary?["CFBundleShortVersionString"] as? String
let currentVersion = currentBundle?.infoDictionary?[kCFBundleVersionKey as String] as? String
currentVersionNumber = (currentVersion as NSString?)?.integerValue ?? 0
if shortVersion != nil, let currentVersion = currentVersion,
currentVersion.compare(installingVersion, options: .numeric) == .orderedAscending
{
// Upgrading confirmed.
installButton.title = NSLocalizedString("Upgrade", comment: "")
}
}
window.center()
window.orderFront(self)
NSApp.activate(ignoringOtherApps: true)
}
@IBAction func agreeAndInstallAction(_: AnyObject) {
cancelButton.isEnabled = false
installButton.isEnabled = false
removeThenInstallInputMethod()
}
@objc func timerTick(_ timer: Timer) {
guard let window = window else { return }
let elapsed = Date().timeIntervalSince(translocationRemovalStartTime ?? Date())
if elapsed >= kTranslocationRemovalDeadline {
timer.invalidate()
window.endSheet(progressSheet, returnCode: .cancel)
} else if Reloc.isAppBundleTranslocated(atPath: kTargetPartialPath) == false {
progressIndicator.doubleValue = 1.0
timer.invalidate()
window.endSheet(progressSheet, returnCode: .continue)
}
}
func endAppWithDelay() {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) {
NSApp.terminate(self)
}
}
@IBAction func cancelAction(_: AnyObject) {
NSApp.terminate(self)
}
func windowWillClose(_: Notification) {
NSApp.terminate(self)
}
func shell(_ command: String) throws -> String {
let task = Process()
let pipe = Pipe()
task.standardOutput = pipe
task.standardError = pipe
task.arguments = ["-c", command]
if #available(macOS 10.13, *) {
task.executableURL = URL(fileURLWithPath: "/bin/zsh")
} else {
task.launchPath = "/bin/zsh"
}
task.standardInput = nil
if #available(macOS 10.13, *) {
try task.run()
} else {
task.launch()
}
var output = ""
do {
let data = try pipe.fileHandleForReading.readToEnd()
if let data = data, let str = String(data: data, encoding: .utf8) {
output.append(str)
}
} catch {
return ""
}
return output
}
}