Initial commit
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
// AppDelegate.swift
|
||||
// GreenshotMac
|
||||
|
||||
import AppKit
|
||||
|
||||
@MainActor
|
||||
final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
|
||||
private var statusMenu: StatusMenuController?
|
||||
|
||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||
// A second copy would install its own menu bar item and register the same global
|
||||
// hotkeys, which the first copy already owns. Hand over to the running instance.
|
||||
if let other = AppDelegate.otherRunningInstance() {
|
||||
NSLog("GreenshotMac: another instance is already running (pid \(other.processIdentifier)); exiting.")
|
||||
other.activate()
|
||||
Notifier.show(title: "GreenshotMac is already running",
|
||||
body: "Use the camera icon in the menu bar.")
|
||||
NSApp.terminate(nil)
|
||||
return
|
||||
}
|
||||
|
||||
NSApp.setActivationPolicy(.accessory)
|
||||
statusMenu = StatusMenuController()
|
||||
MainMenuBuilder.install()
|
||||
HotkeyBindings.shared.registerAll()
|
||||
|
||||
if !CGPreflightScreenCaptureAccess() {
|
||||
// Triggers the system prompt on first launch so the first capture just works.
|
||||
CGRequestScreenCaptureAccess()
|
||||
}
|
||||
}
|
||||
|
||||
/// Another process running the same bundle identifier, if there is one.
|
||||
static func otherRunningInstance() -> NSRunningApplication? {
|
||||
guard let bundleID = Bundle.main.bundleIdentifier else { return nil }
|
||||
let current = NSRunningApplication.current
|
||||
return NSRunningApplication.runningApplications(withBundleIdentifier: bundleID)
|
||||
.first { $0.processIdentifier != current.processIdentifier && !$0.isTerminated }
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ notification: Notification) {
|
||||
HotkeyManager.shared.unregisterAll()
|
||||
}
|
||||
|
||||
func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { true }
|
||||
|
||||
func application(_ application: NSApplication, open urls: [URL]) {
|
||||
for url in urls {
|
||||
guard let image = NSImage(contentsOf: url),
|
||||
let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { continue }
|
||||
var details = CaptureDetails()
|
||||
details.captureMode = .importedFile
|
||||
details.title = url.deletingPathExtension().lastPathComponent
|
||||
details.scaleFactor = CGFloat(cgImage.width) / max(1, image.size.width)
|
||||
EditorWindowController.open(with: Capture(image: cgImage, details: details))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
// CaptureCoordinator.swift
|
||||
// GreenshotMac
|
||||
//
|
||||
// Drives the capture flows, the macOS counterpart of Greenshot's CaptureHelper.
|
||||
|
||||
import AppKit
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
@MainActor
|
||||
final class CaptureCoordinator {
|
||||
static let shared = CaptureCoordinator()
|
||||
|
||||
private var overlay: CaptureOverlayController?
|
||||
private var isCapturing = false
|
||||
private var lastRegion: CGRect?
|
||||
|
||||
private init() {}
|
||||
|
||||
var hasLastRegion: Bool { lastRegion != nil }
|
||||
|
||||
// MARK: - Entry points
|
||||
|
||||
func captureRegion() {
|
||||
beginOverlayCapture(mode: .region)
|
||||
}
|
||||
|
||||
func captureWindow() {
|
||||
beginOverlayCapture(mode: .window)
|
||||
}
|
||||
|
||||
func captureFullscreen() {
|
||||
guard !isCapturing, CaptureEngine.ensurePermission() else { return requestPermission() }
|
||||
isCapturing = true
|
||||
Task {
|
||||
defer { isCapturing = false }
|
||||
do {
|
||||
try await delayIfNeeded()
|
||||
let capture = try await CaptureEngine.captureFullscreen()
|
||||
finish(capture)
|
||||
} catch {
|
||||
report(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func captureLastRegion() {
|
||||
guard let lastRegion else { return captureRegion() }
|
||||
guard !isCapturing, CaptureEngine.ensurePermission() else { return requestPermission() }
|
||||
isCapturing = true
|
||||
Task {
|
||||
defer { isCapturing = false }
|
||||
do {
|
||||
try await delayIfNeeded()
|
||||
let snapshots = try await CaptureEngine.snapshotAllDisplays()
|
||||
guard let snapshot = snapshots.first(where: { $0.cgFrame.intersects(lastRegion) }) else {
|
||||
throw CaptureError.noContent
|
||||
}
|
||||
let capture = try CaptureEngine.crop(snapshot: snapshot, cgRect: lastRegion, mode: .lastRegion)
|
||||
finish(capture)
|
||||
} catch {
|
||||
report(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens an image file in the editor, the equivalent of Greenshot's "Open image from file".
|
||||
func openImageFile() {
|
||||
let panel = NSOpenPanel()
|
||||
panel.allowedContentTypes = [.image]
|
||||
panel.allowsMultipleSelection = false
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
guard panel.runModal() == .OK,
|
||||
let url = panel.url,
|
||||
let image = NSImage(contentsOf: url),
|
||||
let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return }
|
||||
|
||||
var details = CaptureDetails()
|
||||
details.captureMode = .importedFile
|
||||
details.title = url.deletingPathExtension().lastPathComponent
|
||||
details.scaleFactor = CGFloat(cgImage.width) / max(1, image.size.width)
|
||||
EditorWindowController.open(with: Capture(image: cgImage, details: details))
|
||||
}
|
||||
|
||||
// MARK: - Overlay flow
|
||||
|
||||
private func beginOverlayCapture(mode: OverlayMode) {
|
||||
guard !isCapturing else { return }
|
||||
guard CaptureEngine.ensurePermission() else { return requestPermission() }
|
||||
isCapturing = true
|
||||
|
||||
Task {
|
||||
do {
|
||||
try await delayIfNeeded()
|
||||
let content = try await CaptureEngine.shareableContent()
|
||||
let snapshots = try await CaptureEngine.snapshotAllDisplays()
|
||||
let windows = CaptureEngine.selectableWindows(from: content)
|
||||
|
||||
let controller = CaptureOverlayController(snapshots: snapshots, windows: windows, mode: mode)
|
||||
overlay = controller
|
||||
controller.present { result in
|
||||
self.overlay = nil
|
||||
self.isCapturing = false
|
||||
self.handle(result)
|
||||
}
|
||||
} catch {
|
||||
isCapturing = false
|
||||
report(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func handle(_ result: OverlayResult) {
|
||||
switch result {
|
||||
case .cancelled:
|
||||
return
|
||||
case .region(let snapshot, let rect):
|
||||
lastRegion = rect
|
||||
do {
|
||||
let capture = try CaptureEngine.crop(snapshot: snapshot, cgRect: rect, mode: .region)
|
||||
finish(capture)
|
||||
} catch {
|
||||
report(error)
|
||||
}
|
||||
case .window(let info):
|
||||
Task {
|
||||
do {
|
||||
let capture = try await CaptureEngine.captureWindow(info)
|
||||
finish(capture)
|
||||
} catch {
|
||||
report(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Shared
|
||||
|
||||
private func finish(_ capture: Capture) {
|
||||
CaptureEngine.playFeedback(on: capture.details.screenBounds)
|
||||
Destinations.export(capture, to: Configuration.shared.defaultDestination)
|
||||
}
|
||||
|
||||
private func delayIfNeeded() async throws {
|
||||
let millis = Configuration.shared.captureDelayMillis
|
||||
guard millis > 0 else { return }
|
||||
try await Task.sleep(nanoseconds: UInt64(millis) * 1_000_000)
|
||||
}
|
||||
|
||||
private func report(_ error: Error) {
|
||||
if let captureError = error as? CaptureError, case .cancelled = captureError { return }
|
||||
Destinations.presentError(error)
|
||||
}
|
||||
|
||||
private func requestPermission() {
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "Screen Recording permission needed"
|
||||
alert.informativeText = """
|
||||
macOS requires explicit permission before an app can capture the screen.
|
||||
|
||||
Open System Settings › Privacy & Security › Screen Recording, enable GreenshotMac, then try again.
|
||||
"""
|
||||
alert.addButton(withTitle: "Open System Settings")
|
||||
alert.addButton(withTitle: "Cancel")
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
if alert.runModal() == .alertFirstButtonReturn,
|
||||
let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture") {
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// HotkeyBindings.swift
|
||||
// GreenshotMac
|
||||
//
|
||||
// Maps the four capture actions onto global hotkeys and keeps them registered.
|
||||
|
||||
import AppKit
|
||||
|
||||
enum HotkeyAction: String, CaseIterable {
|
||||
case region
|
||||
case window
|
||||
case fullscreen
|
||||
case lastRegion
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .region: return "Capture region"
|
||||
case .window: return "Capture window"
|
||||
case .fullscreen: return "Capture full screen"
|
||||
case .lastRegion: return "Capture last region"
|
||||
}
|
||||
}
|
||||
|
||||
var defaultsKey: String {
|
||||
switch self {
|
||||
case .region: return Configuration.Keys.hotkeyRegion
|
||||
case .window: return Configuration.Keys.hotkeyWindow
|
||||
case .fullscreen: return Configuration.Keys.hotkeyFullscreen
|
||||
case .lastRegion: return Configuration.Keys.hotkeyLastRegion
|
||||
}
|
||||
}
|
||||
|
||||
var defaultSpec: HotkeySpec {
|
||||
switch self {
|
||||
case .region: return DefaultHotkeys.region
|
||||
case .window: return DefaultHotkeys.window
|
||||
case .fullscreen: return DefaultHotkeys.fullscreen
|
||||
case .lastRegion: return DefaultHotkeys.lastRegion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class HotkeyBindings {
|
||||
static let shared = HotkeyBindings()
|
||||
|
||||
private var registrations: [HotkeyAction: UInt32] = [:]
|
||||
|
||||
private init() {}
|
||||
|
||||
func spec(for action: HotkeyAction) -> HotkeySpec {
|
||||
Configuration.shared.hotkey(forKey: action.defaultsKey, default: action.defaultSpec)
|
||||
}
|
||||
|
||||
func set(_ spec: HotkeySpec, for action: HotkeyAction) {
|
||||
Configuration.shared.setHotkey(spec, forKey: action.defaultsKey)
|
||||
registerAll()
|
||||
NotificationCenter.default.post(name: .hotkeysChanged, object: nil)
|
||||
}
|
||||
|
||||
func registerAll() {
|
||||
for (_, id) in registrations { HotkeyManager.shared.unregister(id) }
|
||||
registrations.removeAll()
|
||||
|
||||
for action in HotkeyAction.allCases {
|
||||
let spec = self.spec(for: action)
|
||||
guard !spec.isEmpty else { continue }
|
||||
if let id = HotkeyManager.shared.register(spec, handler: { HotkeyBindings.perform(action) }) {
|
||||
registrations[action] = id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func perform(_ action: HotkeyAction) {
|
||||
Task { @MainActor in
|
||||
switch action {
|
||||
case .region: CaptureCoordinator.shared.captureRegion()
|
||||
case .window: CaptureCoordinator.shared.captureWindow()
|
||||
case .fullscreen: CaptureCoordinator.shared.captureFullscreen()
|
||||
case .lastRegion: CaptureCoordinator.shared.captureLastRegion()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Notification.Name {
|
||||
static let hotkeysChanged = Notification.Name("GreenshotMacHotkeysChanged")
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// MainMenuBuilder.swift
|
||||
// GreenshotMac
|
||||
//
|
||||
// A menu bar for the editor windows; the app itself lives in the status bar.
|
||||
|
||||
import AppKit
|
||||
|
||||
@MainActor
|
||||
enum MainMenuBuilder {
|
||||
|
||||
static func install() {
|
||||
let mainMenu = NSMenu()
|
||||
|
||||
// Application menu
|
||||
let appMenuItem = NSMenuItem()
|
||||
let appMenu = NSMenu()
|
||||
appMenu.addItem(withTitle: "About GreenshotMac", action: #selector(NSApplication.orderFrontStandardAboutPanel(_:)), keyEquivalent: "")
|
||||
appMenu.addItem(.separator())
|
||||
appMenu.addItem(withTitle: "Hide GreenshotMac", action: #selector(NSApplication.hide(_:)), keyEquivalent: "h")
|
||||
appMenu.addItem(.separator())
|
||||
appMenu.addItem(withTitle: "Quit GreenshotMac", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")
|
||||
appMenuItem.submenu = appMenu
|
||||
mainMenu.addItem(appMenuItem)
|
||||
|
||||
// File
|
||||
let fileItem = NSMenuItem()
|
||||
let fileMenu = NSMenu(title: "File")
|
||||
fileMenu.addItem(withTitle: "Save", action: #selector(EditorWindowController.saveDocument(_:)), keyEquivalent: "s")
|
||||
let saveAs = NSMenuItem(title: "Save As…",
|
||||
action: #selector(EditorWindowController.saveDocumentAs(_:)),
|
||||
keyEquivalent: "s")
|
||||
saveAs.keyEquivalentModifierMask = [.command, .shift]
|
||||
fileMenu.addItem(saveAs)
|
||||
fileMenu.addItem(withTitle: "Print…", action: #selector(EditorWindowController.printDocument(_:)), keyEquivalent: "p")
|
||||
fileMenu.addItem(withTitle: "Share…", action: #selector(EditorWindowController.shareDocument(_:)), keyEquivalent: "")
|
||||
fileMenu.addItem(.separator())
|
||||
fileMenu.addItem(withTitle: "Close", action: #selector(NSWindow.performClose(_:)), keyEquivalent: "w")
|
||||
fileItem.submenu = fileMenu
|
||||
mainMenu.addItem(fileItem)
|
||||
|
||||
// Edit
|
||||
let editItem = NSMenuItem()
|
||||
let editMenu = NSMenu(title: "Edit")
|
||||
editMenu.addItem(withTitle: "Undo", action: Selector(("undo:")), keyEquivalent: "z")
|
||||
let redo = NSMenuItem(title: "Redo", action: Selector(("redo:")), keyEquivalent: "z")
|
||||
redo.keyEquivalentModifierMask = [.command, .shift]
|
||||
editMenu.addItem(redo)
|
||||
editMenu.addItem(.separator())
|
||||
editMenu.addItem(withTitle: "Cut", action: #selector(NSText.cut(_:)), keyEquivalent: "x")
|
||||
editMenu.addItem(withTitle: "Copy", action: #selector(EditorWindowController.copy(_:)), keyEquivalent: "c")
|
||||
editMenu.addItem(withTitle: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v")
|
||||
editMenu.addItem(withTitle: "Duplicate", action: #selector(EditorWindowController.duplicateSelection(_:)), keyEquivalent: "d")
|
||||
editMenu.addItem(withTitle: "Delete", action: #selector(EditorWindowController.deleteSelection(_:)), keyEquivalent: "\u{8}")
|
||||
editItem.submenu = editMenu
|
||||
mainMenu.addItem(editItem)
|
||||
|
||||
// Capture
|
||||
let captureItem = NSMenuItem()
|
||||
let captureMenu = NSMenu(title: "Capture")
|
||||
captureMenu.addItem(withTitle: "Region", action: #selector(CaptureMenuTarget.captureRegion), keyEquivalent: "")
|
||||
captureMenu.addItem(withTitle: "Window", action: #selector(CaptureMenuTarget.captureWindow), keyEquivalent: "")
|
||||
captureMenu.addItem(withTitle: "Full screen", action: #selector(CaptureMenuTarget.captureFullscreen), keyEquivalent: "")
|
||||
captureMenu.addItem(withTitle: "Last region", action: #selector(CaptureMenuTarget.captureLastRegion), keyEquivalent: "")
|
||||
for menuItem in captureMenu.items { menuItem.target = CaptureMenuTarget.shared }
|
||||
captureItem.submenu = captureMenu
|
||||
mainMenu.addItem(captureItem)
|
||||
|
||||
// Tools
|
||||
let toolsItem = NSMenuItem()
|
||||
let toolsMenu = NSMenu(title: "Tools")
|
||||
for tool in ToolKind.allCases {
|
||||
let menuItem = NSMenuItem(title: tool.title,
|
||||
action: #selector(EditorWindowController.selectTool(_:)),
|
||||
keyEquivalent: tool.keyEquivalent)
|
||||
menuItem.keyEquivalentModifierMask = []
|
||||
menuItem.representedObject = tool.rawValue
|
||||
toolsMenu.addItem(menuItem)
|
||||
}
|
||||
toolsMenu.addItem(.separator())
|
||||
toolsMenu.addItem(withTitle: "Apply crop", action: #selector(EditorWindowController.applyCrop(_:)), keyEquivalent: "\r")
|
||||
toolsMenu.addItem(.separator())
|
||||
toolsMenu.addItem(withTitle: "Bring to Front", action: #selector(EditorWindowController.bringToFront(_:)), keyEquivalent: "")
|
||||
toolsMenu.addItem(withTitle: "Send to Back", action: #selector(EditorWindowController.sendToBack(_:)), keyEquivalent: "")
|
||||
toolsItem.submenu = toolsMenu
|
||||
mainMenu.addItem(toolsItem)
|
||||
|
||||
// View
|
||||
let viewItem = NSMenuItem()
|
||||
let viewMenu = NSMenu(title: "View")
|
||||
viewMenu.addItem(withTitle: "Zoom In", action: #selector(EditorWindowController.zoomIn(_:)), keyEquivalent: "+")
|
||||
viewMenu.addItem(withTitle: "Zoom Out", action: #selector(EditorWindowController.zoomOut(_:)), keyEquivalent: "-")
|
||||
viewMenu.addItem(withTitle: "Actual Size", action: #selector(EditorWindowController.actualSize(_:)), keyEquivalent: "0")
|
||||
viewItem.submenu = viewMenu
|
||||
mainMenu.addItem(viewItem)
|
||||
|
||||
NSApp.mainMenu = mainMenu
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class CaptureMenuTarget: NSObject {
|
||||
static let shared = CaptureMenuTarget()
|
||||
|
||||
@objc func captureRegion() { CaptureCoordinator.shared.captureRegion() }
|
||||
@objc func captureWindow() { CaptureCoordinator.shared.captureWindow() }
|
||||
@objc func captureFullscreen() { CaptureCoordinator.shared.captureFullscreen() }
|
||||
@objc func captureLastRegion() { CaptureCoordinator.shared.captureLastRegion() }
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
// StatusMenuController.swift
|
||||
// GreenshotMac
|
||||
//
|
||||
// The menu bar item, equivalent to Greenshot's tray icon and context menu.
|
||||
|
||||
import AppKit
|
||||
|
||||
@MainActor
|
||||
final class StatusMenuController: NSObject, NSMenuDelegate {
|
||||
|
||||
private let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
||||
private var preferencesController: PreferencesWindowController?
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
if let button = statusItem.button {
|
||||
button.image = NSImage(systemSymbolName: "camera.viewfinder", accessibilityDescription: "GreenshotMac")
|
||||
button.image?.isTemplate = true
|
||||
button.toolTip = "GreenshotMac"
|
||||
}
|
||||
statusItem.menu = buildMenu()
|
||||
}
|
||||
|
||||
private func buildMenu() -> NSMenu {
|
||||
let menu = NSMenu()
|
||||
menu.delegate = self
|
||||
|
||||
menu.addItem(item(title: "Capture region",
|
||||
action: #selector(captureRegion),
|
||||
hotkey: HotkeyBindings.shared.spec(for: .region)))
|
||||
menu.addItem(item(title: "Capture window",
|
||||
action: #selector(captureWindow),
|
||||
hotkey: HotkeyBindings.shared.spec(for: .window)))
|
||||
menu.addItem(item(title: "Capture full screen",
|
||||
action: #selector(captureFullscreen),
|
||||
hotkey: HotkeyBindings.shared.spec(for: .fullscreen)))
|
||||
menu.addItem(item(title: "Capture last region",
|
||||
action: #selector(captureLastRegion),
|
||||
hotkey: HotkeyBindings.shared.spec(for: .lastRegion)))
|
||||
|
||||
menu.addItem(.separator())
|
||||
menu.addItem(item(title: "Open image from file…", action: #selector(openImageFile), hotkey: nil))
|
||||
menu.addItem(item(title: "Open last screenshot in Finder", action: #selector(revealLast), hotkey: nil))
|
||||
|
||||
menu.addItem(.separator())
|
||||
let destinationItem = NSMenuItem(title: "Default destination", action: nil, keyEquivalent: "")
|
||||
destinationItem.submenu = destinationMenu()
|
||||
menu.addItem(destinationItem)
|
||||
menu.addItem(item(title: "Preferences…", action: #selector(showPreferences), hotkey: nil))
|
||||
|
||||
menu.addItem(.separator())
|
||||
menu.addItem(item(title: "About GreenshotMac", action: #selector(showAbout), hotkey: nil))
|
||||
menu.addItem(item(title: "Quit GreenshotMac", action: #selector(quit), hotkey: nil))
|
||||
return menu
|
||||
}
|
||||
|
||||
private func destinationMenu() -> NSMenu {
|
||||
let menu = NSMenu()
|
||||
for destination in DestinationID.allCases {
|
||||
let menuItem = NSMenuItem(title: destination.title,
|
||||
action: #selector(selectDestination(_:)),
|
||||
keyEquivalent: "")
|
||||
menuItem.target = self
|
||||
menuItem.representedObject = destination.rawValue
|
||||
menuItem.state = Configuration.shared.defaultDestination == destination ? .on : .off
|
||||
menu.addItem(menuItem)
|
||||
}
|
||||
return menu
|
||||
}
|
||||
|
||||
private func item(title: String, action: Selector, hotkey: HotkeySpec?) -> NSMenuItem {
|
||||
let menuItem = NSMenuItem(title: title, action: action, keyEquivalent: "")
|
||||
menuItem.target = self
|
||||
if let hotkey, !hotkey.isEmpty {
|
||||
menuItem.toolTip = HotkeyManager.description(of: hotkey)
|
||||
}
|
||||
return menuItem
|
||||
}
|
||||
|
||||
func menuWillOpen(_ menu: NSMenu) {
|
||||
for menuItem in menu.items where menuItem.title == "Default destination" {
|
||||
menuItem.submenu = destinationMenu()
|
||||
}
|
||||
if let revealItem = menu.items.first(where: { $0.title == "Open last screenshot in Finder" }) {
|
||||
revealItem.isEnabled = Notifier.mostRecentSavedURL != nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
@objc private func captureRegion() { CaptureCoordinator.shared.captureRegion() }
|
||||
@objc private func captureWindow() { CaptureCoordinator.shared.captureWindow() }
|
||||
@objc private func captureFullscreen() { CaptureCoordinator.shared.captureFullscreen() }
|
||||
@objc private func captureLastRegion() { CaptureCoordinator.shared.captureLastRegion() }
|
||||
@objc private func openImageFile() { CaptureCoordinator.shared.openImageFile() }
|
||||
@objc private func revealLast() { Notifier.revealMostRecent() }
|
||||
|
||||
@objc private func selectDestination(_ sender: NSMenuItem) {
|
||||
guard let raw = sender.representedObject as? String,
|
||||
let destination = DestinationID(rawValue: raw) else { return }
|
||||
Configuration.shared.defaultDestination = destination
|
||||
}
|
||||
|
||||
@objc private func showPreferences() {
|
||||
if preferencesController == nil {
|
||||
preferencesController = PreferencesWindowController()
|
||||
}
|
||||
preferencesController?.showWindow(nil)
|
||||
preferencesController?.window?.makeKeyAndOrderFront(nil)
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
}
|
||||
|
||||
@objc private func showAbout() {
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
NSApp.orderFrontStandardAboutPanel(options: [
|
||||
.applicationName: "GreenshotMac",
|
||||
.credits: NSAttributedString(
|
||||
string: "A native macOS screenshot tool modelled on Greenshot for Windows.\n"
|
||||
+ "Licensed under the GNU General Public License v3.",
|
||||
attributes: [.font: NSFont.systemFont(ofSize: 11)])
|
||||
])
|
||||
}
|
||||
|
||||
@objc private func quit() {
|
||||
NSApp.terminate(nil)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// main.swift
|
||||
// GreenshotMac
|
||||
|
||||
import AppKit
|
||||
|
||||
/// NSApplication.delegate is a weak reference, so the delegate is held here for the
|
||||
/// lifetime of the process.
|
||||
enum AppLauncher {
|
||||
@MainActor private static var retainedDelegate: AppDelegate?
|
||||
|
||||
@MainActor
|
||||
static func launch() {
|
||||
let application = NSApplication.shared
|
||||
let delegate = AppDelegate()
|
||||
retainedDelegate = delegate
|
||||
application.delegate = delegate
|
||||
application.setActivationPolicy(.accessory)
|
||||
application.run()
|
||||
}
|
||||
}
|
||||
|
||||
// Top-level code always runs on the main thread, so assuming main actor isolation is safe here.
|
||||
MainActor.assumeIsolated {
|
||||
AppLauncher.launch()
|
||||
}
|
||||
Reference in New Issue
Block a user