feat: add debug tools
This commit is contained in:
1 parent
8cab3e1f8b
commit
07e4024a9c
8 files changed
+461
-13
No files matched your search
@@ -227,6 +227,7 @@
|
||||
D04BA2A6FF9DDDC5A1A68E36 /* Applications.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA282BB16C1554595A968 /* Applications.swift */; };
|
||||
D04BA2D8099594E1593EC8EF /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = D04BA88EDDDD6CB3FBE71FC5 /* InfoPlist.strings */; };
|
||||
D04BA2E64C59D96F6EB27D9D /* FeedbackWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA26C75F76C277653C932 /* FeedbackWindow.swift */; };
|
||||
D04BA2F0A1B2C3D4E5F60003 /* DebugWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA2F0A1B2C3D4E5F60004 /* DebugWindow.swift */; };
|
||||
D04BA328F7C85DD1A6C1C7ED /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D04BAB163FDEE79F096A410A /* Localizable.strings */; };
|
||||
D04BA34AC850A273AB288B1E /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D04BA3B51D05213404938366 /* Localizable.strings */; };
|
||||
D04BA3744F48116DF4252B19 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D04BA02355EB28D639F854DF /* Localizable.strings */; };
|
||||
@@ -604,6 +605,7 @@
|
||||
D04BA2D022B64A876CCA6BB7 /* apple-touch-icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "apple-touch-icon.png"; sourceTree = "<group>"; };
|
||||
D04BA2E723062A1DAC58618F /* setup_ci_pr.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = setup_ci_pr.sh; sourceTree = "<group>"; };
|
||||
D04BA2E8D5D5CC23456BC8F7 /* tr */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = tr; path = Localizable.strings; sourceTree = "<group>"; };
|
||||
D04BA2F0A1B2C3D4E5F60004 /* DebugWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DebugWindow.swift; sourceTree = "<group>"; };
|
||||
D04BA32F25860B686DFE818A /* 3 windows - 1 line.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "3 windows - 1 line.jpg"; sourceTree = "<group>"; };
|
||||
D04BA3632A2EB5C64A3FEE59 /* hu */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = hu; path = InfoPlist.strings; sourceTree = "<group>"; };
|
||||
D04BA369B4D9FD6A13936F53 /* changelog-bare.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = "changelog-bare.html"; sourceTree = "<group>"; };
|
||||
@@ -1471,6 +1473,7 @@
|
||||
D04BABCD12CA72B06290C248 /* settings-window */,
|
||||
D04BAD905546AA93E5117B0A /* Menubar.swift */,
|
||||
D04BA26C75F76C277653C932 /* FeedbackWindow.swift */,
|
||||
D04BA2F0A1B2C3D4E5F60004 /* DebugWindow.swift */,
|
||||
D04BA2CBF267F267225CA671 /* generic-components */,
|
||||
D04BA1C271F0DA573345B860 /* permission-window */,
|
||||
D04BAAF79737A0673502FEB5 /* AppCenterApplication.h */,
|
||||
@@ -2511,6 +2514,7 @@
|
||||
D04BA8092885B40CE3527370 /* PoliciesTab.swift in Sources */,
|
||||
D04BAD451966B43720120D2E /* Menubar.swift in Sources */,
|
||||
D04BA2E64C59D96F6EB27D9D /* FeedbackWindow.swift in Sources */,
|
||||
D04BA2F0A1B2C3D4E5F60003 /* DebugWindow.swift in Sources */,
|
||||
D04BA76A74267B1346D23687 /* GridView.swift in Sources */,
|
||||
D04BAB4EB890853B5B9B2C61 /* BoldLabel.swift in Sources */,
|
||||
D04BAABE804F3769CE22BEB6 /* HyperlinkLabel.swift in Sources */,
|
||||
|
||||
@@ -172,6 +172,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Dark" = "Dark";
|
||||
|
||||
/* Menubar option */
|
||||
"Debug tools" = "Debug tools";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Disabled" = "Disabled";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"About" = "Az App-ról";
|
||||
"About %@" = "Körülbelül %@";
|
||||
"About %@" = "Az %@ról";
|
||||
"Accessibility" = "Kisegítő lehetőségek";
|
||||
"Acknowledgments" = "Köszönetnyílvánítás";
|
||||
"Active app" = "Aktív alkalmazás";
|
||||
@@ -126,7 +126,7 @@
|
||||
"Preview selected window:" = "Választott ablak előnézete:";
|
||||
"Preview the selected window." = "A kiválasztott ablak előnézete";
|
||||
"Quit" = "Kilépés";
|
||||
"Quit %@" = "Kilépés %@";
|
||||
"Quit %@" = "Kilépés az %@ból";
|
||||
"Quit app" = "Kilépés az alkalmazásból";
|
||||
"Recently Created" = "Nemrég Elindított";
|
||||
"Recently Created First" = "Mostanában megnyitott";
|
||||
|
||||
@@ -10,16 +10,31 @@ class Logger {
|
||||
static func initialize() {
|
||||
let console = ConsoleDestination()
|
||||
console.useTerminalColors = true
|
||||
console.levelString.verbose = "VERB"
|
||||
console.levelString.debug = "DEBG"
|
||||
console.levelString.info = "INFO"
|
||||
console.levelString.warning = "WARN"
|
||||
console.levelString.error = "ERRO"
|
||||
console.format = "$C$D\(longDateTimeFormat)$d $L$c $N.swift:$l $F $M"
|
||||
configureDestination(console)
|
||||
console.format = "$C$D\(shortDateTimeFormat)$d $L$c $N.swift:$l $F $M"
|
||||
console.minLevel = decideLevel()
|
||||
logger.addDestination(console)
|
||||
}
|
||||
|
||||
static func configureDestination(_ dest: BaseDestination) {
|
||||
dest.levelString.verbose = "VERB"
|
||||
dest.levelString.debug = "DEBG"
|
||||
dest.levelString.info = "INFO"
|
||||
dest.levelString.warning = "WARN"
|
||||
dest.levelString.error = "ERRO"
|
||||
dest.format = "$D\(shortDateTimeFormat)$d $L $N.swift:$l $F $M"
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
static func addDestination(_ dest: BaseDestination) -> Bool {
|
||||
logger.addDestination(dest)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
static func removeDestination(_ dest: BaseDestination) -> Bool {
|
||||
logger.removeDestination(dest)
|
||||
}
|
||||
|
||||
static func decideLevel() -> SwiftyBeaver.Level {
|
||||
if let level = (CommandLine.arguments.first { $0.starts(with: flag) })?.dropFirst(flag.count) {
|
||||
switch level {
|
||||
@@ -66,3 +81,26 @@ class Logger {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// custom destination to display logs in the debug window
|
||||
class DebugWindowDestination: BaseDestination {
|
||||
var onNewEntry: ((SwiftyBeaver.Level, String) -> Void)?
|
||||
|
||||
override var defaultHashValue: Int { return 2 }
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
Logger.configureDestination(self)
|
||||
minLevel = .debug
|
||||
}
|
||||
|
||||
override func send(_ level: SwiftyBeaver.Level, msg: String, thread: String,
|
||||
file: String, function: String, line: Int, context: Any? = nil) -> String? {
|
||||
let formattedString = super.send(level, msg: msg, thread: thread,
|
||||
file: file, function: function, line: line, context: context)
|
||||
guard let formatted = formattedString else { return nil }
|
||||
let callback = onNewEntry
|
||||
DispatchQueue.main.async { callback?(level, formatted) }
|
||||
return formattedString
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ class WindowDiscriminator {
|
||||
// Minimized windows or windows of a hidden app have subrole "AXDialog"
|
||||
// Activity Monitor main window subrole is "AXDialog" for a brief moment at launch; it then becomes "AXStandardWindow"
|
||||
// Some non-windows have cgWindowId == 0 (e.g. windows of apps starting at login with the checkbox "Hidden" checked)
|
||||
// Logger.error { (app.bundleIdentifier, level, title, subrole, role, size) }
|
||||
Logger.debug { (app.debugId, wid, level, title, subrole, role, size) }
|
||||
return wid != 0
|
||||
// Finder's file copy dialogs are wide but < 100 height (see https://github.com/lwouis/alt-tab-macos/issues/1466)
|
||||
// Sonoma introduced a bug: a caps-lock & language indicators shows as a small window.
|
||||
|
||||
+17
-4
@@ -21,13 +21,14 @@ class App: AppCenterApplication {
|
||||
var isTerminating = false
|
||||
var thumbnailsPanel: TilesPanel!
|
||||
var previewPanel: PreviewPanel!
|
||||
var settingsWindow: SettingsWindow!
|
||||
var aboutWindow: AboutWindow!
|
||||
var permissionsWindow: PermissionsWindow?
|
||||
var appIsBeingUsed = false
|
||||
var shortcutIndex = 0
|
||||
var forceDoNothingOnRelease = false
|
||||
var settingsWindow: SettingsWindow!
|
||||
var aboutWindow: AboutWindow!
|
||||
var permissionsWindow: PermissionsWindow?
|
||||
private var feedbackWindow: FeedbackWindow!
|
||||
private var debugWindow: DebugWindow!
|
||||
private var isFirstSummon = true
|
||||
private var isVeryFirstSummon = true
|
||||
// periphery:ignore
|
||||
@@ -95,8 +96,9 @@ class App: AppCenterApplication {
|
||||
private func allSecondaryWindowsCanBecomeKey(_ canBecomeKey_: Bool) {
|
||||
settingsWindow?.canBecomeKey_ = canBecomeKey_
|
||||
aboutWindow?.canBecomeKey_ = canBecomeKey_
|
||||
feedbackWindow?.canBecomeKey_ = canBecomeKey_
|
||||
permissionsWindow?.canBecomeKey_ = canBecomeKey_
|
||||
feedbackWindow?.canBecomeKey_ = canBecomeKey_
|
||||
debugWindow?.canBecomeKey_ = canBecomeKey_
|
||||
}
|
||||
|
||||
func closeSelectedWindow() {
|
||||
@@ -161,6 +163,10 @@ class App: AppCenterApplication {
|
||||
showSecondaryWindow(getOrCreateFeedbackWindow())
|
||||
}
|
||||
|
||||
@objc func showDebugWindow() {
|
||||
showSecondaryWindow(getOrCreateDebugWindow())
|
||||
}
|
||||
|
||||
@objc func showSettingsWindow() {
|
||||
showSecondaryWindow(getOrCreateSettingsWindow())
|
||||
if settingsWindow?.isVisible != true {
|
||||
@@ -206,6 +212,13 @@ class App: AppCenterApplication {
|
||||
return feedbackWindow
|
||||
}
|
||||
|
||||
private func getOrCreateDebugWindow() -> DebugWindow {
|
||||
if debugWindow == nil {
|
||||
debugWindow = DebugWindow()
|
||||
}
|
||||
return debugWindow
|
||||
}
|
||||
|
||||
private func getOrCreatePermissionsWindow() -> PermissionsWindow {
|
||||
if permissionsWindow == nil {
|
||||
permissionsWindow = PermissionsWindow()
|
||||
|
||||
@@ -0,0 +1,389 @@
|
||||
import Cocoa
|
||||
import SwiftyBeaver
|
||||
|
||||
class DebugWindow: NSPanel {
|
||||
var canBecomeKey_ = true
|
||||
override var canBecomeKey: Bool { canBecomeKey_ }
|
||||
private var scrollView: NSScrollView!
|
||||
private var textView: NSTextView!
|
||||
private var filterControl: NSSegmentedControl!
|
||||
private var selectedMinLevel: SwiftyBeaver.Level = .debug
|
||||
private var isAutoScrolling = true
|
||||
private var isPerformingAutoScroll = false
|
||||
private var entries = [(SwiftyBeaver.Level, String)]()
|
||||
private var destination: DebugWindowDestination?
|
||||
private var inspectButton: NSButton!
|
||||
private var inspectColumns: NSStackView!
|
||||
private var inspectAppField: NSTextField!
|
||||
private var inspectCgField: NSTextField!
|
||||
private var inspectAxField: NSTextField!
|
||||
private var inspectTimer: Timer?
|
||||
private var inspectClickMonitor: Any?
|
||||
private var isInspecting = false
|
||||
private static let logFont = NSFont.userFixedPitchFont(ofSize: 11)!
|
||||
private static let levels: [SwiftyBeaver.Level] = [.debug, .info, .warning, .error]
|
||||
private static let defaultAttrs: [NSAttributedString.Key: Any] = [.font: logFont, .foregroundColor: NSColor.textColor]
|
||||
private static let levelWords: [SwiftyBeaver.Level: String] = [.debug: "DEBG", .info: "INFO", .warning: "WARN", .error: "ERRO"]
|
||||
|
||||
convenience init() {
|
||||
self.init(contentRect: NSRect(x: 0, y: 0, width: 1100, height: 750),
|
||||
styleMask: [.titled, .utilityWindow, .closable, .miniaturizable, .resizable],
|
||||
backing: .buffered, defer: false)
|
||||
setupWindow()
|
||||
setupView()
|
||||
}
|
||||
|
||||
private func setupWindow() {
|
||||
title = NSLocalizedString("Debug tools", comment: "")
|
||||
hidesOnDeactivate = false
|
||||
isReleasedWhenClosed = false
|
||||
minSize = NSSize(width: 400, height: 300)
|
||||
}
|
||||
|
||||
private static func colorForLevel(_ level: SwiftyBeaver.Level) -> NSColor {
|
||||
switch level {
|
||||
case .debug: return NSColor(calibratedRed: 0.0, green: 0.7, blue: 0.0, alpha: 1.0)
|
||||
case .info: return NSColor(calibratedRed: 0.0, green: 0.5, blue: 0.85, alpha: 1.0)
|
||||
case .warning: return NSColor(calibratedRed: 0.82, green: 0.62, blue: 0.0, alpha: 1.0)
|
||||
case .error: return NSColor(calibratedRed: 0.85, green: 0.15, blue: 0.3, alpha: 1.0)
|
||||
default: return .textColor
|
||||
}
|
||||
}
|
||||
|
||||
private static func colorDot(_ color: NSColor) -> NSImage {
|
||||
let size = NSSize(width: 8, height: 8)
|
||||
let image = NSImage(size: size)
|
||||
image.lockFocus()
|
||||
color.setFill()
|
||||
NSBezierPath(ovalIn: NSRect(origin: .zero, size: size)).fill()
|
||||
image.unlockFocus()
|
||||
return image
|
||||
}
|
||||
|
||||
private func setupView() {
|
||||
// Inspect section
|
||||
inspectButton = NSButton(title: "Inspect window", target: nil, action: nil)
|
||||
inspectButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
inspectButton.bezelStyle = .rounded
|
||||
inspectButton.onAction = { [weak self] _ in self?.toggleInspect() }
|
||||
inspectAppField = Self.makeInspectColumn("App", ["Name", "BundleID", "PID"])
|
||||
inspectCgField = Self.makeInspectColumn("Window (CG API)", ["Title", "WID", "Level (kCGWindowLayer)", "Level (CGSGetWindowLevel)", "Size", "Position", "Alpha", "IsOnScreen"])
|
||||
inspectAxField = Self.makeInspectColumn("Window (AX API)", ["Title", "Role", "Subrole", "Minimized", "Fullscreen", "Size", "Position"])
|
||||
inspectColumns = NSStackView(views: [inspectAppField, inspectCgField, inspectAxField])
|
||||
inspectColumns.translatesAutoresizingMaskIntoConstraints = false
|
||||
inspectColumns.orientation = .horizontal
|
||||
inspectColumns.alignment = .top
|
||||
inspectColumns.spacing = 16
|
||||
inspectColumns.distribution = .fillEqually
|
||||
// Log header and filter
|
||||
let filterLabel = NSTextField(labelWithString: "Log Level:")
|
||||
filterLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
filterControl = NSSegmentedControl(labels: ["Debug", "Info", "Warning", "Error"],
|
||||
trackingMode: .selectOne, target: nil, action: nil)
|
||||
filterControl.translatesAutoresizingMaskIntoConstraints = false
|
||||
filterControl.selectedSegment = 0
|
||||
filterControl.onAction = { [weak self] _ in self?.filterChanged() }
|
||||
for i in 0..<Self.levels.count {
|
||||
filterControl.setImage(Self.colorDot(Self.colorForLevel(Self.levels[i])), forSegment: i)
|
||||
filterControl.setImageScaling(.scaleProportionallyDown, forSegment: i)
|
||||
}
|
||||
let filterRow = NSStackView(views: [filterLabel, filterControl])
|
||||
filterRow.translatesAutoresizingMaskIntoConstraints = false
|
||||
filterRow.orientation = .horizontal
|
||||
filterRow.spacing = 8
|
||||
// Log scroll view
|
||||
scrollView = NSScrollView()
|
||||
scrollView.translatesAutoresizingMaskIntoConstraints = false
|
||||
scrollView.hasVerticalScroller = true
|
||||
scrollView.hasHorizontalScroller = true
|
||||
scrollView.autohidesScrollers = true
|
||||
scrollView.drawsBackground = true
|
||||
textView = NSTextView()
|
||||
textView.isEditable = false
|
||||
textView.isSelectable = true
|
||||
textView.isRichText = true
|
||||
textView.font = Self.logFont
|
||||
textView.isHorizontallyResizable = true
|
||||
textView.maxSize = NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
|
||||
textView.textContainer?.widthTracksTextView = false
|
||||
textView.textContainer?.containerSize = NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
|
||||
textView.drawsBackground = true
|
||||
textView.backgroundColor = NSColor(calibratedWhite: 0.95, alpha: 1.0)
|
||||
scrollView.documentView = textView
|
||||
scrollView.contentView.postsBoundsChangedNotifications = true
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(scrollViewDidScroll),
|
||||
name: NSView.boundsDidChangeNotification,
|
||||
object: scrollView.contentView)
|
||||
// Inspect group box
|
||||
let inspectBox = NSBox()
|
||||
inspectBox.translatesAutoresizingMaskIntoConstraints = false
|
||||
inspectBox.title = "Inspect"
|
||||
inspectBox.contentView = NSView()
|
||||
inspectBox.contentView!.addSubview(inspectButton)
|
||||
inspectBox.contentView!.addSubview(inspectColumns)
|
||||
NSLayoutConstraint.activate([
|
||||
inspectButton.topAnchor.constraint(equalTo: inspectBox.contentView!.topAnchor, constant: 4),
|
||||
inspectButton.leadingAnchor.constraint(equalTo: inspectBox.contentView!.leadingAnchor, constant: 4),
|
||||
inspectColumns.topAnchor.constraint(equalTo: inspectButton.bottomAnchor, constant: 8),
|
||||
inspectColumns.leadingAnchor.constraint(equalTo: inspectBox.contentView!.leadingAnchor, constant: 4),
|
||||
inspectColumns.trailingAnchor.constraint(equalTo: inspectBox.contentView!.trailingAnchor, constant: -4),
|
||||
inspectColumns.bottomAnchor.constraint(equalTo: inspectBox.contentView!.bottomAnchor, constant: -4),
|
||||
])
|
||||
// Log group box
|
||||
let logBox = NSBox()
|
||||
logBox.translatesAutoresizingMaskIntoConstraints = false
|
||||
logBox.title = "Logs"
|
||||
logBox.contentView = NSView()
|
||||
logBox.contentView!.addSubview(filterRow)
|
||||
logBox.contentView!.addSubview(scrollView)
|
||||
NSLayoutConstraint.activate([
|
||||
filterRow.topAnchor.constraint(equalTo: filterRow.superview!.topAnchor, constant: 8),
|
||||
filterRow.leadingAnchor.constraint(equalTo: logBox.contentView!.leadingAnchor, constant: 4),
|
||||
scrollView.topAnchor.constraint(equalTo: filterRow.bottomAnchor, constant: 8),
|
||||
scrollView.leadingAnchor.constraint(equalTo: logBox.contentView!.leadingAnchor, constant: 4),
|
||||
scrollView.trailingAnchor.constraint(equalTo: logBox.contentView!.trailingAnchor, constant: -4),
|
||||
scrollView.bottomAnchor.constraint(equalTo: logBox.contentView!.bottomAnchor, constant: -4),
|
||||
])
|
||||
// Main container
|
||||
let container = NSView()
|
||||
container.translatesAutoresizingMaskIntoConstraints = false
|
||||
container.addSubview(inspectBox)
|
||||
container.addSubview(logBox)
|
||||
contentView = container
|
||||
let padding: CGFloat = 12
|
||||
NSLayoutConstraint.activate([
|
||||
inspectBox.topAnchor.constraint(equalTo: container.topAnchor, constant: padding),
|
||||
inspectBox.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: padding),
|
||||
inspectBox.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -padding),
|
||||
logBox.topAnchor.constraint(equalTo: inspectBox.bottomAnchor, constant: padding),
|
||||
logBox.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: padding),
|
||||
logBox.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -padding),
|
||||
logBox.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: -padding),
|
||||
])
|
||||
}
|
||||
|
||||
private func startListening() {
|
||||
guard destination == nil else { return }
|
||||
let dest = DebugWindowDestination()
|
||||
dest.onNewEntry = { [weak self] level, message in
|
||||
self?.appendEntry(level, message)
|
||||
}
|
||||
Logger.addDestination(dest)
|
||||
destination = dest
|
||||
}
|
||||
|
||||
private func stopListening() {
|
||||
guard let dest = destination else { return }
|
||||
Logger.removeDestination(dest)
|
||||
destination = nil
|
||||
}
|
||||
|
||||
private func filterChanged() {
|
||||
selectedMinLevel = Self.levels[filterControl.selectedSegment]
|
||||
rebuildText()
|
||||
}
|
||||
|
||||
private func attributedLine(_ text: String, _ level: SwiftyBeaver.Level) -> NSAttributedString {
|
||||
let result = NSMutableAttributedString(string: text, attributes: Self.defaultAttrs)
|
||||
guard let word = Self.levelWords[level],
|
||||
let range = text.range(of: word) else { return result }
|
||||
result.addAttribute(.foregroundColor, value: Self.colorForLevel(level),
|
||||
range: NSRange(range, in: text))
|
||||
return result
|
||||
}
|
||||
|
||||
private func rebuildText() {
|
||||
let filtered = entries.filter { $0.0.rawValue >= selectedMinLevel.rawValue }
|
||||
let result = NSMutableAttributedString()
|
||||
for (i, entry) in filtered.enumerated() {
|
||||
if i > 0 { result.append(NSAttributedString(string: "\n", attributes: Self.defaultAttrs)) }
|
||||
result.append(attributedLine(entry.1, entry.0))
|
||||
}
|
||||
textView.textStorage?.setAttributedString(result)
|
||||
textView.needsDisplay = true
|
||||
if isAutoScrolling {
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
||||
|
||||
private func appendEntry(_ level: SwiftyBeaver.Level, _ message: String) {
|
||||
entries.append((level, message))
|
||||
guard level.rawValue >= selectedMinLevel.rawValue else { return }
|
||||
let prefix = textView.string.isEmpty ? "" : "\n"
|
||||
textView.textStorage?.append(attributedLine(prefix + message, level))
|
||||
if isAutoScrolling {
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
||||
|
||||
private func scrollToBottom() {
|
||||
textView.layoutManager?.ensureLayout(for: textView.textContainer!)
|
||||
guard let documentView = scrollView.documentView else { return }
|
||||
let clipView = scrollView.contentView
|
||||
let maxY = documentView.frame.height - clipView.bounds.height
|
||||
guard maxY > 0 else { return }
|
||||
isPerformingAutoScroll = true
|
||||
clipView.setBoundsOrigin(NSPoint(x: clipView.bounds.origin.x, y: maxY))
|
||||
isPerformingAutoScroll = false
|
||||
}
|
||||
|
||||
@objc private func scrollViewDidScroll(_ notification: Notification) {
|
||||
guard !isPerformingAutoScroll,
|
||||
let documentView = scrollView.documentView else { return }
|
||||
let clipView = scrollView.contentView
|
||||
let contentHeight = documentView.frame.height
|
||||
let scrollOffset = clipView.bounds.origin.y + clipView.bounds.height
|
||||
isAutoScrolling = scrollOffset >= contentHeight - 1
|
||||
}
|
||||
|
||||
override func makeKeyAndOrderFront(_ sender: Any?) {
|
||||
startListening()
|
||||
super.makeKeyAndOrderFront(sender)
|
||||
}
|
||||
|
||||
override func close() {
|
||||
stopInspecting()
|
||||
stopListening()
|
||||
clearInspectData()
|
||||
entries.removeAll()
|
||||
textView.textStorage?.setAttributedString(NSAttributedString())
|
||||
selectedMinLevel = .debug
|
||||
filterControl.selectedSegment = 0
|
||||
isAutoScrolling = true
|
||||
hideAppIfLastWindowIsClosed()
|
||||
super.close()
|
||||
}
|
||||
|
||||
private func toggleInspect() {
|
||||
if isInspecting { stopInspecting() } else { startInspecting() }
|
||||
}
|
||||
|
||||
private func startInspecting() {
|
||||
isInspecting = true
|
||||
inspectButton.title = "Stop inspecting"
|
||||
inspectTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in
|
||||
self?.updateInspectData()
|
||||
}
|
||||
inspectClickMonitor = NSEvent.addGlobalMonitorForEvents(matching: .leftMouseDown) { [weak self] _ in
|
||||
self?.stopInspecting()
|
||||
}
|
||||
}
|
||||
|
||||
private func stopInspecting() {
|
||||
guard isInspecting else { return }
|
||||
isInspecting = false
|
||||
inspectButton.title = "Inspect window"
|
||||
inspectTimer?.invalidate()
|
||||
inspectTimer = nil
|
||||
if let monitor = inspectClickMonitor {
|
||||
NSEvent.removeMonitor(monitor)
|
||||
inspectClickMonitor = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func updateInspectData() {
|
||||
let mouseLocation = NSEvent.mouseLocation
|
||||
guard let mainScreen = NSScreen.screens.first else { return }
|
||||
let cgMousePoint = CGPoint(x: mouseLocation.x, y: mainScreen.frame.height - mouseLocation.y)
|
||||
let myWid = CGWindowID(windowNumber)
|
||||
guard let found = CGWindow.windows(.optionOnScreenOnly).first(where: { win in
|
||||
guard let wid = win.id(), wid != myWid,
|
||||
let bounds = win.bounds() else { return false }
|
||||
return CGRect(x: bounds.origin.x, y: bounds.origin.y, width: bounds.width, height: bounds.height)
|
||||
.contains(cgMousePoint)
|
||||
}) else {
|
||||
clearInspectData()
|
||||
return
|
||||
}
|
||||
let wid = (found[kCGWindowNumber] as? Int).map { CGWindowID($0) }
|
||||
let pid = found.ownerPID()
|
||||
// App column
|
||||
var appName = "-"
|
||||
var appBundleId = "-"
|
||||
let pidStr = pid.map { String($0) } ?? "-"
|
||||
if let pid {
|
||||
let app = NSRunningApplication(processIdentifier: pid)
|
||||
appName = app?.localizedName ?? "-"
|
||||
appBundleId = app?.bundleIdentifier ?? "-"
|
||||
}
|
||||
inspectAppField.stringValue = Self.formatColumn("App", [
|
||||
("Name", appName), ("BundleID", appBundleId), ("PID", pidStr),
|
||||
])
|
||||
// CG column
|
||||
let cgTitle = found.title() ?? "-"
|
||||
let widStr = wid.map { "\($0)" } ?? "-"
|
||||
let cgLayer = found.layer().map { "\($0)" } ?? "-"
|
||||
let cgsLevel = wid.map { "\($0.level())" } ?? "-"
|
||||
let bounds = found.bounds()
|
||||
let cgSize = bounds.map { "\(Int($0.width))x\(Int($0.height))" } ?? "-"
|
||||
let cgPos = bounds.map { "\(Int($0.origin.x)),\(Int($0.origin.y))" } ?? "-"
|
||||
let alpha = (found[kCGWindowAlpha] as? Double).map { "\($0)" } ?? "-"
|
||||
let isOnScreen = (found[kCGWindowIsOnscreen] as? Bool).map { "\($0)" } ?? "-"
|
||||
inspectCgField.stringValue = Self.formatColumn("Window (CG API)", [
|
||||
("Title", cgTitle), ("WID", widStr),
|
||||
("Level (kCGWindowLayer)", cgLayer), ("Level (CGSGetWindowLevel)", cgsLevel),
|
||||
("Size", cgSize), ("Position", cgPos), ("Alpha", alpha), ("IsOnScreen", isOnScreen),
|
||||
])
|
||||
// AX column
|
||||
var axTitle = "-"
|
||||
var axRole = "-"
|
||||
var axSubrole = "-"
|
||||
var axMinimized = "-"
|
||||
var axFullscreen = "-"
|
||||
var axSize = "-"
|
||||
var axPosition = "-"
|
||||
if let pid, let wid {
|
||||
let axApp = AXUIElementCreateApplication(pid)
|
||||
if let axWindows = try? axApp.attributes([kAXWindowsAttribute]).windows {
|
||||
for axWin in axWindows {
|
||||
if let axWid = try? axWin.cgWindowId(), axWid == wid {
|
||||
let attrs = try? axWin.attributes([kAXTitleAttribute, kAXRoleAttribute, kAXSubroleAttribute,
|
||||
kAXMinimizedAttribute, kAXFullscreenAttribute,
|
||||
kAXSizeAttribute, kAXPositionAttribute])
|
||||
axTitle = attrs?.title ?? "-"
|
||||
axRole = attrs?.role ?? "-"
|
||||
axSubrole = attrs?.subrole ?? "-"
|
||||
axMinimized = attrs?.isMinimized.map { "\($0)" } ?? "-"
|
||||
axFullscreen = attrs?.isFullscreen.map { "\($0)" } ?? "-"
|
||||
if let s = attrs?.size { axSize = "\(Int(s.width))x\(Int(s.height))" }
|
||||
if let p = attrs?.position { axPosition = "\(Int(p.x)),\(Int(p.y))" }
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
inspectAxField.stringValue = Self.formatColumn("Window (AX API)", [
|
||||
("Title", axTitle), ("Role", axRole), ("Subrole", axSubrole),
|
||||
("Minimized", axMinimized), ("Fullscreen", axFullscreen),
|
||||
("Size", axSize), ("Position", axPosition),
|
||||
])
|
||||
}
|
||||
|
||||
private func clearInspectData() {
|
||||
inspectAppField.stringValue = Self.formatColumn("App", [("Name", "-"), ("BundleID", "-"), ("PID", "-")])
|
||||
inspectCgField.stringValue = Self.formatColumn("Window (CG API)", [
|
||||
("Title", "-"), ("WID", "-"), ("Level (kCGWindowLayer)", "-"), ("Level (CGSGetWindowLevel)", "-"),
|
||||
("Size", "-"), ("Position", "-"), ("Alpha", "-"), ("IsOnScreen", "-"),
|
||||
])
|
||||
inspectAxField.stringValue = Self.formatColumn("Window (AX API)", [
|
||||
("Title", "-"), ("Role", "-"), ("Subrole", "-"), ("Minimized", "-"), ("Fullscreen", "-"), ("Size", "-"), ("Position", "-"),
|
||||
])
|
||||
}
|
||||
|
||||
private static func makeInspectColumn(_ title: String, _ labels: [String]) -> NSTextField {
|
||||
let field = NSTextField(labelWithString: formatColumn(title, labels.map { ($0, "-") }))
|
||||
field.translatesAutoresizingMaskIntoConstraints = false
|
||||
field.font = logFont
|
||||
field.isSelectable = true
|
||||
field.maximumNumberOfLines = 0
|
||||
field.cell?.lineBreakMode = .byTruncatingTail
|
||||
field.cell?.truncatesLastVisibleLine = true
|
||||
field.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||
field.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
return field
|
||||
}
|
||||
|
||||
private static func formatColumn(_ title: String, _ rows: [(String, String)]) -> String {
|
||||
return "\(title)\n" + rows.map { "• \($0.0): \($0.1)" }.joined(separator: "\n")
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ class Menubar {
|
||||
addMenuItem(NSLocalizedString("Check permissions…", comment: "Menubar option"), #selector(App.app.checkPermissions), "", "hand.raised")
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
addMenuItem(String(format: NSLocalizedString("About %@", comment: "Menubar option. %@ is AltTab"), App.name), #selector(App.app.showAboutWindow), "", "info.circle")
|
||||
addMenuItem(NSLocalizedString("Debug tools", comment: "Menubar option"), #selector(App.app.showDebugWindow), "", "scope")
|
||||
addMenuItem(NSLocalizedString("Send feedback…", comment: "Menubar option"), #selector(App.app.showFeedbackPanel), "", "text.bubble")
|
||||
addMenuItem(NSLocalizedString("Support this project", comment: "Menubar option"), App.supportProjectAction, "", "heart.fill", .red)
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
|
||||
Reference in new issue
Block a user