Skip to content

Commit

Permalink
add hotkey support, add macOS 12.3 support, add screen magnifier, add…
Browse files Browse the repository at this point in the history
… frameshot, fix UI layout
  • Loading branch information
lihaoyun6 committed Apr 26, 2024
1 parent 2bec323 commit 475228a
Show file tree
Hide file tree
Showing 14 changed files with 453 additions and 111 deletions.
43 changes: 37 additions & 6 deletions QuickRecorder.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
18D3BE012BCEB1BF006CFFC0 /* AppSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18D3BE002BCEB1BF006CFFC0 /* AppSelector.swift */; };
18D3BE042BCEC847006CFFC0 /* SCContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18D3BE032BCEC847006CFFC0 /* SCContext.swift */; };
18F1A0E22BD3E4C000DB102C /* AreaSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F1A0E12BD3E4C000DB102C /* AreaSelector.swift */; };
18FDFC8E2BDA435F0020E685 /* ScreenMagnifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FDFC8D2BDA435F0020E685 /* ScreenMagnifier.swift */; };
18FDFC912BDA4DB30020E685 /* KeyboardShortcuts in Frameworks */ = {isa = PBXBuildFile; productRef = 18FDFC902BDA4DB30020E685 /* KeyboardShortcuts */; };
18FEBDAA2BCF8200003F09BC /* RecordEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEBDA92BCF8200003F09BC /* RecordEngine.swift */; };
18FEBDAC2BD01E82003F09BC /* WinSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEBDAB2BD01E82003F09BC /* WinSelector.swift */; };
18FEBDAE2BD10ED1003F09BC /* ScreenSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEBDAD2BD10ED1003F09BC /* ScreenSelector.swift */; };
Expand All @@ -39,6 +41,7 @@
18D3BE002BCEB1BF006CFFC0 /* AppSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSelector.swift; sourceTree = "<group>"; };
18D3BE032BCEC847006CFFC0 /* SCContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SCContext.swift; sourceTree = "<group>"; };
18F1A0E12BD3E4C000DB102C /* AreaSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AreaSelector.swift; sourceTree = "<group>"; };
18FDFC8D2BDA435F0020E685 /* ScreenMagnifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenMagnifier.swift; sourceTree = "<group>"; };
18FEBDA92BCF8200003F09BC /* RecordEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordEngine.swift; sourceTree = "<group>"; };
18FEBDAB2BD01E82003F09BC /* WinSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WinSelector.swift; sourceTree = "<group>"; };
18FEBDAD2BD10ED1003F09BC /* ScreenSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenSelector.swift; sourceTree = "<group>"; };
Expand All @@ -50,6 +53,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
18FDFC912BDA4DB30020E685 /* KeyboardShortcuts in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -107,6 +111,7 @@
18FEBDAD2BD10ED1003F09BC /* ScreenSelector.swift */,
18F1A0E12BD3E4C000DB102C /* AreaSelector.swift */,
187966F92BD5639D003DB1B2 /* MousePointer.swift */,
18FDFC8D2BDA435F0020E685 /* ScreenMagnifier.swift */,
);
path = ViewModel;
sourceTree = "<group>";
Expand All @@ -127,6 +132,9 @@
dependencies = (
);
name = QuickRecorder;
packageProductDependencies = (
18FDFC902BDA4DB30020E685 /* KeyboardShortcuts */,
);
productName = QuickRecorder;
productReference = 18D3BDE72BCE5DC1006CFFC0 /* QuickRecorder.app */;
productType = "com.apple.product-type.application";
Expand Down Expand Up @@ -156,6 +164,9 @@
"zh-Hans",
);
mainGroup = 18D3BDDE2BCE5DC1006CFFC0;
packageReferences = (
18FDFC8F2BDA4DB30020E685 /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */,
);
productRefGroup = 18D3BDE82BCE5DC1006CFFC0 /* Products */;
projectDirPath = "";
projectRoot = "";
Expand Down Expand Up @@ -186,6 +197,7 @@
files = (
18FEBDAE2BD10ED1003F09BC /* ScreenSelector.swift in Sources */,
18F1A0E22BD3E4C000DB102C /* AreaSelector.swift in Sources */,
18FDFC8E2BDA435F0020E685 /* ScreenMagnifier.swift in Sources */,
187966FA2BD5639D003DB1B2 /* MousePointer.swift in Sources */,
18D3BDFF2BCE5E4B006CFFC0 /* StatusBar.swift in Sources */,
18D3BE042BCEC847006CFFC0 /* SCContext.swift in Sources */,
Expand Down Expand Up @@ -347,7 +359,7 @@
CODE_SIGN_ENTITLEMENTS = QuickRecorder/QuickRecorder.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 105;
CURRENT_PROJECT_VERSION = 107;
DEVELOPMENT_ASSET_PATHS = "\"QuickRecorder/Preview Content\"";
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES;
Expand All @@ -360,8 +372,8 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.0.5;
MACOSX_DEPLOYMENT_TARGET = 12.3;
MARKETING_VERSION = 1.0.7;
PRODUCT_BUNDLE_IDENTIFIER = com.lihaoyun6.QuickRecorder;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
Expand All @@ -377,7 +389,7 @@
CODE_SIGN_ENTITLEMENTS = QuickRecorder/QuickRecorder.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 105;
CURRENT_PROJECT_VERSION = 107;
DEVELOPMENT_ASSET_PATHS = "\"QuickRecorder/Preview Content\"";
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES;
Expand All @@ -390,8 +402,8 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.0.5;
MACOSX_DEPLOYMENT_TARGET = 12.3;
MARKETING_VERSION = 1.0.7;
PRODUCT_BUNDLE_IDENTIFIER = com.lihaoyun6.QuickRecorder;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
Expand Down Expand Up @@ -421,6 +433,25 @@
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */

/* Begin XCRemoteSwiftPackageReference section */
18FDFC8F2BDA4DB30020E685 /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/sindresorhus/KeyboardShortcuts.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 2.0.0;
};
};
/* End XCRemoteSwiftPackageReference section */

/* Begin XCSwiftPackageProductDependency section */
18FDFC902BDA4DB30020E685 /* KeyboardShortcuts */ = {
isa = XCSwiftPackageProductDependency;
package = 18FDFC8F2BDA4DB30020E685 /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */;
productName = KeyboardShortcuts;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 18D3BDDF2BCE5DC1006CFFC0 /* Project object */;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"pins" : [
{
"identity" : "keyboardshortcuts",
"kind" : "remoteSourceControl",
"location" : "https://github.com/sindresorhus/KeyboardShortcuts.git",
"state" : {
"revision" : "09e4a10ed6b65b3a40fe07b6bf0c84809313efc4",
"version" : "2.0.0"
}
}
],
"version" : 2
}
164 changes: 156 additions & 8 deletions QuickRecorder/QuickRecorderApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,20 @@ import AVFAudio
import AVFoundation
import ScreenCaptureKit
import UserNotifications
import KeyboardShortcuts
import ServiceManagement

var firstRun = true
let ud = UserDefaults.standard
var statusMenu: NSMenu = NSMenu()
var statusBarItem: NSStatusItem!
var mouseMonitor: Any?
var hideMousePointer = false
var hideScreenMagnifier = false
let info = NSMenuItem(title: "Waiting on update…".local, action: nil, keyEquivalent: "")
let mousePointer = NSWindow(contentRect: NSRect(x: -70, y: -70, width: 70, height: 70), styleMask: [.borderless], backing: .buffered, defer: false)
let updateTimer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
let mousePointer = NSWindow(contentRect: NSRect(x: -70, y: -70, width: 70, height: 70), styleMask: [.borderless], backing: .buffered, defer: false)
let screenMagnifier = NSWindow(contentRect: NSRect(x: -402, y: -402, width: 402, height: 348), styleMask: [.borderless], backing: .buffered, defer: false)

@main
struct QuickRecorderApp: App {
Expand All @@ -32,9 +37,8 @@ struct QuickRecorderApp: App {
.fixedSize()
.onAppear { setMainWindow() }
}
//.windowStyle(HiddenTitleBarWindowStyle())
.windowResizability(.contentSize)
.commands { CommandGroup(replacing: .newItem) {} }
.myWindowIsContentResizable()

Settings {
SettingsView()
Expand All @@ -58,10 +62,22 @@ struct QuickRecorderApp: App {
}
}

extension Scene {
func myWindowIsContentResizable() -> some Scene {
if #available(macOS 13.0, *) {
return self.windowResizability(.contentSize)
}
else {
return self
}
}
}

class AppDelegate: NSObject, NSApplicationDelegate, SCStreamDelegate, SCStreamOutput {
var filter: SCContentFilter?

func mousePointerReLocation(event: NSEvent) {
if event.type == .scrollWheel { return }
if !ud.bool(forKey: "highlightMouse")
|| hideMousePointer
|| SCContext.stream == nil
Expand All @@ -75,16 +91,31 @@ class AppDelegate: NSObject, NSApplicationDelegate, SCStreamDelegate, SCStreamOu
mousePointer.orderFront(nil)
}

func screenMagnifierReLocation(event: NSEvent) {
if !SCContext.showMagnifier
|| hideScreenMagnifier
{ screenMagnifier.orderOut(nil); return }
let mouseLocation = event.locationInWindow
var windowFrame = screenMagnifier.frame
windowFrame.origin = NSPoint(x: mouseLocation.x - windowFrame.width / 2, y: mouseLocation.y - windowFrame.height / 2)
guard let image = NSImage.createScreenShot() else { return }
let rect = NSRect(x: mouseLocation.x - 67, y: mouseLocation.y - 58, width: 134, height: 116)
let croppedImage = image.trim(rect: rect)
//let newSize = NSSize(width: image.size.width * 8, height: image.size.height * 8)
//guard let newImage = croppedImage.resizedImageNearestNeighbor(to: newSize) else { return }
screenMagnifier.contentView = NSHostingView(rootView: ScreenMagnifier(screenShot: croppedImage, event: event))
screenMagnifier.setFrameOrigin(windowFrame.origin)
screenMagnifier.orderFront(nil)
}

func registerGlobalMouseMonitor() {
// 注册全局鼠标监听器
mouseMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.mouseMoved, .rightMouseUp, .rightMouseDown, .rightMouseDragged, .leftMouseUp, .leftMouseDown, .leftMouseDragged, .otherMouseUp, .otherMouseDown, .otherMouseDragged]) { event in
// 处理鼠标事件
mouseMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.scrollWheel, .mouseMoved, .rightMouseUp, .rightMouseDown, .rightMouseDragged, .leftMouseUp, .leftMouseDown, .leftMouseDragged, .otherMouseUp, .otherMouseDown, .otherMouseDragged]) { event in
self.mousePointerReLocation(event: event)
self.screenMagnifierReLocation(event: event)
}
}

func stopGlobalMouseMonitor() {
// 停止全局鼠标监听器
mousePointer.orderOut(nil)
if let monitor = mouseMonitor { NSEvent.removeMonitor(monitor) }
}
Expand Down Expand Up @@ -128,22 +159,139 @@ class AppDelegate: NSObject, NSApplicationDelegate, SCStreamDelegate, SCStreamOu
statusMenu.delegate = self
statusBarItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
statusBarItem.menu = statusMenu

mousePointer.title = "Mouse Pointer".local
mousePointer.level = .screenSaver
mousePointer.ignoresMouseEvents = true
mousePointer.isReleasedWhenClosed = false
mousePointer.backgroundColor = NSColor.clear
//if ud.bool(forKey: "highlightMouse") { registerGlobalMouseMonitor() }

screenMagnifier.title = "Screen Magnifier".local
screenMagnifier.level = .floating
screenMagnifier.ignoresMouseEvents = true
screenMagnifier.isReleasedWhenClosed = false
screenMagnifier.backgroundColor = NSColor.clear

KeyboardShortcuts.onKeyDown(for: .saveFrame) { [] in SCContext.saveFrame = true }
KeyboardShortcuts.onKeyDown(for: .screenMagnifier) { [] in SCContext.showMagnifier.toggle() }
KeyboardShortcuts.onKeyDown(for: .stop) { [] in if SCContext.stream != nil { SCContext.stopRecording() }}
KeyboardShortcuts.onKeyDown(for: .pauseResume) { [] in if SCContext.stream != nil { SCContext.pauseRecording() }}
KeyboardShortcuts.onKeyDown(for: .startWithScreen) {[self] in
for w in NSApplication.shared.windows.filter({ $0.title == "QuickReader".local }) { w.close() }
prepRecord(type: "display", screens: SCContext.getSCDisplayWithMouse(), windows: nil, applications: nil, fastStart: true)
}
KeyboardShortcuts.onKeyDown(for: .startWithWindow) { [self] in
for w in NSApplication.shared.windows.filter({ $0.title == "QuickReader".local }) { w.close() }
let frontmostApp = NSWorkspace.shared.frontmostApplication
if let pid = frontmostApp?.processIdentifier {
let options: CGWindowListOption = .optionOnScreenOnly
let windowListInfo = CGWindowListCopyWindowInfo(options, kCGNullWindowID)
if let infoList = windowListInfo as? [[String: AnyObject]] {
for info in infoList {
if let windowPID = info[kCGWindowOwnerPID as String] as? pid_t, windowPID == pid {
if let windowNumber = info[kCGWindowNumber as String] as? CGWindowID {
guard let scWindow = SCContext.getWindows().first(where: { $0.windowID == windowNumber }) else { return }
prepRecord(type: "window", screens: SCContext.getSCDisplayWithMouse(), windows: [scWindow], applications: nil, fastStart: true)
return
}
}
}
}
}
}

UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
if let error = error { print("Notification authorization denied: \(error.localizedDescription)") }
}
if #available(macOS 13, *) {
if firstRun && (SMAppService.mainApp.status == .enabled) {
firstRun = false
NSApp.windows.forEach { $0.close() }
}
}
}
}

extension String {
var local: String { return NSLocalizedString(self, comment: "") }
}

extension NSImage {
static func createScreenShot() -> NSImage? {
let excludedAppBundleIDs = ["com.lihaoyun6.QuickRecorder"]
var exclusionPIDs = [Int]()
for app in NSWorkspace.shared.runningApplications {
if excludedAppBundleIDs.contains(app.bundleIdentifier ?? "") {
exclusionPIDs.append(Int(app.processIdentifier))
}
}

let windowDescriptions = CGWindowListCopyWindowInfo(.optionOnScreenOnly, kCGNullWindowID) as? [[String: Any]] ?? []
var windowIDs = [CGWindowID]()
for windowDict in windowDescriptions {
if let windowProcessID = windowDict[kCGWindowOwnerPID as String] as? Int,
!exclusionPIDs.contains(windowProcessID),
let windowID = windowDict[kCGWindowNumber as String] as? CGWindowID {
windowIDs.append(windowID)
}
}
let pointer = UnsafeMutablePointer<UnsafeRawPointer?>.allocate(capacity: windowIDs.count)
for (index, window) in windowIDs.enumerated() { pointer[index] = UnsafeRawPointer(bitPattern: UInt(window)) }
let cWindowIDArray: CFArray = CFArrayCreate(kCFAllocatorDefault, pointer, windowIDs.count, nil)

guard let imageRef = CGImage(windowListFromArrayScreenBounds: CGRect.infinite, windowArray: cWindowIDArray, imageOption: []) else {
print("No image available")
return nil
}
let factor = SCContext.getScreenWithMouse()?.backingScaleFactor ?? 1.0
return NSImage(cgImage: imageRef, size: NSSize(width: CGFloat(imageRef.width)/factor, height: CGFloat(imageRef.height)/factor))
}

static func screenshot() -> NSImage? {
let mouseLocation = NSEvent.mouseLocation
guard let screen = NSScreen.screens.first(where: { NSMouseInRect(mouseLocation, $0.frame, false) }) else { return nil }
let frame = screen.frame
let image = CGDisplayCreateImage(CGMainDisplayID(), rect: frame)!
return NSImage(cgImage: image, size: frame.size)
}

func resizedImageNearestNeighbor(to size: NSSize) -> NSImage? {
guard let cgImage = self.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return nil }
let bitsPerComponent = cgImage.bitsPerComponent
let bytesPerRow = 8 * size.width
let colorSpace = cgImage.colorSpace ?? CGColorSpaceCreateDeviceRGB() // 提供默认的颜色空间
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.noneSkipLast.rawValue)
guard let context = CGContext(data: nil, width: Int(size.width), height: Int(size.height), bitsPerComponent: bitsPerComponent, bytesPerRow: Int(bytesPerRow), space: colorSpace, bitmapInfo: bitmapInfo.rawValue) else { return nil }
context.interpolationQuality = .none
context.draw(cgImage, in: CGRect(origin: .zero, size: size))
if let scaledImage = context.makeImage() { return NSImage(cgImage: scaledImage, size: size) }
return nil
}

func saveToFile(_ url: URL) {
if let tiffData = self.tiffRepresentation,
let imageRep = NSBitmapImageRep(data: tiffData) {
let pngData = imageRep.representation(using: .png, properties: [:])
do {
try pngData?.write(to: url)
} catch {
print("Error saving image: \(error.localizedDescription)")
}
}
}

func trim(rect: CGRect) -> NSImage {
let result = NSImage(size: rect.size)
result.lockFocus()

let destRect = CGRect(origin: .zero, size: result.size)
self.draw(in: destRect, from: rect, operation: .copy, fraction: 1.0)

result.unlockFocus()
return result
}
}

enum AudioQuality: Int { case normal = 128, good = 192, high = 256, extreme = 320 }

enum AudioFormat: String { case aac, alac, flac, opus }
Expand Down
Loading

0 comments on commit 475228a

Please sign in to comment.