AppInstaller // Tearing down AppDelegate into parts.
This commit is contained in:
parent
dc3ed0a864
commit
77d219fde8
|
@ -12,41 +12,41 @@ import Cocoa
|
||||||
import IMKUtils
|
import IMKUtils
|
||||||
import InputMethodKit
|
import InputMethodKit
|
||||||
|
|
||||||
private let kTargetBin = "vChewing"
|
public let kTargetBin = "vChewing"
|
||||||
private let kTargetType = "app"
|
public let kTargetType = "app"
|
||||||
private let kTargetBundle = "vChewing.app"
|
public let kTargetBundle = "vChewing.app"
|
||||||
private let kTargetBundleWithComponents = "Library/Input%20Methods/vChewing.app"
|
public let kTargetBundleWithComponents = "Library/Input%20Methods/vChewing.app"
|
||||||
|
|
||||||
private let realHomeDir = URL(
|
public let realHomeDir = URL(
|
||||||
fileURLWithFileSystemRepresentation: getpwuid(getuid()).pointee.pw_dir, isDirectory: true, relativeTo: nil
|
fileURLWithFileSystemRepresentation: getpwuid(getuid()).pointee.pw_dir, isDirectory: true, relativeTo: nil
|
||||||
)
|
)
|
||||||
private let urlDestinationPartial = realHomeDir.appendingPathComponent("Library/Input Methods")
|
public let urlDestinationPartial = realHomeDir.appendingPathComponent("Library/Input Methods")
|
||||||
private let urlTargetPartial = realHomeDir.appendingPathComponent(kTargetBundleWithComponents)
|
public let urlTargetPartial = realHomeDir.appendingPathComponent(kTargetBundleWithComponents)
|
||||||
private let urlTargetFullBinPartial = urlTargetPartial.appendingPathComponent("Contents/MacOS")
|
public let urlTargetFullBinPartial = urlTargetPartial.appendingPathComponent("Contents/MacOS")
|
||||||
.appendingPathComponent(kTargetBin)
|
.appendingPathComponent(kTargetBin)
|
||||||
|
|
||||||
private let kDestinationPartial = urlDestinationPartial.path
|
public let kDestinationPartial = urlDestinationPartial.path
|
||||||
private let kTargetPartialPath = urlTargetPartial.path
|
public let kTargetPartialPath = urlTargetPartial.path
|
||||||
private let kTargetFullBinPartialPath = urlTargetFullBinPartial.path
|
public let kTargetFullBinPartialPath = urlTargetFullBinPartial.path
|
||||||
|
|
||||||
private let kTranslocationRemovalTickInterval: TimeInterval = 0.5
|
public let kTranslocationRemovalTickInterval: TimeInterval = 0.5
|
||||||
private let kTranslocationRemovalDeadline: TimeInterval = 60.0
|
public let kTranslocationRemovalDeadline: TimeInterval = 60.0
|
||||||
|
|
||||||
@NSApplicationMain
|
@NSApplicationMain
|
||||||
@objc(AppDelegate)
|
@objc(AppDelegate)
|
||||||
class AppDelegate: NSWindowController, NSApplicationDelegate {
|
class AppDelegate: NSWindowController, NSApplicationDelegate {
|
||||||
@IBOutlet private var installButton: NSButton!
|
@IBOutlet var installButton: NSButton!
|
||||||
@IBOutlet private var cancelButton: NSButton!
|
@IBOutlet var cancelButton: NSButton!
|
||||||
@IBOutlet private var progressSheet: NSWindow!
|
@IBOutlet var progressSheet: NSWindow!
|
||||||
@IBOutlet private var progressIndicator: NSProgressIndicator!
|
@IBOutlet var progressIndicator: NSProgressIndicator!
|
||||||
@IBOutlet private var appVersionLabel: NSTextField!
|
@IBOutlet var appVersionLabel: NSTextField!
|
||||||
@IBOutlet private var appCopyrightLabel: NSTextField!
|
@IBOutlet var appCopyrightLabel: NSTextField!
|
||||||
@IBOutlet private var appEULAContent: NSTextView!
|
@IBOutlet var appEULAContent: NSTextView!
|
||||||
|
|
||||||
private var installingVersion = ""
|
var installingVersion = ""
|
||||||
private var upgrading = false
|
var upgrading = false
|
||||||
private var translocationRemovalStartTime: Date?
|
var translocationRemovalStartTime: Date?
|
||||||
private var currentVersionNumber: Int = 0
|
var currentVersionNumber: Int = 0
|
||||||
|
|
||||||
let imeURLInstalled = realHomeDir.appendingPathComponent("Library/Input Methods/vChewing.app")
|
let imeURLInstalled = realHomeDir.appendingPathComponent("Library/Input Methods/vChewing.app")
|
||||||
|
|
||||||
|
@ -138,208 +138,13 @@ class AppDelegate: NSWindowController, NSApplicationDelegate {
|
||||||
if elapsed >= kTranslocationRemovalDeadline {
|
if elapsed >= kTranslocationRemovalDeadline {
|
||||||
timer.invalidate()
|
timer.invalidate()
|
||||||
window.endSheet(progressSheet, returnCode: .cancel)
|
window.endSheet(progressSheet, returnCode: .cancel)
|
||||||
} else if isAppBundleTranslocated(atPath: kTargetPartialPath) == false {
|
} else if Reloc.isAppBundleTranslocated(atPath: kTargetPartialPath) == false {
|
||||||
progressIndicator.doubleValue = 1.0
|
progressIndicator.doubleValue = 1.0
|
||||||
timer.invalidate()
|
timer.invalidate()
|
||||||
window.endSheet(progressSheet, returnCode: .continue)
|
window.endSheet(progressSheet, returnCode: .continue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeThenInstallInputMethod() {
|
|
||||||
// if !FileManager.default.fileExists(atPath: kTargetPartialPath) {
|
|
||||||
// installInputMethod(
|
|
||||||
// previousExists: false, previousVersionNotFullyDeactivatedWarning: false
|
|
||||||
// )
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
|
|
||||||
guard let window = window else { return }
|
|
||||||
|
|
||||||
let shouldWaitForTranslocationRemoval =
|
|
||||||
isAppBundleTranslocated(atPath: kTargetPartialPath)
|
|
||||||
&& window.responds(to: #selector(NSWindow.beginSheet(_:completionHandler:)))
|
|
||||||
|
|
||||||
// 將既存輸入法扔到垃圾桶內
|
|
||||||
do {
|
|
||||||
let sourceDir = kDestinationPartial
|
|
||||||
let fileManager = FileManager.default
|
|
||||||
let fileURLString = sourceDir + "/" + kTargetBundle
|
|
||||||
let fileURL = URL(fileURLWithPath: fileURLString)
|
|
||||||
|
|
||||||
// 檢查檔案是否存在
|
|
||||||
if fileManager.fileExists(atPath: fileURLString) {
|
|
||||||
// 塞入垃圾桶
|
|
||||||
try fileManager.trashItem(at: fileURL, resultingItemURL: nil)
|
|
||||||
} else {
|
|
||||||
NSLog("File does not exist")
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch let error as NSError {
|
|
||||||
NSLog("An error took place: \(error)")
|
|
||||||
}
|
|
||||||
|
|
||||||
let killTask = Process()
|
|
||||||
killTask.launchPath = "/usr/bin/killall"
|
|
||||||
killTask.arguments = ["-9", kTargetBin]
|
|
||||||
killTask.launch()
|
|
||||||
killTask.waitUntilExit()
|
|
||||||
|
|
||||||
if shouldWaitForTranslocationRemoval {
|
|
||||||
progressIndicator.startAnimation(self)
|
|
||||||
window.beginSheet(progressSheet) { returnCode in
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
if returnCode == .continue {
|
|
||||||
self.installInputMethod(
|
|
||||||
previousExists: true,
|
|
||||||
previousVersionNotFullyDeactivatedWarning: false
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
self.installInputMethod(
|
|
||||||
previousExists: true,
|
|
||||||
previousVersionNotFullyDeactivatedWarning: true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
translocationRemovalStartTime = Date()
|
|
||||||
Timer.scheduledTimer(
|
|
||||||
timeInterval: kTranslocationRemovalTickInterval, target: self,
|
|
||||||
selector: #selector(timerTick(_:)), userInfo: nil, repeats: true
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
installInputMethod(
|
|
||||||
previousExists: false, previousVersionNotFullyDeactivatedWarning: false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func installInputMethod(
|
|
||||||
previousExists _: Bool, previousVersionNotFullyDeactivatedWarning warning: Bool
|
|
||||||
) {
|
|
||||||
guard
|
|
||||||
let targetBundle = Bundle.main.path(forResource: kTargetBin, ofType: kTargetType)
|
|
||||||
else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let cpTask = Process()
|
|
||||||
cpTask.launchPath = "/bin/cp"
|
|
||||||
print(kDestinationPartial)
|
|
||||||
cpTask.arguments = [
|
|
||||||
"-R", targetBundle, kDestinationPartial,
|
|
||||||
]
|
|
||||||
cpTask.launch()
|
|
||||||
cpTask.waitUntilExit()
|
|
||||||
|
|
||||||
if cpTask.terminationStatus != 0 {
|
|
||||||
runAlertPanel(
|
|
||||||
title: NSLocalizedString("Install Failed", comment: ""),
|
|
||||||
message: NSLocalizedString("Cannot copy the file to the destination.", comment: ""),
|
|
||||||
buttonTitle: NSLocalizedString("Cancel", comment: "")
|
|
||||||
)
|
|
||||||
endAppWithDelay()
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = try? shell("/usr/bin/xattr -drs com.apple.quarantine \(kTargetPartialPath)")
|
|
||||||
|
|
||||||
guard let theBundle = Bundle(url: imeURLInstalled),
|
|
||||||
let imeIdentifier = theBundle.bundleIdentifier
|
|
||||||
else {
|
|
||||||
endAppWithDelay()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let imeBundleURL = theBundle.bundleURL
|
|
||||||
|
|
||||||
if allRegisteredInstancesOfThisInputMethod.isEmpty {
|
|
||||||
NSLog("Registering input source \(imeIdentifier) at \(imeBundleURL.absoluteString).")
|
|
||||||
let status = (TISRegisterInputSource(imeBundleURL as CFURL) == noErr)
|
|
||||||
if !status {
|
|
||||||
let message = String(
|
|
||||||
format: NSLocalizedString(
|
|
||||||
"Cannot find input source %@ after registration.", comment: ""
|
|
||||||
),
|
|
||||||
imeIdentifier
|
|
||||||
)
|
|
||||||
runAlertPanel(
|
|
||||||
title: NSLocalizedString("Fatal Error", comment: ""), message: message,
|
|
||||||
buttonTitle: NSLocalizedString("Abort", comment: "")
|
|
||||||
)
|
|
||||||
endAppWithDelay()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if allRegisteredInstancesOfThisInputMethod.isEmpty {
|
|
||||||
let message = String(
|
|
||||||
format: NSLocalizedString(
|
|
||||||
"Cannot find input source %@ after registration.", comment: ""
|
|
||||||
),
|
|
||||||
imeIdentifier
|
|
||||||
)
|
|
||||||
runAlertPanel(
|
|
||||||
title: NSLocalizedString("Fatal Error", comment: ""), message: message,
|
|
||||||
buttonTitle: NSLocalizedString("Abort", comment: "")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var isMacOS12OrAbove = false
|
|
||||||
if #available(macOS 12.0, *) {
|
|
||||||
NSLog("macOS 12 or later detected.")
|
|
||||||
isMacOS12OrAbove = true
|
|
||||||
} else {
|
|
||||||
NSLog("Installer runs with the pre-macOS 12 flow.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unconditionally enable the IME on macOS 12.0+,
|
|
||||||
// as the kTISPropertyInputSourceIsEnabled can still be true even if the IME is *not*
|
|
||||||
// enabled in the user's current set of IMEs (which means the IME does not show up in
|
|
||||||
// the user's input menu).
|
|
||||||
|
|
||||||
var mainInputSourceEnabled = false
|
|
||||||
|
|
||||||
allRegisteredInstancesOfThisInputMethod.forEach {
|
|
||||||
if $0.activate() {
|
|
||||||
NSLog("Input method enabled: \(imeIdentifier)")
|
|
||||||
} else {
|
|
||||||
NSLog("Failed to enable input method: \(imeIdentifier)")
|
|
||||||
}
|
|
||||||
mainInputSourceEnabled = $0.isActivated
|
|
||||||
}
|
|
||||||
|
|
||||||
// Alert Panel
|
|
||||||
let ntfPostInstall = NSAlert()
|
|
||||||
if warning {
|
|
||||||
ntfPostInstall.messageText = NSLocalizedString("Attention", comment: "")
|
|
||||||
ntfPostInstall.informativeText = NSLocalizedString(
|
|
||||||
"vChewing is upgraded, but please log out or reboot for the new version to be fully functional.",
|
|
||||||
comment: ""
|
|
||||||
)
|
|
||||||
ntfPostInstall.addButton(withTitle: NSLocalizedString("OK", comment: ""))
|
|
||||||
} else {
|
|
||||||
if !mainInputSourceEnabled, !isMacOS12OrAbove {
|
|
||||||
ntfPostInstall.messageText = NSLocalizedString("Warning", comment: "")
|
|
||||||
ntfPostInstall.informativeText = NSLocalizedString(
|
|
||||||
"Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources.",
|
|
||||||
comment: ""
|
|
||||||
)
|
|
||||||
ntfPostInstall.addButton(withTitle: NSLocalizedString("Continue", comment: ""))
|
|
||||||
} else {
|
|
||||||
ntfPostInstall.messageText = NSLocalizedString(
|
|
||||||
"Installation Successful", comment: ""
|
|
||||||
)
|
|
||||||
ntfPostInstall.informativeText = NSLocalizedString(
|
|
||||||
"vChewing is ready to use. \n\nPlease relogin if this is the first time you install it in this user account.",
|
|
||||||
comment: ""
|
|
||||||
)
|
|
||||||
ntfPostInstall.addButton(withTitle: NSLocalizedString("OK", comment: ""))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ntfPostInstall.beginSheetModal(for: window!) { _ in
|
|
||||||
self.endAppWithDelay()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func endAppWithDelay() {
|
func endAppWithDelay() {
|
||||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) {
|
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) {
|
||||||
NSApp.terminate(self)
|
NSApp.terminate(self)
|
||||||
|
@ -354,7 +159,7 @@ class AppDelegate: NSWindowController, NSApplicationDelegate {
|
||||||
NSApp.terminate(self)
|
NSApp.terminate(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func shell(_ command: String) throws -> String {
|
func shell(_ command: String) throws -> String {
|
||||||
let task = Process()
|
let task = Process()
|
||||||
let pipe = Pipe()
|
let pipe = Pipe()
|
||||||
|
|
||||||
|
@ -377,26 +182,4 @@ class AppDelegate: NSWindowController, NSApplicationDelegate {
|
||||||
|
|
||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determines if an app is translocated by Gatekeeper to a randomized path.
|
|
||||||
// See https://weblog.rogueamoeba.com/2016/06/29/sierra-and-gatekeeper-path-randomization/
|
|
||||||
// Originally written by Zonble Yang in Objective-C (MIT License).
|
|
||||||
// Swiftified by: Rob Mayoff. Ref: https://forums.swift.org/t/58719/5
|
|
||||||
func isAppBundleTranslocated(atPath bundlePath: String) -> Bool {
|
|
||||||
var entryCount = getfsstat(nil, 0, 0)
|
|
||||||
var entries: [statfs] = .init(repeating: .init(), count: Int(entryCount))
|
|
||||||
let absPath = bundlePath.cString(using: .utf8)
|
|
||||||
entryCount = getfsstat(&entries, entryCount * Int32(MemoryLayout<statfs>.stride), MNT_NOWAIT)
|
|
||||||
for entry in entries.prefix(Int(entryCount)) {
|
|
||||||
let isMatch = withUnsafeBytes(of: entry.f_mntfromname) { mntFromName in
|
|
||||||
strcmp(absPath, mntFromName.baseAddress) == 0
|
|
||||||
}
|
|
||||||
if isMatch {
|
|
||||||
var stat = statfs()
|
|
||||||
let rc = statfs(absPath, &stat)
|
|
||||||
return rc == 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,209 @@
|
||||||
|
// (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 InputMethodKit
|
||||||
|
|
||||||
|
extension AppDelegate {
|
||||||
|
func removeThenInstallInputMethod() {
|
||||||
|
// if !FileManager.default.fileExists(atPath: kTargetPartialPath) {
|
||||||
|
// installInputMethod(
|
||||||
|
// previousExists: false, previousVersionNotFullyDeactivatedWarning: false
|
||||||
|
// )
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
guard let window = window else { return }
|
||||||
|
|
||||||
|
let shouldWaitForTranslocationRemoval =
|
||||||
|
Reloc.isAppBundleTranslocated(atPath: kTargetPartialPath)
|
||||||
|
&& window.responds(to: #selector(NSWindow.beginSheet(_:completionHandler:)))
|
||||||
|
|
||||||
|
// 將既存輸入法扔到垃圾桶內
|
||||||
|
do {
|
||||||
|
let sourceDir = kDestinationPartial
|
||||||
|
let fileManager = FileManager.default
|
||||||
|
let fileURLString = sourceDir + "/" + kTargetBundle
|
||||||
|
let fileURL = URL(fileURLWithPath: fileURLString)
|
||||||
|
|
||||||
|
// 檢查檔案是否存在
|
||||||
|
if fileManager.fileExists(atPath: fileURLString) {
|
||||||
|
// 塞入垃圾桶
|
||||||
|
try fileManager.trashItem(at: fileURL, resultingItemURL: nil)
|
||||||
|
} else {
|
||||||
|
NSLog("File does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch let error as NSError {
|
||||||
|
NSLog("An error took place: \(error)")
|
||||||
|
}
|
||||||
|
|
||||||
|
let killTask = Process()
|
||||||
|
killTask.launchPath = "/usr/bin/killall"
|
||||||
|
killTask.arguments = ["-9", kTargetBin]
|
||||||
|
killTask.launch()
|
||||||
|
killTask.waitUntilExit()
|
||||||
|
|
||||||
|
if shouldWaitForTranslocationRemoval {
|
||||||
|
progressIndicator.startAnimation(self)
|
||||||
|
window.beginSheet(progressSheet) { returnCode in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if returnCode == .continue {
|
||||||
|
self.installInputMethod(
|
||||||
|
previousExists: true,
|
||||||
|
previousVersionNotFullyDeactivatedWarning: false
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
self.installInputMethod(
|
||||||
|
previousExists: true,
|
||||||
|
previousVersionNotFullyDeactivatedWarning: true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
translocationRemovalStartTime = Date()
|
||||||
|
Timer.scheduledTimer(
|
||||||
|
timeInterval: kTranslocationRemovalTickInterval, target: self,
|
||||||
|
selector: #selector(timerTick(_:)), userInfo: nil, repeats: true
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
installInputMethod(
|
||||||
|
previousExists: false, previousVersionNotFullyDeactivatedWarning: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func installInputMethod(
|
||||||
|
previousExists _: Bool, previousVersionNotFullyDeactivatedWarning warning: Bool
|
||||||
|
) {
|
||||||
|
guard
|
||||||
|
let targetBundle = Bundle.main.path(forResource: kTargetBin, ofType: kTargetType)
|
||||||
|
else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let cpTask = Process()
|
||||||
|
cpTask.launchPath = "/bin/cp"
|
||||||
|
print(kDestinationPartial)
|
||||||
|
cpTask.arguments = [
|
||||||
|
"-R", targetBundle, kDestinationPartial,
|
||||||
|
]
|
||||||
|
cpTask.launch()
|
||||||
|
cpTask.waitUntilExit()
|
||||||
|
|
||||||
|
if cpTask.terminationStatus != 0 {
|
||||||
|
runAlertPanel(
|
||||||
|
title: NSLocalizedString("Install Failed", comment: ""),
|
||||||
|
message: NSLocalizedString("Cannot copy the file to the destination.", comment: ""),
|
||||||
|
buttonTitle: NSLocalizedString("Cancel", comment: "")
|
||||||
|
)
|
||||||
|
endAppWithDelay()
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = try? shell("/usr/bin/xattr -drs com.apple.quarantine \(kTargetPartialPath)")
|
||||||
|
|
||||||
|
guard let theBundle = Bundle(url: imeURLInstalled),
|
||||||
|
let imeIdentifier = theBundle.bundleIdentifier
|
||||||
|
else {
|
||||||
|
endAppWithDelay()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let imeBundleURL = theBundle.bundleURL
|
||||||
|
|
||||||
|
if allRegisteredInstancesOfThisInputMethod.isEmpty {
|
||||||
|
NSLog("Registering input source \(imeIdentifier) at \(imeBundleURL.absoluteString).")
|
||||||
|
let status = (TISRegisterInputSource(imeBundleURL as CFURL) == noErr)
|
||||||
|
if !status {
|
||||||
|
let message = String(
|
||||||
|
format: NSLocalizedString(
|
||||||
|
"Cannot find input source %@ after registration.", comment: ""
|
||||||
|
),
|
||||||
|
imeIdentifier
|
||||||
|
)
|
||||||
|
runAlertPanel(
|
||||||
|
title: NSLocalizedString("Fatal Error", comment: ""), message: message,
|
||||||
|
buttonTitle: NSLocalizedString("Abort", comment: "")
|
||||||
|
)
|
||||||
|
endAppWithDelay()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if allRegisteredInstancesOfThisInputMethod.isEmpty {
|
||||||
|
let message = String(
|
||||||
|
format: NSLocalizedString(
|
||||||
|
"Cannot find input source %@ after registration.", comment: ""
|
||||||
|
),
|
||||||
|
imeIdentifier
|
||||||
|
)
|
||||||
|
runAlertPanel(
|
||||||
|
title: NSLocalizedString("Fatal Error", comment: ""), message: message,
|
||||||
|
buttonTitle: NSLocalizedString("Abort", comment: "")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isMacOS12OrAbove = false
|
||||||
|
if #available(macOS 12.0, *) {
|
||||||
|
NSLog("macOS 12 or later detected.")
|
||||||
|
isMacOS12OrAbove = true
|
||||||
|
} else {
|
||||||
|
NSLog("Installer runs with the pre-macOS 12 flow.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unconditionally enable the IME on macOS 12.0+,
|
||||||
|
// as the kTISPropertyInputSourceIsEnabled can still be true even if the IME is *not*
|
||||||
|
// enabled in the user's current set of IMEs (which means the IME does not show up in
|
||||||
|
// the user's input menu).
|
||||||
|
|
||||||
|
var mainInputSourceEnabled = false
|
||||||
|
|
||||||
|
allRegisteredInstancesOfThisInputMethod.forEach {
|
||||||
|
if $0.activate() {
|
||||||
|
NSLog("Input method enabled: \(imeIdentifier)")
|
||||||
|
} else {
|
||||||
|
NSLog("Failed to enable input method: \(imeIdentifier)")
|
||||||
|
}
|
||||||
|
mainInputSourceEnabled = $0.isActivated
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alert Panel
|
||||||
|
let ntfPostInstall = NSAlert()
|
||||||
|
if warning {
|
||||||
|
ntfPostInstall.messageText = NSLocalizedString("Attention", comment: "")
|
||||||
|
ntfPostInstall.informativeText = NSLocalizedString(
|
||||||
|
"vChewing is upgraded, but please log out or reboot for the new version to be fully functional.",
|
||||||
|
comment: ""
|
||||||
|
)
|
||||||
|
ntfPostInstall.addButton(withTitle: NSLocalizedString("OK", comment: ""))
|
||||||
|
} else {
|
||||||
|
if !mainInputSourceEnabled, !isMacOS12OrAbove {
|
||||||
|
ntfPostInstall.messageText = NSLocalizedString("Warning", comment: "")
|
||||||
|
ntfPostInstall.informativeText = NSLocalizedString(
|
||||||
|
"Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources.",
|
||||||
|
comment: ""
|
||||||
|
)
|
||||||
|
ntfPostInstall.addButton(withTitle: NSLocalizedString("Continue", comment: ""))
|
||||||
|
} else {
|
||||||
|
ntfPostInstall.messageText = NSLocalizedString(
|
||||||
|
"Installation Successful", comment: ""
|
||||||
|
)
|
||||||
|
ntfPostInstall.informativeText = NSLocalizedString(
|
||||||
|
"vChewing is ready to use. \n\nPlease relogin if this is the first time you install it in this user account.",
|
||||||
|
comment: ""
|
||||||
|
)
|
||||||
|
ntfPostInstall.addButton(withTitle: NSLocalizedString("OK", comment: ""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ntfPostInstall.beginSheetModal(for: window!) { _ in
|
||||||
|
self.endAppWithDelay()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
// (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 Foundation
|
||||||
|
|
||||||
|
public enum Reloc {
|
||||||
|
// Determines if an app is translocated by Gatekeeper to a randomized path.
|
||||||
|
// See https://weblog.rogueamoeba.com/2016/06/29/sierra-and-gatekeeper-path-randomization/
|
||||||
|
// Originally written by Zonble Yang in Objective-C (MIT License).
|
||||||
|
// Swiftified by: Rob Mayoff. Ref: https://forums.swift.org/t/58719/5
|
||||||
|
public static func isAppBundleTranslocated(atPath bundlePath: String) -> Bool {
|
||||||
|
var entryCount = getfsstat(nil, 0, 0)
|
||||||
|
var entries: [statfs] = .init(repeating: .init(), count: Int(entryCount))
|
||||||
|
let absPath = bundlePath.cString(using: .utf8)
|
||||||
|
entryCount = getfsstat(&entries, entryCount * Int32(MemoryLayout<statfs>.stride), MNT_NOWAIT)
|
||||||
|
for entry in entries.prefix(Int(entryCount)) {
|
||||||
|
let isMatch = withUnsafeBytes(of: entry.f_mntfromname) { mntFromName in
|
||||||
|
strcmp(absPath, mntFromName.baseAddress) == 0
|
||||||
|
}
|
||||||
|
if isMatch {
|
||||||
|
var stat = statfs()
|
||||||
|
let rc = statfs(absPath, &stat)
|
||||||
|
return rc == 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
|
@ -54,6 +54,8 @@
|
||||||
5BBC2D9F28F51C0400C986F6 /* LICENSE-CHT.txt in Resources */ = {isa = PBXBuildFile; fileRef = 5B18BA7427C7BD8C0056EB19 /* LICENSE-CHT.txt */; };
|
5BBC2D9F28F51C0400C986F6 /* LICENSE-CHT.txt in Resources */ = {isa = PBXBuildFile; fileRef = 5B18BA7427C7BD8C0056EB19 /* LICENSE-CHT.txt */; };
|
||||||
5BBC2DA028F51C0400C986F6 /* LICENSE-JPN.txt in Resources */ = {isa = PBXBuildFile; fileRef = 5B18BA7327C7BD8C0056EB19 /* LICENSE-JPN.txt */; };
|
5BBC2DA028F51C0400C986F6 /* LICENSE-JPN.txt in Resources */ = {isa = PBXBuildFile; fileRef = 5B18BA7327C7BD8C0056EB19 /* LICENSE-JPN.txt */; };
|
||||||
5BBC2DA128F51C0400C986F6 /* LICENSE-CHS.txt in Resources */ = {isa = PBXBuildFile; fileRef = 5B18BA6F27C7BD8B0056EB19 /* LICENSE-CHS.txt */; };
|
5BBC2DA128F51C0400C986F6 /* LICENSE-CHS.txt in Resources */ = {isa = PBXBuildFile; fileRef = 5B18BA6F27C7BD8B0056EB19 /* LICENSE-CHS.txt */; };
|
||||||
|
5BBC2DA328F5212100C986F6 /* RelocationDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BBC2DA228F5212100C986F6 /* RelocationDetector.swift */; };
|
||||||
|
5BBC2DA528F521C200C986F6 /* AppDelegate_Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BBC2DA428F521C200C986F6 /* AppDelegate_Extension.swift */; };
|
||||||
5BC2652227E04B7E00700291 /* uninstall.sh in Resources */ = {isa = PBXBuildFile; fileRef = 5BC2652127E04B7B00700291 /* uninstall.sh */; };
|
5BC2652227E04B7E00700291 /* uninstall.sh in Resources */ = {isa = PBXBuildFile; fileRef = 5BC2652127E04B7B00700291 /* uninstall.sh */; };
|
||||||
5BC4479D2865686500EDC323 /* data-chs.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5BEDB71C283B4AEA0078EB25 /* data-chs.plist */; };
|
5BC4479D2865686500EDC323 /* data-chs.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5BEDB71C283B4AEA0078EB25 /* data-chs.plist */; };
|
||||||
5BC4479E2865686500EDC323 /* data-cht.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5BEDB720283B4AEA0078EB25 /* data-cht.plist */; };
|
5BC4479E2865686500EDC323 /* data-cht.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5BEDB720283B4AEA0078EB25 /* data-cht.plist */; };
|
||||||
|
@ -243,6 +245,8 @@
|
||||||
5BBBB77127AED70B0023B93A /* MenuIcon-SCVIM.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "MenuIcon-SCVIM.png"; sourceTree = "<group>"; };
|
5BBBB77127AED70B0023B93A /* MenuIcon-SCVIM.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "MenuIcon-SCVIM.png"; sourceTree = "<group>"; };
|
||||||
5BBBB77227AED70B0023B93A /* MenuIcon-TCVIM.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "MenuIcon-TCVIM.png"; sourceTree = "<group>"; };
|
5BBBB77227AED70B0023B93A /* MenuIcon-TCVIM.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "MenuIcon-TCVIM.png"; sourceTree = "<group>"; };
|
||||||
5BBBB77727AEDB290023B93A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/MainMenu.strings; sourceTree = "<group>"; };
|
5BBBB77727AEDB290023B93A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/MainMenu.strings; sourceTree = "<group>"; };
|
||||||
|
5BBC2DA228F5212100C986F6 /* RelocationDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelocationDetector.swift; sourceTree = "<group>"; };
|
||||||
|
5BBC2DA428F521C200C986F6 /* AppDelegate_Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate_Extension.swift; sourceTree = "<group>"; };
|
||||||
5BBD627827B6C4D900271480 /* Update-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Update-Info.plist"; sourceTree = "<group>"; };
|
5BBD627827B6C4D900271480 /* Update-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Update-Info.plist"; sourceTree = "<group>"; };
|
||||||
5BC0AAC927F58472002D33E9 /* pkgPreInstall.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = pkgPreInstall.sh; sourceTree = "<group>"; };
|
5BC0AAC927F58472002D33E9 /* pkgPreInstall.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = pkgPreInstall.sh; sourceTree = "<group>"; };
|
||||||
5BC0AACA27F58472002D33E9 /* pkgPostInstall.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = pkgPostInstall.sh; sourceTree = "<group>"; };
|
5BC0AACA27F58472002D33E9 /* pkgPostInstall.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = pkgPostInstall.sh; sourceTree = "<group>"; };
|
||||||
|
@ -695,6 +699,8 @@
|
||||||
5B44B97C28D2F283004508BF /* PKGRoot */,
|
5B44B97C28D2F283004508BF /* PKGRoot */,
|
||||||
5BBBB77827AEDB330023B93A /* Resources */,
|
5BBBB77827AEDB330023B93A /* Resources */,
|
||||||
D4F0BBE0279AF8B30071253C /* AppDelegate.swift */,
|
D4F0BBE0279AF8B30071253C /* AppDelegate.swift */,
|
||||||
|
5BBC2DA428F521C200C986F6 /* AppDelegate_Extension.swift */,
|
||||||
|
5BBC2DA228F5212100C986F6 /* RelocationDetector.swift */,
|
||||||
6ACA41F215FC1D9000935EF6 /* Installer-Info.plist */,
|
6ACA41F215FC1D9000935EF6 /* Installer-Info.plist */,
|
||||||
5BC0AACA27F58472002D33E9 /* pkgPostInstall.sh */,
|
5BC0AACA27F58472002D33E9 /* pkgPostInstall.sh */,
|
||||||
5BC0AAC927F58472002D33E9 /* pkgPreInstall.sh */,
|
5BC0AAC927F58472002D33E9 /* pkgPreInstall.sh */,
|
||||||
|
@ -1094,6 +1100,8 @@
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
D4F0BBE1279AF8B30071253C /* AppDelegate.swift in Sources */,
|
D4F0BBE1279AF8B30071253C /* AppDelegate.swift in Sources */,
|
||||||
|
5BBC2DA528F521C200C986F6 /* AppDelegate_Extension.swift in Sources */,
|
||||||
|
5BBC2DA328F5212100C986F6 /* RelocationDetector.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue