-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
199 additions
and
199 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,211 +1,211 @@ | ||
// | ||
// AppDelegate.swift | ||
// | ||
// See LICENSE file for licensing information | ||
// | ||
// | ||
// AppDelegate.swift | ||
// | ||
// See LICENSE file for licensing information | ||
// | ||
|
||
import Cocoa | ||
import SwiftUI | ||
import Cocoa | ||
import SwiftUI | ||
|
||
enum MenuItem { | ||
case restart(Int) | ||
case preferences | ||
} | ||
|
||
@main | ||
class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate, ObservableObject { | ||
// Set fallback values for popover widths, in case fetching from Localizable.strings fails | ||
static let defaultRestartPopoverWidth = 310 | ||
static let defaultPreferencesPopoverWidth = 390 | ||
static let defaultHelpPopoverWidth = 300 | ||
|
||
// Use a popover i.e. "menubar icon speech bubble" instead of dialogs / windows | ||
var popover: NSPopover = NSPopover() | ||
enum MenuItem { | ||
case restart(Int) | ||
case preferences | ||
} | ||
|
||
// NSHostingController is used for bridging SwiftUI views into AppKit's NSPopover | ||
var hostingController: NSHostingController<AnyView> = NSHostingController(rootView: AnyView(EmptyView())) | ||
|
||
lazy var statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength) | ||
@Published var currentView: CurrentView = .restart(1) | ||
var menuItemToView: [NSMenuItem: MenuItem] = [:] | ||
|
||
func setStartupDiskForNextBootOnlyAndRestart() { | ||
let script = """ | ||
@main | ||
class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate, ObservableObject { | ||
// Set fallback values for popover widths, in case fetching from Localizable.strings fails | ||
static let defaultRestartPopoverWidth = 310 | ||
static let defaultPreferencesPopoverWidth = 390 | ||
static let defaultHelpPopoverWidth = 300 | ||
|
||
// Use a popover i.e. "menubar icon speech bubble" instead of dialogs / windows | ||
var popover: NSPopover = NSPopover() | ||
|
||
// NSHostingController is used for bridging SwiftUI views into AppKit's NSPopover | ||
var hostingController: NSHostingController<AnyView> = NSHostingController(rootView: AnyView(EmptyView())) | ||
|
||
lazy var statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength) | ||
@Published var currentView: CurrentView = .restart(1) | ||
var menuItemToView: [NSMenuItem: MenuItem] = [:] | ||
|
||
func setStartupDiskForNextBootOnlyAndRestart() { | ||
let script = """ | ||
do shell script "bless --mount \\"/Volumes/FedoraLinux\\" --setBoot --nextonly" with administrator privileges | ||
""" | ||
let appleScript = NSAppleScript(source: script) | ||
|
||
var error: NSDictionary? | ||
if appleScript?.executeAndReturnError(&error) == nil { | ||
// TODO: implement better error handling | ||
// If the user cancels the password prompt, just close the popover | ||
if let error = error, (error[NSAppleScript.errorNumber] as? Int) == -128 { | ||
DispatchQueue.main.async { | ||
self.closePopover(nil) | ||
} | ||
return | ||
} | ||
// Display an error dialog with the actual error message from sudo bless | ||
let alert = NSAlert() | ||
alert.messageText = "Error" | ||
if let errorMessage = error?[NSAppleScript.errorMessage] as? String { | ||
alert.informativeText = errorMessage | ||
} else { | ||
alert.informativeText = "Unknown error" | ||
let appleScript = NSAppleScript(source: script) | ||
|
||
var error: NSDictionary? | ||
if appleScript?.executeAndReturnError(&error) == nil { | ||
// TODO: implement better error handling | ||
// If the user cancels the password prompt, just close the popover | ||
if let error = error, (error[NSAppleScript.errorNumber] as? Int) == -128 { | ||
DispatchQueue.main.async { | ||
self.closePopover(nil) | ||
} | ||
alert.alertStyle = .warning | ||
alert.runModal() | ||
return | ||
} | ||
// Display an error dialog with the actual error message from sudo bless | ||
let alert = NSAlert() | ||
alert.messageText = "Error" | ||
if let errorMessage = error?[NSAppleScript.errorMessage] as? String { | ||
alert.informativeText = errorMessage | ||
} else { | ||
// DEBUG: Fake restart | ||
NSAppleScript(source: "display dialog \"Fake restart\"")?.executeAndReturnError(nil) | ||
// Perform the actual restart | ||
//NSAppleScript(source: "tell application \"System Events\" to restart")?.executeAndReturnError(nil) | ||
alert.informativeText = "Unknown error" | ||
} | ||
alert.alertStyle = .warning | ||
alert.runModal() | ||
return | ||
} else { | ||
// DEBUG: Fake restart | ||
NSAppleScript(source: "display dialog \"Fake restart\"")?.executeAndReturnError(nil) | ||
// Perform the actual restart | ||
//NSAppleScript(source: "tell application \"System Events\" to restart")?.executeAndReturnError(nil) | ||
} | ||
} | ||
|
||
|
||
|
||
func applicationDidFinishLaunching(_ aNotification: Notification) { | ||
if let button = statusItem.button { | ||
button.image = NSImage(named: "MenuBarIcon") | ||
button.action = #selector(togglePopover(_:)) | ||
} | ||
|
||
|
||
// Make the popover close if the user clicks elsewhere | ||
popover.behavior = .transient | ||
popover.delegate = self | ||
|
||
func applicationDidFinishLaunching(_ aNotification: Notification) { | ||
if let button = statusItem.button { | ||
button.image = NSImage(named: "MenuBarIcon") | ||
button.action = #selector(togglePopover(_:)) | ||
} | ||
|
||
// Make the popover close if the user clicks elsewhere | ||
popover.behavior = .transient | ||
popover.delegate = self | ||
|
||
|
||
let menu = NSMenu() | ||
|
||
// todo: implement actually querying user's disks, use "diskutil list -plist" | ||
// note: if there are more than one non-macOS disk, list them all by name instead of "Linux" | ||
// ie. don't ask user what "Linux" means | ||
let item1 = NSMenuItem(title: "Restart in Linux...", | ||
action: #selector(closeMenuAndShowPopover(_:)), keyEquivalent: "") | ||
menu.addItem(item1) | ||
menuItemToView[item1] = .restart(1) | ||
|
||
let item2 = NSMenuItem(title: "Restart in macOS... (default)", | ||
action: #selector(closeMenuAndShowPopover(_:)), keyEquivalent: "") | ||
menu.addItem(item2) | ||
menuItemToView[item2] = .restart(2) | ||
|
||
menu.addItem(NSMenuItem.separator()) | ||
|
||
let submenu = NSMenu() | ||
|
||
//FIXME: actually implement | ||
let newStartupDiskItem = NSMenuItem(title: "Use Linux...", | ||
action: #selector(newStartupDiskSelected(_:)), keyEquivalent: "") | ||
newStartupDiskItem.tag = 1 // replace with the actual disk number | ||
submenu.addItem(newStartupDiskItem) | ||
|
||
let submenuItem = NSMenuItem(title: "Change default Startup Disk", action: nil, keyEquivalent: "") | ||
submenuItem.submenu = submenu | ||
menu.addItem(submenuItem) | ||
menuItemToView[submenuItem] = .restart(3) | ||
|
||
menu.addItem(NSMenuItem.separator()) | ||
|
||
let item3 = NSMenuItem(title: "Preferences...", | ||
action: #selector(closeMenuAndShowPopover(_:)), keyEquivalent: "") | ||
menu.addItem(item3) | ||
menuItemToView[item3] = .preferences | ||
|
||
statusItem.menu = menu | ||
|
||
|
||
let menu = NSMenu() | ||
|
||
// todo: implement actually querying user's disks, use "diskutil list -plist" | ||
// note: if there are more than one non-macOS disk, list them all by name instead of "Linux" | ||
// ie. don't ask user what "Linux" means | ||
let item1 = NSMenuItem(title: "Restart in Linux...", | ||
action: #selector(closeMenuAndShowPopover(_:)), keyEquivalent: "") | ||
menu.addItem(item1) | ||
menuItemToView[item1] = .restart(1) | ||
|
||
let item2 = NSMenuItem(title: "Restart in macOS... (default)", | ||
action: #selector(closeMenuAndShowPopover(_:)), keyEquivalent: "") | ||
menu.addItem(item2) | ||
menuItemToView[item2] = .restart(2) | ||
|
||
menu.addItem(NSMenuItem.separator()) | ||
|
||
let submenu = NSMenu() | ||
|
||
//FIXME: actually implement | ||
let newStartupDiskItem = NSMenuItem(title: "Use Linux...", | ||
action: #selector(newStartupDiskSelected(_:)), keyEquivalent: "") | ||
newStartupDiskItem.tag = 1 // replace with the actual disk number | ||
submenu.addItem(newStartupDiskItem) | ||
|
||
let submenuItem = NSMenuItem(title: "Change default Startup Disk", action: nil, keyEquivalent: "") | ||
submenuItem.submenu = submenu | ||
menu.addItem(submenuItem) | ||
menuItemToView[submenuItem] = .restart(3) | ||
|
||
menu.addItem(NSMenuItem.separator()) | ||
|
||
let item3 = NSMenuItem(title: "Preferences...", | ||
action: #selector(closeMenuAndShowPopover(_:)), keyEquivalent: "") | ||
menu.addItem(item3) | ||
menuItemToView[item3] = .preferences | ||
|
||
statusItem.menu = menu | ||
|
||
} | ||
|
||
@objc func closeMenuAndShowPopover(_ sender: NSMenuItem) { | ||
guard let menuItem = menuItemToView[sender] else { | ||
return | ||
} | ||
|
||
@objc func closeMenuAndShowPopover(_ sender: NSMenuItem) { | ||
guard let menuItem = menuItemToView[sender] else { | ||
return | ||
} | ||
|
||
// Close the menu | ||
statusItem.menu?.cancelTracking() | ||
// Close the menu | ||
statusItem.menu?.cancelTracking() | ||
|
||
// Set the current view based on the menu item | ||
switch menuItem { | ||
case .restart(let diskNumber): | ||
currentView = .restart(diskNumber) | ||
case .preferences: | ||
currentView = .preferences | ||
} | ||
|
||
// Show the popover | ||
hostingController.rootView = AnyView(ContentView().environmentObject(self)) | ||
popover.contentViewController = hostingController | ||
if let button = statusItem.button { | ||
showPopover(button) | ||
} | ||
} | ||
|
||
@objc func togglePopover(_ sender: AnyObject?) { | ||
if popover.isShown { | ||
closePopover(sender) | ||
} else { | ||
hostingController.rootView = AnyView(ContentView().environmentObject(self)) | ||
popover.contentViewController = hostingController | ||
showPopover(sender) | ||
} | ||
} | ||
|
||
@objc func showPopover(_ sender: AnyObject?) { | ||
// Display the popover. Adjust horizontal position if it won't otherwise fit on the screen. | ||
if let button = statusItem.button { | ||
let screenRect = NSScreen.main?.visibleFrame ?? NSRect.zero | ||
let buttonRect = button.window?.convertToScreen(button.frame) ?? NSRect.zero | ||
|
||
// Set the current view based on the menu item | ||
switch menuItem { | ||
case .restart(let diskNumber): | ||
currentView = .restart(diskNumber) | ||
var popoverWidth: CGFloat | ||
switch currentView { | ||
case .restart: | ||
popoverWidth = CGFloat(AppDelegate.defaultRestartPopoverWidth) | ||
case .preferences: | ||
currentView = .preferences | ||
popoverWidth = CGFloat(AppDelegate.defaultPreferencesPopoverWidth) | ||
case .help: | ||
popoverWidth = CGFloat(AppDelegate.defaultHelpPopoverWidth) | ||
} | ||
|
||
// Show the popover | ||
hostingController.rootView = AnyView(ContentView().environmentObject(self)) | ||
popover.contentViewController = hostingController | ||
if let button = statusItem.button { | ||
showPopover(button) | ||
} | ||
} | ||
|
||
@objc func togglePopover(_ sender: AnyObject?) { | ||
if popover.isShown { | ||
closePopover(sender) | ||
popover.contentSize = NSSize(width: popoverWidth, height: popover.contentSize.height) | ||
|
||
if buttonRect.origin.x + popoverWidth > screenRect.origin.x + screenRect.width { | ||
popover.show(relativeTo: NSRect(x: button.bounds.width - popoverWidth, | ||
y: button.bounds.height, width: 0, height: 0), | ||
of: button, preferredEdge: .minY) | ||
} else { | ||
hostingController.rootView = AnyView(ContentView().environmentObject(self)) | ||
popover.contentViewController = hostingController | ||
showPopover(sender) | ||
} | ||
} | ||
|
||
@objc func showPopover(_ sender: AnyObject?) { | ||
// Display the popover. Adjust horizontal position if it won't otherwise fit on the screen. | ||
if let button = statusItem.button { | ||
let screenRect = NSScreen.main?.visibleFrame ?? NSRect.zero | ||
let buttonRect = button.window?.convertToScreen(button.frame) ?? NSRect.zero | ||
|
||
var popoverWidth: CGFloat | ||
switch currentView { | ||
case .restart: | ||
popoverWidth = CGFloat(AppDelegate.defaultRestartPopoverWidth) | ||
case .preferences: | ||
popoverWidth = CGFloat(AppDelegate.defaultPreferencesPopoverWidth) | ||
case .help: | ||
popoverWidth = CGFloat(AppDelegate.defaultHelpPopoverWidth) | ||
} | ||
hostingController.rootView = AnyView(ContentView().environmentObject(self)) | ||
popover.contentViewController = hostingController | ||
popover.contentSize = NSSize(width: popoverWidth, height: popover.contentSize.height) | ||
|
||
if buttonRect.origin.x + popoverWidth > screenRect.origin.x + screenRect.width { | ||
popover.show(relativeTo: NSRect(x: button.bounds.width - popoverWidth, | ||
y: button.bounds.height, width: 0, height: 0), | ||
of: button, preferredEdge: .minY) | ||
} else { | ||
|
||
popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY) | ||
} | ||
} | ||
} | ||
|
||
func closePopover(_ sender: AnyObject?) { | ||
popover.performClose(sender) | ||
} | ||
|
||
@objc func preferencesSelected() { | ||
// Close the menu | ||
statusItem.menu?.cancelTracking() | ||
|
||
// Show the popover | ||
currentView = .preferences | ||
if let button = statusItem.button { | ||
showPopover(button) | ||
popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY) | ||
} | ||
} | ||
} | ||
|
||
func closePopover(_ sender: AnyObject?) { | ||
popover.performClose(sender) | ||
} | ||
|
||
@objc func preferencesSelected() { | ||
// Close the menu | ||
statusItem.menu?.cancelTracking() | ||
|
||
@objc func newStartupDiskSelected(_ sender: NSMenuItem) { | ||
let alert = NSAlert() | ||
alert.messageText = "(todo, placeholder) Change default Startup Disk?" | ||
alert.informativeText = "(todo, placeholder) Are you sure you want to use (disk number \(sender.tag))?" | ||
alert.alertStyle = .informational | ||
alert.addButton(withTitle: "Change") | ||
alert.addButton(withTitle: "Cancel") | ||
alert.runModal() | ||
// Show the popover | ||
currentView = .preferences | ||
if let button = statusItem.button { | ||
showPopover(button) | ||
} | ||
|
||
|
||
} | ||
|
||
@objc func newStartupDiskSelected(_ sender: NSMenuItem) { | ||
let alert = NSAlert() | ||
alert.messageText = "(todo, placeholder) Change default Startup Disk?" | ||
alert.informativeText = "(todo, placeholder) Are you sure you want to use (disk number \(sender.tag))?" | ||
alert.alertStyle = .informational | ||
alert.addButton(withTitle: "Change") | ||
alert.addButton(withTitle: "Cancel") | ||
alert.runModal() | ||
} | ||
|
||
|
||
} |
Oops, something went wrong.